src/eric7/QScintilla/Editor.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9203
b201a2ffe174
child 9214
bd28e56047d7
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the editor component of the eric IDE.
8 """
9
10 import bisect
11 import collections
12 import contextlib
13 import difflib
14 import os
15 import pathlib
16 import re
17
18 import editorconfig
19
20 from PyQt6.QtCore import (
21 pyqtSignal, pyqtSlot, Qt, QDir, QTimer, QModelIndex, QCryptographicHash,
22 QEvent, QDateTime, QPoint, QSize
23 )
24 from PyQt6.QtGui import QPalette, QFont, QPixmap, QPainter, QActionGroup
25 from PyQt6.QtWidgets import (
26 QLineEdit, QDialog, QInputDialog, QApplication, QMenu
27 )
28 from PyQt6.QtPrintSupport import QPrinter, QPrintDialog, QAbstractPrintDialog
29 from PyQt6.Qsci import QsciScintilla, QsciMacro, QsciStyledText
30
31 from EricWidgets.EricApplication import ericApp
32 from EricWidgets import EricFileDialog, EricMessageBox
33 from EricGui.EricOverrideCursor import EricOverrideCursor
34
35 from EricUtilities.EricCache import EricCache
36
37 from .QsciScintillaCompat import QsciScintillaCompat
38 from .EditorMarkerMap import EditorMarkerMap
39 from .SpellChecker import SpellChecker
40
41 from Globals import recentNameBreakpointConditions
42
43 import Preferences
44 import Utilities
45 from Utilities import MouseUtilities
46
47 import UI.PixmapCache
48
49 from UI import PythonDisViewer
50
51 EditorAutoCompletionListID = 1
52 TemplateCompletionListID = 2
53 ReferencesListID = 3
54
55 ReferenceItem = collections.namedtuple(
56 "ReferenceItem", ["modulePath", "codeLine", "line", "column"])
57
58
59 class Editor(QsciScintillaCompat):
60 """
61 Class implementing the editor component of the eric IDE.
62
63 @signal modificationStatusChanged(bool, QsciScintillaCompat) emitted when
64 the modification status has changed
65 @signal undoAvailable(bool) emitted to signal the undo availability
66 @signal redoAvailable(bool) emitted to signal the redo availability
67 @signal cursorChanged(str, int, int) emitted when the cursor position
68 was changed
69 @signal cursorLineChanged(int) emitted when the cursor line was changed
70 @signal editorAboutToBeSaved(str) emitted before the editor is saved
71 @signal editorSaved(str) emitted after the editor has been saved
72 @signal editorRenamed(str) emitted after the editor got a new name
73 (i.e. after a 'Save As')
74 @signal captionChanged(str, QsciScintillaCompat) emitted when the caption
75 is updated. Typically due to a readOnly attribute change.
76 @signal breakpointToggled(QsciScintillaCompat) emitted when a breakpoint
77 is toggled
78 @signal bookmarkToggled(QsciScintillaCompat) emitted when a bookmark is
79 toggled
80 @signal syntaxerrorToggled(QsciScintillaCompat) emitted when a syntax error
81 was discovered
82 @signal autoCompletionAPIsAvailable(bool) emitted after the autocompletion
83 function has been configured
84 @signal coverageMarkersShown(bool) emitted after the coverage markers have
85 been shown or cleared
86 @signal taskMarkersUpdated(QsciScintillaCompat) emitted when the task
87 markers were updated
88 @signal changeMarkersUpdated(QsciScintillaCompat) emitted when the change
89 markers were updated
90 @signal showMenu(str, QMenu, QsciScintillaCompat) emitted when a menu is
91 about to be shown. The name of the menu, a reference to the menu and
92 a reference to the editor are given.
93 @signal languageChanged(str) emitted when the editors language was set. The
94 language is passed as a parameter.
95 @signal eolChanged(str) emitted when the editors eol type was set. The eol
96 string is passed as a parameter.
97 @signal encodingChanged(str) emitted when the editors encoding was set. The
98 encoding name is passed as a parameter.
99 @signal spellLanguageChanged(str) emitted when the editor spell check
100 language was set. The language is passed as a parameter.
101 @signal lastEditPositionAvailable() emitted when a last edit position is
102 available
103 @signal refreshed() emitted to signal a refresh of the editor contents
104 @signal settingsRead() emitted to signal, that the settings have been read
105 and set
106 @signal mouseDoubleClick(position, buttons) emitted to signal a mouse
107 double click somewhere in the editor area
108 """
109 modificationStatusChanged = pyqtSignal(bool, QsciScintillaCompat)
110 undoAvailable = pyqtSignal(bool)
111 redoAvailable = pyqtSignal(bool)
112 cursorChanged = pyqtSignal(str, int, int)
113 cursorLineChanged = pyqtSignal(int)
114 editorAboutToBeSaved = pyqtSignal(str)
115 editorSaved = pyqtSignal(str)
116 editorRenamed = pyqtSignal(str)
117 captionChanged = pyqtSignal(str, QsciScintillaCompat)
118 breakpointToggled = pyqtSignal(QsciScintillaCompat)
119 bookmarkToggled = pyqtSignal(QsciScintillaCompat)
120 syntaxerrorToggled = pyqtSignal(QsciScintillaCompat)
121 autoCompletionAPIsAvailable = pyqtSignal(bool)
122 coverageMarkersShown = pyqtSignal(bool)
123 taskMarkersUpdated = pyqtSignal(QsciScintillaCompat)
124 changeMarkersUpdated = pyqtSignal(QsciScintillaCompat)
125 showMenu = pyqtSignal(str, QMenu, QsciScintillaCompat)
126 languageChanged = pyqtSignal(str)
127 eolChanged = pyqtSignal(str)
128 encodingChanged = pyqtSignal(str)
129 spellLanguageChanged = pyqtSignal(str)
130 lastEditPositionAvailable = pyqtSignal()
131 refreshed = pyqtSignal()
132 settingsRead = pyqtSignal()
133 mouseDoubleClick = pyqtSignal(QPoint, int)
134
135 WarningCode = 1
136 WarningStyle = 2
137
138 # Autocompletion icon definitions
139 ClassID = 1
140 ClassProtectedID = 2
141 ClassPrivateID = 3
142 MethodID = 4
143 MethodProtectedID = 5
144 MethodPrivateID = 6
145 AttributeID = 7
146 AttributeProtectedID = 8
147 AttributePrivateID = 9
148 EnumID = 10
149 KeywordsID = 11
150 ModuleID = 12
151
152 FromDocumentID = 99
153
154 TemplateImageID = 100
155
156 # Cooperation related definitions
157 Separator = "@@@"
158
159 StartEditToken = "START_EDIT"
160 EndEditToken = "END_EDIT"
161 CancelEditToken = "CANCEL_EDIT"
162 RequestSyncToken = "REQUEST_SYNC"
163 SyncToken = "SYNC"
164
165 VcsConflictMarkerLineRegExpList = (
166 r"""^<<<<<<< .*?$""",
167 r"""^\|\|\|\|\|\|\| .*?$""",
168 r"""^=======.*?$""",
169 r"""^>>>>>>> .*?$""",
170 )
171
172 EncloseChars = {
173 '"': '"',
174 "'": "'",
175 "(": "()",
176 ")": "()",
177 "{": "{}", # __IGNORE_WARNING_M613__
178 "}": "{}", # __IGNORE_WARNING_M613__
179 "[": "[]",
180 "]": "[]",
181 "<": "<>",
182 ">": "<>",
183 }
184
185 def __init__(self, dbs, fn="", vm=None,
186 filetype="", editor=None, tv=None,
187 parent=None):
188 """
189 Constructor
190
191 @param dbs reference to the debug server object
192 @type DebugServer
193 @param fn name of the file to be opened. If it is None, a new (empty)
194 editor is opened.
195 @type str
196 @param vm reference to the view manager object
197 @type ViewManager
198 @param filetype type of the source file
199 @type str
200 @param editor reference to an Editor object, if this is a cloned view
201 @type Editor
202 @param tv reference to the task viewer object
203 @type TaskViewer
204 @param parent reference to the parent widget
205 @type QWidget
206 @exception OSError raised to indicate an issue accessing the file
207 """
208 super().__init__(parent)
209 self.setAttribute(Qt.WidgetAttribute.WA_KeyCompression)
210 self.setUtf8(True)
211
212 self.enableMultiCursorSupport()
213
214 self.dbs = dbs
215 self.taskViewer = tv
216 self.__setFileName(fn)
217 self.vm = vm
218 self.filetype = filetype
219 self.filetypeByFlag = False
220 self.noName = ""
221 self.project = ericApp().getObject("Project")
222
223 # clear some variables
224 self.lastHighlight = None # remember the last highlighted line
225 self.lastErrorMarker = None # remember the last error line
226 self.lastCurrMarker = None # remember the last current line
227
228 self.breaks = {}
229 # key: marker handle,
230 # value: (lineno, condition, temporary,
231 # enabled, ignorecount)
232 self.bookmarks = []
233 # bookmarks are just a list of handles to the
234 # bookmark markers
235 self.syntaxerrors = {}
236 # key: marker handle
237 # value: list of (error message, error index)
238 self.warnings = {}
239 # key: marker handle
240 # value: list of (warning message, warning type)
241 self.notcoveredMarkers = [] # just a list of marker handles
242 self.showingNotcoveredMarkers = False
243
244 self.lexer_ = None
245 self.apiLanguage = ''
246
247 self.__loadEditorConfig()
248
249 self.__lexerReset = False
250 self.completer = None
251 self.encoding = self.__getEditorConfig("DefaultEncoding")
252 self.lastModified = 0
253 self.line = -1
254 self.inReopenPrompt = False
255 # true if the prompt to reload a changed source is present
256 self.inFileRenamed = False
257 # true if we are propagating a rename action
258 self.inLanguageChanged = False
259 # true if we are propagating a language change
260 self.inEolChanged = False
261 # true if we are propagating an eol change
262 self.inEncodingChanged = False
263 # true if we are propagating an encoding change
264 self.inDragDrop = False
265 # true if we are in drop mode
266 self.inLinesChanged = False
267 # true if we are propagating a lines changed event
268 self.__hasTaskMarkers = False
269 # no task markers present
270
271 self.macros = {} # list of defined macros
272 self.curMacro = None
273 self.recording = False
274
275 self.acAPI = False
276
277 self.__lastEditPosition = None
278 self.__annotationLines = 0
279
280 self.__docstringGenerator = None
281
282 # list of clones
283 self.__clones = []
284
285 # clear QScintilla defined keyboard commands
286 # we do our own handling through the view manager
287 self.clearAlternateKeys()
288 self.clearKeys()
289
290 self.__markerMap = EditorMarkerMap(self)
291
292 # initialize the mark occurrences timer
293 self.__markOccurrencesTimer = QTimer(self)
294 self.__markOccurrencesTimer.setSingleShot(True)
295 self.__markOccurrencesTimer.setInterval(
296 Preferences.getEditor("MarkOccurrencesTimeout"))
297 self.__markOccurrencesTimer.timeout.connect(self.__markOccurrences)
298 self.__markedText = ""
299 self.__searchIndicatorLines = []
300
301 # initialize some spellchecking stuff
302 self.spell = None
303 self.lastLine = 0
304 self.lastIndex = 0
305 self.__inSpellLanguageChanged = False
306
307 # initialize some cooperation stuff
308 self.__isSyncing = False
309 self.__receivedWhileSyncing = []
310 self.__savedText = ""
311 self.__inSharedEdit = False
312 self.__isShared = False
313 self.__inRemoteSharedEdit = False
314
315 # connect signals before loading the text
316 self.modificationChanged.connect(self.__modificationChanged)
317 self.cursorPositionChanged.connect(self.__cursorPositionChanged)
318 self.modificationAttempted.connect(self.__modificationReadOnly)
319
320 # define the margins markers
321 self.__changeMarkerSaved = self.markerDefine(
322 self.__createChangeMarkerPixmap(
323 "OnlineChangeTraceMarkerSaved"))
324 self.__changeMarkerUnsaved = self.markerDefine(
325 self.__createChangeMarkerPixmap(
326 "OnlineChangeTraceMarkerUnsaved"))
327 self.breakpoint = self.markerDefine(
328 UI.PixmapCache.getPixmap("break"))
329 self.cbreakpoint = self.markerDefine(
330 UI.PixmapCache.getPixmap("cBreak"))
331 self.tbreakpoint = self.markerDefine(
332 UI.PixmapCache.getPixmap("tBreak"))
333 self.tcbreakpoint = self.markerDefine(
334 UI.PixmapCache.getPixmap("tCBreak"))
335 self.dbreakpoint = self.markerDefine(
336 UI.PixmapCache.getPixmap("breakDisabled"))
337 self.bookmark = self.markerDefine(
338 UI.PixmapCache.getPixmap("bookmark16"))
339 self.syntaxerror = self.markerDefine(
340 UI.PixmapCache.getPixmap("syntaxError"))
341 self.notcovered = self.markerDefine(
342 UI.PixmapCache.getPixmap("notcovered"))
343 self.taskmarker = self.markerDefine(
344 UI.PixmapCache.getPixmap("task"))
345 self.warning = self.markerDefine(
346 UI.PixmapCache.getPixmap("warning"))
347
348 # define the line markers
349 if Preferences.getEditor("LineMarkersBackground"):
350 self.currentline = self.markerDefine(
351 QsciScintilla.MarkerSymbol.Background)
352 self.errorline = self.markerDefine(
353 QsciScintilla.MarkerSymbol.Background)
354 self.__setLineMarkerColours()
355 else:
356 self.currentline = self.markerDefine(
357 UI.PixmapCache.getPixmap("currentLineMarker"))
358 self.errorline = self.markerDefine(
359 UI.PixmapCache.getPixmap("errorLineMarker"))
360
361 self.breakpointMask = (
362 (1 << self.breakpoint) |
363 (1 << self.cbreakpoint) |
364 (1 << self.tbreakpoint) |
365 (1 << self.tcbreakpoint) |
366 (1 << self.dbreakpoint)
367 )
368
369 self.changeMarkersMask = (
370 (1 << self.__changeMarkerSaved) |
371 (1 << self.__changeMarkerUnsaved)
372 )
373
374 # configure the margins
375 self.__setMarginsDisplay()
376 self.linesChanged.connect(self.__resizeLinenoMargin)
377
378 self.marginClicked.connect(self.__marginClicked)
379
380 # set the eol mode
381 self.__setEolMode()
382
383 # set the text display
384 self.__setTextDisplay()
385
386 # initialize the online syntax check timer
387 try:
388 self.syntaxCheckService = ericApp().getObject('SyntaxCheckService')
389 self.syntaxCheckService.syntaxChecked.connect(
390 self.__processSyntaxCheckResult)
391 self.syntaxCheckService.error.connect(
392 self.__processSyntaxCheckError)
393 self.__initOnlineSyntaxCheck()
394 except KeyError:
395 self.syntaxCheckService = None
396
397 self.isResourcesFile = False
398 if editor is None:
399 if self.fileName:
400 if (
401 (pathlib.Path(self.fileName).stat().st_size // 1024) >
402 Preferences.getEditor("WarnFilesize")
403 ):
404 res = EricMessageBox.yesNo(
405 self,
406 self.tr("Open File"),
407 self.tr("""<p>The size of the file <b>{0}</b>"""
408 """ is <b>{1} KB</b>."""
409 """ Do you really want to load it?</p>""")
410 .format(
411 self.fileName,
412 pathlib.Path(self.fileName).stat().st_size // 1024
413 ),
414 icon=EricMessageBox.Warning)
415 if not res:
416 raise OSError()
417 self.readFile(self.fileName, True)
418 self.__bindLexer(self.fileName)
419 self.__bindCompleter(self.fileName)
420 self.checkSyntax()
421 self.isResourcesFile = self.fileName.endswith(".qrc")
422
423 self.__convertTabs()
424
425 self.recolor()
426 else:
427 # clone the given editor
428 self.setDocument(editor.document())
429 self.breaks = editor.breaks
430 self.bookmarks = editor.bookmarks
431 self.syntaxerrors = editor.syntaxerrors
432 self.notcoveredMarkers = editor.notcoveredMarkers
433 self.showingNotcoveredMarkers = editor.showingNotcoveredMarkers
434 self.isResourcesFile = editor.isResourcesFile
435 self.lastModified = editor.lastModified
436
437 self.addClone(editor)
438 editor.addClone(self)
439
440 self.gotoLine(1)
441
442 # connect the mouse hover signals
443 self.SCN_DWELLSTART.connect(self.__showMouseHoverHelp)
444 self.SCN_DWELLEND.connect(self.__cancelMouseHoverHelp)
445 self.__mouseHoverHelp = None
446 self.__showingMouseHoverHelp = False
447
448 # set the text display again
449 self.__setTextDisplay()
450
451 # set the auto-completion function
452 self.__acContext = True
453 self.__acText = ""
454 self.__acCompletions = set()
455 self.__acCompletionsFinished = 0
456 self.__acCache = EricCache(
457 size=Preferences.getEditor("AutoCompletionCacheSize"))
458 self.__acCache.setMaximumCacheTime(
459 Preferences.getEditor("AutoCompletionCacheTime"))
460 self.__acCacheEnabled = Preferences.getEditor(
461 "AutoCompletionCacheEnabled")
462 self.__acTimer = QTimer(self)
463 self.__acTimer.setSingleShot(True)
464 self.__acTimer.setInterval(
465 Preferences.getEditor("AutoCompletionTimeout"))
466 self.__acTimer.timeout.connect(self.__autoComplete)
467
468 self.__acWatchdog = QTimer(self)
469 self.__acWatchdog.setSingleShot(True)
470 self.__acWatchdog.setInterval(
471 Preferences.getEditor("AutoCompletionWatchdogTime"))
472 self.__acWatchdog.timeout.connect(self.autoCompleteQScintilla)
473
474 self.userListActivated.connect(self.__completionListSelected)
475 self.SCN_CHARADDED.connect(self.__charAdded)
476 self.SCN_AUTOCCANCELLED.connect(self.__autocompletionCancelled)
477
478 self.__completionListHookFunctions = {}
479 self.__completionListAsyncHookFunctions = {}
480 self.__setAutoCompletion()
481
482 # set the call-tips function
483 self.__ctHookFunctions = {}
484 self.__setCallTips()
485
486 # set the mouse click handlers (fired on mouse release)
487 self.__mouseClickHandlers = {}
488 # dictionary with tuple of keyboard modifier and mouse button as key
489 # and tuple of plug-in name and function as value
490
491 sh = self.sizeHint()
492 if sh.height() < 300:
493 sh.setHeight(300)
494 self.resize(sh)
495
496 # Make sure tabbing through a QWorkspace works.
497 self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
498
499 self.__updateReadOnly(True)
500
501 self.setWhatsThis(self.tr(
502 """<b>A Source Editor Window</b>"""
503 """<p>This window is used to display and edit a source file."""
504 """ You can open as many of these as you like. The name of the"""
505 """ file is displayed in the window's titlebar.</p>"""
506 """<p>In order to set breakpoints just click in the space"""
507 """ between the line numbers and the fold markers. Via the"""
508 """ context menu of the margins they may be edited.</p>"""
509 """<p>In order to set bookmarks just Shift click in the space"""
510 """ between the line numbers and the fold markers.</p>"""
511 """<p>These actions can be reversed via the context menu.</p>"""
512 """<p>Ctrl clicking on a syntax error marker shows some info"""
513 """ about this error.</p>"""
514 ))
515
516 # Set the editors size, if it is too big for the view manager.
517 if self.vm is not None:
518 req = self.size()
519 bnd = req.boundedTo(self.vm.size())
520
521 if bnd.width() < req.width() or bnd.height() < req.height():
522 self.resize(bnd)
523
524 # set the autosave flag
525 self.autosaveEnabled = Preferences.getEditor("AutosaveInterval") > 0
526 self.autosaveManuallyDisabled = False
527
528 # code coverage related attributes
529 self.__coverageFile = ""
530
531 self.__initContextMenu()
532 self.__initContextMenuMargins()
533
534 self.__checkEol()
535 if editor is None:
536 self.__checkLanguage()
537 self.__checkEncoding()
538 self.__checkSpellLanguage()
539 else:
540 # it's a clone
541 self.__languageChanged(editor.apiLanguage, propagate=False)
542 self.__encodingChanged(editor.encoding, propagate=False)
543 self.__spellLanguageChanged(editor.getSpellingLanguage(),
544 propagate=False)
545 # link the warnings to the original editor
546 self.warnings = editor.warnings
547
548 self.setAcceptDrops(True)
549
550 # breakpoint handling
551 self.breakpointModel = self.dbs.getBreakPointModel()
552 self.__restoreBreakpoints()
553 self.breakpointModel.rowsAboutToBeRemoved.connect(
554 self.__deleteBreakPoints)
555 self.breakpointModel.dataAboutToBeChanged.connect(
556 self.__breakPointDataAboutToBeChanged)
557 self.breakpointModel.dataChanged.connect(
558 self.__changeBreakPoints)
559 self.breakpointModel.rowsInserted.connect(
560 self.__addBreakPoints)
561 self.SCN_MODIFIED.connect(self.__modified)
562
563 # establish connection to some ViewManager action groups
564 self.addActions(self.vm.editorActGrp.actions())
565 self.addActions(self.vm.editActGrp.actions())
566 self.addActions(self.vm.copyActGrp.actions())
567 self.addActions(self.vm.viewActGrp.actions())
568
569 # register images to be shown in autocompletion lists
570 self.__registerImages()
571
572 # connect signals after loading the text
573 self.textChanged.connect(self.__textChanged)
574
575 # initialize the online change trace timer
576 self.__initOnlineChangeTrace()
577
578 if (
579 self.fileName and
580 self.project.isOpen() and
581 self.project.isProjectSource(self.fileName)
582 ):
583 self.project.projectPropertiesChanged.connect(
584 self.__projectPropertiesChanged)
585
586 self.grabGesture(Qt.GestureType.PinchGesture)
587
588 self.SCN_ZOOM.connect(self.__markerMap.update)
589 self.__markerMap.update()
590
591 def __setFileName(self, name):
592 """
593 Private method to set the file name of the current file.
594
595 @param name name of the current file
596 @type str
597 """
598 self.fileName = name
599
600 if self.fileName:
601 self.__fileNameExtension = (
602 os.path.splitext(self.fileName)[1][1:].lower()
603 )
604 else:
605 self.__fileNameExtension = ""
606
607 def __registerImages(self):
608 """
609 Private method to register images for autocompletion lists.
610 """
611 # finale size of the completion images
612 imageSize = QSize(22, 22)
613
614 self.registerImage(
615 self.ClassID,
616 UI.PixmapCache.getPixmap("class", imageSize))
617 self.registerImage(
618 self.ClassProtectedID,
619 UI.PixmapCache.getPixmap("class_protected", imageSize))
620 self.registerImage(
621 self.ClassPrivateID,
622 UI.PixmapCache.getPixmap("class_private", imageSize))
623 self.registerImage(
624 self.MethodID,
625 UI.PixmapCache.getPixmap("method", imageSize))
626 self.registerImage(
627 self.MethodProtectedID,
628 UI.PixmapCache.getPixmap("method_protected", imageSize))
629 self.registerImage(
630 self.MethodPrivateID,
631 UI.PixmapCache.getPixmap("method_private", imageSize))
632 self.registerImage(
633 self.AttributeID,
634 UI.PixmapCache.getPixmap("attribute", imageSize))
635 self.registerImage(
636 self.AttributeProtectedID,
637 UI.PixmapCache.getPixmap("attribute_protected", imageSize))
638 self.registerImage(
639 self.AttributePrivateID,
640 UI.PixmapCache.getPixmap("attribute_private", imageSize))
641 self.registerImage(
642 self.EnumID,
643 UI.PixmapCache.getPixmap("enum", imageSize))
644 self.registerImage(
645 self.KeywordsID,
646 UI.PixmapCache.getPixmap("keywords", imageSize))
647 self.registerImage(
648 self.ModuleID,
649 UI.PixmapCache.getPixmap("module", imageSize))
650
651 self.registerImage(
652 self.FromDocumentID,
653 UI.PixmapCache.getPixmap("editor", imageSize))
654
655 self.registerImage(
656 self.TemplateImageID,
657 UI.PixmapCache.getPixmap("templateViewer", imageSize))
658
659 def addClone(self, editor):
660 """
661 Public method to add a clone to our list.
662
663 @param editor reference to the cloned editor
664 @type Editor
665 """
666 self.__clones.append(editor)
667
668 editor.editorRenamed.connect(self.fileRenamed)
669 editor.languageChanged.connect(self.languageChanged)
670 editor.eolChanged.connect(self.__eolChanged)
671 editor.encodingChanged.connect(self.__encodingChanged)
672 editor.spellLanguageChanged.connect(self.__spellLanguageChanged)
673
674 def removeClone(self, editor):
675 """
676 Public method to remove a clone from our list.
677
678 @param editor reference to the cloned editor
679 @type Editor
680 """
681 if editor in self.__clones:
682 editor.editorRenamed.disconnect(self.fileRenamed)
683 editor.languageChanged.disconnect(self.languageChanged)
684 editor.eolChanged.disconnect(self.__eolChanged)
685 editor.encodingChanged.disconnect(self.__encodingChanged)
686 editor.spellLanguageChanged.disconnect(self.__spellLanguageChanged)
687 self.__clones.remove(editor)
688
689 def isClone(self, editor):
690 """
691 Public method to test, if the given editor is a clone.
692
693 @param editor reference to the cloned editor
694 @type Editor
695 @return flag indicating a clone
696 @rtype bool
697 """
698 return editor in self.__clones
699
700 def __bindName(self, line0):
701 """
702 Private method to generate a dummy filename for binding a lexer.
703
704 @param line0 first line of text to use in the generation process
705 (string)
706 @return dummy file name to be used for binding a lexer (string)
707 """
708 bindName = ""
709 line0 = line0.lower()
710
711 # check first line if it does not start with #!
712 if line0.startswith(("<html", "<!doctype html", "<?php")):
713 bindName = "dummy.html"
714 elif line0.startswith(("<?xml", "<!doctype")):
715 bindName = "dummy.xml"
716 elif line0.startswith("index: "):
717 bindName = "dummy.diff"
718 elif line0.startswith("\\documentclass"):
719 bindName = "dummy.tex"
720
721 if not bindName and self.filetype:
722 # check filetype
723 from . import Lexers
724 supportedLanguages = Lexers.getSupportedLanguages()
725 if self.filetype in supportedLanguages:
726 bindName = supportedLanguages[self.filetype][1]
727 elif self.filetype in ["Python", "Python3", "MicroPython"]:
728 bindName = "dummy.py"
729
730 if not bindName and line0.startswith("#!"):
731 # #! marker detection
732 if (
733 "python3" in line0 or
734 "python" in line0 or
735 "pypy3" in line0 or
736 "pypy" in line0
737 ):
738 bindName = "dummy.py"
739 self.filetype = "Python3"
740 elif ("/bash" in line0 or "/sh" in line0):
741 bindName = "dummy.sh"
742 elif "ruby" in line0:
743 bindName = "dummy.rb"
744 self.filetype = "Ruby"
745 elif "perl" in line0:
746 bindName = "dummy.pl"
747 elif "lua" in line0:
748 bindName = "dummy.lua"
749 elif "dmd" in line0:
750 bindName = "dummy.d"
751 self.filetype = "D"
752
753 if not bindName:
754 # mode line detection: -*- mode: python -*-
755 match = re.search(r"mode[:=]\s*([-\w_.]+)", line0)
756 if match:
757 mode = match.group(1).lower()
758 if mode in ["python3", "pypy3"]:
759 bindName = "dummy.py"
760 self.filetype = "Python3"
761 elif mode == "ruby":
762 bindName = "dummy.rb"
763 self.filetype = "Ruby"
764 elif mode == "perl":
765 bindName = "dummy.pl"
766 elif mode == "lua":
767 bindName = "dummy.lua"
768 elif mode in ["dmd", "d"]:
769 bindName = "dummy.d"
770 self.filetype = "D"
771
772 if not bindName:
773 bindName = self.fileName
774
775 return bindName
776
777 def getMenu(self, menuName):
778 """
779 Public method to get a reference to the main context menu or a submenu.
780
781 @param menuName name of the menu (string)
782 @return reference to the requested menu (QMenu) or None
783 """
784 try:
785 return self.__menus[menuName]
786 except KeyError:
787 return None
788
789 def hasMiniMenu(self):
790 """
791 Public method to check the miniMenu flag.
792
793 @return flag indicating a minimized context menu (boolean)
794 """
795 return self.miniMenu
796
797 def __initContextMenu(self):
798 """
799 Private method used to setup the context menu.
800 """
801 self.miniMenu = Preferences.getEditor("MiniContextMenu")
802
803 self.menuActs = {}
804 self.menu = QMenu()
805 self.__menus = {
806 "Main": self.menu,
807 }
808
809 self.languagesMenu = self.__initContextMenuLanguages()
810 self.__menus["Languages"] = self.languagesMenu
811 if self.isResourcesFile:
812 self.resourcesMenu = self.__initContextMenuResources()
813 self.__menus["Resources"] = self.resourcesMenu
814 else:
815 self.checksMenu = self.__initContextMenuChecks()
816 self.menuShow = self.__initContextMenuShow()
817 self.graphicsMenu = self.__initContextMenuGraphics()
818 self.autocompletionMenu = self.__initContextMenuAutocompletion()
819 self.__menus["Checks"] = self.checksMenu
820 self.__menus["Show"] = self.menuShow
821 self.__menus["Graphics"] = self.graphicsMenu
822 self.__menus["Autocompletion"] = self.autocompletionMenu
823 self.toolsMenu = self.__initContextMenuTools()
824 self.__menus["Tools"] = self.toolsMenu
825 self.eolMenu = self.__initContextMenuEol()
826 self.__menus["Eol"] = self.eolMenu
827 self.encodingsMenu = self.__initContextMenuEncodings()
828 self.__menus["Encodings"] = self.encodingsMenu
829 self.spellCheckMenu = self.__initContextMenuSpellCheck()
830 self.__menus["SpellCheck"] = self.spellCheckMenu
831
832 self.menuActs["Undo"] = self.menu.addAction(
833 UI.PixmapCache.getIcon("editUndo"),
834 self.tr('Undo'), self.undo)
835 self.menuActs["Redo"] = self.menu.addAction(
836 UI.PixmapCache.getIcon("editRedo"),
837 self.tr('Redo'), self.redo)
838 self.menuActs["Revert"] = self.menu.addAction(
839 self.tr("Revert to last saved state"),
840 self.revertToUnmodified)
841 self.menu.addSeparator()
842 self.menuActs["Cut"] = self.menu.addAction(
843 UI.PixmapCache.getIcon("editCut"),
844 self.tr('Cut'), self.cut)
845 self.menuActs["Copy"] = self.menu.addAction(
846 UI.PixmapCache.getIcon("editCopy"),
847 self.tr('Copy'), self.copy)
848 self.menuActs["Paste"] = self.menu.addAction(
849 UI.PixmapCache.getIcon("editPaste"),
850 self.tr('Paste'), self.paste)
851 if not self.miniMenu:
852 self.menu.addSeparator()
853 self.menu.addAction(
854 UI.PixmapCache.getIcon("editIndent"),
855 self.tr('Indent'), self.indentLineOrSelection)
856 self.menu.addAction(
857 UI.PixmapCache.getIcon("editUnindent"),
858 self.tr('Unindent'), self.unindentLineOrSelection)
859 self.menuActs["Comment"] = self.menu.addAction(
860 UI.PixmapCache.getIcon("editComment"),
861 self.tr('Comment'), self.commentLineOrSelection)
862 self.menuActs["Uncomment"] = self.menu.addAction(
863 UI.PixmapCache.getIcon("editUncomment"),
864 self.tr('Uncomment'), self.uncommentLineOrSelection)
865 self.menu.addSeparator()
866 self.menuActs["Docstring"] = self.menu.addAction(
867 self.tr("Generate Docstring"),
868 self.__insertDocstring)
869 self.menu.addSeparator()
870 self.menu.addAction(
871 self.tr('Select to brace'), self.selectToMatchingBrace)
872 self.menu.addAction(self.tr('Select all'), self.__selectAll)
873 self.menu.addAction(
874 self.tr('Deselect all'), self.__deselectAll)
875 self.menuActs["ExecuteSelection"] = self.menu.addAction(
876 self.tr("Execute Selection In Console"),
877 self.__executeSelection)
878 else:
879 self.menuActs["ExecuteSelection"] = None
880 self.menu.addSeparator()
881 self.menu.addMenu(self.spellCheckMenu)
882 self.menu.addSeparator()
883 self.menuActs["Languages"] = self.menu.addMenu(self.languagesMenu)
884 self.menuActs["Encodings"] = self.menu.addMenu(self.encodingsMenu)
885 self.menuActs["Eol"] = self.menu.addMenu(self.eolMenu)
886 self.menu.addSeparator()
887 self.menuActs["MonospacedFont"] = self.menu.addAction(
888 self.tr("Use Monospaced Font"),
889 self.handleMonospacedEnable)
890 self.menuActs["MonospacedFont"].setCheckable(True)
891 self.menuActs["MonospacedFont"].setChecked(self.useMonospaced)
892 self.menuActs["AutosaveEnable"] = self.menu.addAction(
893 self.tr("Autosave enabled"), self.__autosaveEnable)
894 self.menuActs["AutosaveEnable"].setCheckable(True)
895 self.menuActs["AutosaveEnable"].setChecked(self.autosaveEnabled)
896 self.menuActs["TypingAidsEnabled"] = self.menu.addAction(
897 self.tr("Typing aids enabled"), self.__toggleTypingAids)
898 self.menuActs["TypingAidsEnabled"].setCheckable(True)
899 self.menuActs["TypingAidsEnabled"].setEnabled(
900 self.completer is not None)
901 self.menuActs["TypingAidsEnabled"].setChecked(
902 self.completer is not None and self.completer.isEnabled())
903 self.menuActs["AutoCompletionEnable"] = self.menu.addAction(
904 self.tr("Automatic Completion enabled"),
905 self.__toggleAutoCompletionEnable)
906 self.menuActs["AutoCompletionEnable"].setCheckable(True)
907 self.menuActs["AutoCompletionEnable"].setChecked(
908 self.autoCompletionThreshold() != -1)
909 if not self.isResourcesFile:
910 self.menu.addMenu(self.autocompletionMenu)
911 self.menuActs["calltip"] = self.menu.addAction(
912 self.tr('Calltip'), self.callTip)
913 self.menuActs["codeInfo"] = self.menu.addAction(
914 self.tr('Code Info'), self.__showCodeInfo)
915 self.menu.addSeparator()
916 if self.isResourcesFile:
917 self.menu.addMenu(self.resourcesMenu)
918 else:
919 self.menuActs["Check"] = self.menu.addMenu(self.checksMenu)
920 self.menu.addSeparator()
921 self.menuActs["Show"] = self.menu.addMenu(self.menuShow)
922 self.menu.addSeparator()
923 self.menuActs["Diagrams"] = self.menu.addMenu(self.graphicsMenu)
924 self.menu.addSeparator()
925 self.menuActs["Tools"] = self.menu.addMenu(self.toolsMenu)
926 self.menu.addSeparator()
927 self.menu.addAction(
928 UI.PixmapCache.getIcon("documentNewView"),
929 self.tr('New Document View'), self.__newView)
930 self.menuActs["NewSplit"] = self.menu.addAction(
931 UI.PixmapCache.getIcon("splitVertical"),
932 self.tr('New Document View (with new split)'),
933 self.__newViewNewSplit)
934 self.menuActs["NewSplit"].setEnabled(self.vm.canSplit())
935 self.menu.addSeparator()
936 self.reopenEncodingMenu = self.__initContextMenuReopenWithEncoding()
937 self.menuActs["Reopen"] = self.menu.addMenu(self.reopenEncodingMenu)
938 self.menuActs["Save"] = self.menu.addAction(
939 UI.PixmapCache.getIcon("fileSave"),
940 self.tr('Save'), self.__contextSave)
941 self.menu.addAction(
942 UI.PixmapCache.getIcon("fileSaveAs"),
943 self.tr('Save As...'), self.__contextSaveAs)
944 self.menu.addAction(
945 UI.PixmapCache.getIcon("fileSaveCopy"),
946 self.tr('Save Copy...'), self.__contextSaveCopy)
947
948 self.menu.aboutToShow.connect(self.__showContextMenu)
949
950 self.spellingMenu = QMenu()
951 self.__menus["Spelling"] = self.spellingMenu
952
953 self.spellingMenu.aboutToShow.connect(self.__showContextMenuSpelling)
954 self.spellingMenu.triggered.connect(
955 self.__contextMenuSpellingTriggered)
956
957 def __initContextMenuAutocompletion(self):
958 """
959 Private method used to setup the Checks context sub menu.
960
961 @return reference to the generated menu
962 @rtype QMenu
963 """
964 menu = QMenu(self.tr('Complete'))
965
966 self.menuActs["acDynamic"] = menu.addAction(
967 self.tr('Complete'), self.autoComplete)
968 menu.addSeparator()
969 self.menuActs["acClearCache"] = menu.addAction(
970 self.tr("Clear Completions Cache"), self.__clearCompletionsCache)
971 menu.addSeparator()
972 menu.addAction(
973 self.tr('Complete from Document'), self.autoCompleteFromDocument)
974 self.menuActs["acAPI"] = menu.addAction(
975 self.tr('Complete from APIs'), self.autoCompleteFromAPIs)
976 self.menuActs["acAPIDocument"] = menu.addAction(
977 self.tr('Complete from Document and APIs'),
978 self.autoCompleteFromAll)
979
980 menu.aboutToShow.connect(self.__showContextMenuAutocompletion)
981
982 return menu
983
984 def __initContextMenuChecks(self):
985 """
986 Private method used to setup the Checks context sub menu.
987
988 @return reference to the generated menu
989 @rtype QMenu
990 """
991 menu = QMenu(self.tr('Check'))
992 menu.aboutToShow.connect(self.__showContextMenuChecks)
993 return menu
994
995 def __initContextMenuTools(self):
996 """
997 Private method used to setup the Tools context sub menu.
998
999 @return reference to the generated menu
1000 @rtype QMenu
1001 """
1002 menu = QMenu(self.tr('Tools'))
1003 menu.aboutToShow.connect(self.__showContextMenuTools)
1004 return menu
1005
1006 def __initContextMenuShow(self):
1007 """
1008 Private method used to setup the Show context sub menu.
1009
1010 @return reference to the generated menu
1011 @rtype QMenu
1012 """
1013 menu = QMenu(self.tr('Show'))
1014
1015 menu.addAction(self.tr('Code metrics...'), self.__showCodeMetrics)
1016 self.coverageMenuAct = menu.addAction(
1017 self.tr('Code coverage...'), self.__showCodeCoverage)
1018 self.coverageShowAnnotationMenuAct = menu.addAction(
1019 self.tr('Show code coverage annotations'),
1020 self.codeCoverageShowAnnotations)
1021 self.coverageHideAnnotationMenuAct = menu.addAction(
1022 self.tr('Hide code coverage annotations'),
1023 self.__codeCoverageHideAnnotations)
1024 self.profileMenuAct = menu.addAction(
1025 self.tr('Profile data...'), self.__showProfileData)
1026
1027 menu.aboutToShow.connect(self.__showContextMenuShow)
1028
1029 return menu
1030
1031 def __initContextMenuGraphics(self):
1032 """
1033 Private method used to setup the diagrams context sub menu.
1034
1035 @return reference to the generated menu
1036 @rtype QMenu
1037 """
1038 menu = QMenu(self.tr('Diagrams'))
1039
1040 menu.addAction(
1041 self.tr('Class Diagram...'), self.__showClassDiagram)
1042 menu.addAction(
1043 self.tr('Package Diagram...'), self.__showPackageDiagram)
1044 menu.addAction(
1045 self.tr('Imports Diagram...'), self.__showImportsDiagram)
1046 self.applicationDiagramMenuAct = menu.addAction(
1047 self.tr('Application Diagram...'),
1048 self.__showApplicationDiagram)
1049 menu.addSeparator()
1050 menu.addAction(
1051 UI.PixmapCache.getIcon("open"),
1052 self.tr("Load Diagram..."), self.__loadDiagram)
1053
1054 menu.aboutToShow.connect(self.__showContextMenuGraphics)
1055
1056 return menu
1057
1058 def __initContextMenuLanguages(self):
1059 """
1060 Private method used to setup the Languages context sub menu.
1061
1062 @return reference to the generated menu
1063 @rtype QMenu
1064 """
1065 menu = QMenu(self.tr("Languages"))
1066
1067 self.languagesActGrp = QActionGroup(self)
1068 self.noLanguageAct = menu.addAction(
1069 UI.PixmapCache.getIcon("fileText"),
1070 self.tr("Text"))
1071 self.noLanguageAct.setCheckable(True)
1072 self.noLanguageAct.setData("None")
1073 self.languagesActGrp.addAction(self.noLanguageAct)
1074 menu.addSeparator()
1075
1076 from . import Lexers
1077 self.supportedLanguages = {}
1078 supportedLanguages = Lexers.getSupportedLanguages()
1079 languages = sorted(supportedLanguages.keys())
1080 for language in languages:
1081 if language != "Guessed":
1082 self.supportedLanguages[language] = (
1083 supportedLanguages[language][:2]
1084 )
1085 act = menu.addAction(
1086 UI.PixmapCache.getIcon(supportedLanguages[language][2]),
1087 self.supportedLanguages[language][0])
1088 act.setCheckable(True)
1089 act.setData(language)
1090 self.supportedLanguages[language].append(act)
1091 self.languagesActGrp.addAction(act)
1092
1093 menu.addSeparator()
1094 self.pygmentsAct = menu.addAction(self.tr("Guessed"))
1095 self.pygmentsAct.setCheckable(True)
1096 self.pygmentsAct.setData("Guessed")
1097 self.languagesActGrp.addAction(self.pygmentsAct)
1098 self.pygmentsSelAct = menu.addAction(self.tr("Alternatives"))
1099 self.pygmentsSelAct.setData("Alternatives")
1100
1101 menu.triggered.connect(self.__languageMenuTriggered)
1102 menu.aboutToShow.connect(self.__showContextMenuLanguages)
1103
1104 return menu
1105
1106 def __initContextMenuEncodings(self):
1107 """
1108 Private method used to setup the Encodings context sub menu.
1109
1110 @return reference to the generated menu
1111 @rtype QMenu
1112 """
1113 self.supportedEncodings = {}
1114
1115 menu = QMenu(self.tr("Encodings"))
1116
1117 self.encodingsActGrp = QActionGroup(self)
1118
1119 for encoding in sorted(Utilities.supportedCodecs):
1120 act = menu.addAction(encoding)
1121 act.setCheckable(True)
1122 act.setData(encoding)
1123 self.supportedEncodings[encoding] = act
1124 self.encodingsActGrp.addAction(act)
1125
1126 menu.triggered.connect(self.__encodingsMenuTriggered)
1127 menu.aboutToShow.connect(self.__showContextMenuEncodings)
1128
1129 return menu
1130
1131 def __initContextMenuReopenWithEncoding(self):
1132 """
1133 Private method used to setup the Reopen With Encoding context sub menu.
1134
1135 @return reference to the generated menu
1136 @rtype QMenu
1137 """
1138 menu = QMenu(self.tr("Re-Open With Encoding"))
1139 menu.setIcon(UI.PixmapCache.getIcon("open"))
1140
1141 for encoding in sorted(Utilities.supportedCodecs):
1142 act = menu.addAction(encoding)
1143 act.setData(encoding)
1144
1145 menu.triggered.connect(self.__reopenWithEncodingMenuTriggered)
1146
1147 return menu
1148
1149 def __initContextMenuEol(self):
1150 """
1151 Private method to setup the eol context sub menu.
1152
1153 @return reference to the generated menu
1154 @rtype QMenu
1155 """
1156 self.supportedEols = {}
1157
1158 menu = QMenu(self.tr("End-of-Line Type"))
1159
1160 self.eolActGrp = QActionGroup(self)
1161
1162 act = menu.addAction(UI.PixmapCache.getIcon("eolLinux"),
1163 self.tr("Unix"))
1164 act.setCheckable(True)
1165 act.setData('\n')
1166 self.supportedEols['\n'] = act
1167 self.eolActGrp.addAction(act)
1168
1169 act = menu.addAction(UI.PixmapCache.getIcon("eolWindows"),
1170 self.tr("Windows"))
1171 act.setCheckable(True)
1172 act.setData('\r\n')
1173 self.supportedEols['\r\n'] = act
1174 self.eolActGrp.addAction(act)
1175
1176 act = menu.addAction(UI.PixmapCache.getIcon("eolMac"),
1177 self.tr("Macintosh"))
1178 act.setCheckable(True)
1179 act.setData('\r')
1180 self.supportedEols['\r'] = act
1181 self.eolActGrp.addAction(act)
1182
1183 menu.triggered.connect(self.__eolMenuTriggered)
1184 menu.aboutToShow.connect(self.__showContextMenuEol)
1185
1186 return menu
1187
1188 def __initContextMenuSpellCheck(self):
1189 """
1190 Private method used to setup the spell checking context sub menu.
1191
1192 @return reference to the generated menu
1193 @rtype QMenu
1194 """
1195 menu = QMenu(self.tr("Spelling"))
1196 menu.setIcon(UI.PixmapCache.getIcon("spellchecking"))
1197
1198 self.spellLanguagesMenu = self.__initContextMenuSpellLanguages()
1199 self.__menus["SpellLanguages"] = self.spellLanguagesMenu
1200
1201 self.menuActs["SpellCheck"] = menu.addAction(
1202 UI.PixmapCache.getIcon("spellchecking"),
1203 self.tr('Check spelling...'), self.checkSpelling)
1204 self.menuActs["SpellCheckSelection"] = menu.addAction(
1205 UI.PixmapCache.getIcon("spellchecking"),
1206 self.tr('Check spelling of selection...'),
1207 self.__checkSpellingSelection)
1208 self.menuActs["SpellCheckRemove"] = menu.addAction(
1209 self.tr("Remove from dictionary"),
1210 self.__removeFromSpellingDictionary)
1211 self.menuActs["SpellCheckLanguages"] = menu.addMenu(
1212 self.spellLanguagesMenu)
1213
1214 menu.aboutToShow.connect(self.__showContextMenuSpellCheck)
1215
1216 return menu
1217
1218 def __initContextMenuSpellLanguages(self):
1219 """
1220 Private method to setup the spell checking languages context sub menu.
1221
1222 @return reference to the generated menu
1223 @rtype QMenu
1224 """
1225 self.supportedSpellLanguages = {}
1226
1227 menu = QMenu(self.tr("Spell Check Languages"))
1228
1229 self.spellLanguagesActGrp = QActionGroup(self)
1230
1231 self.noSpellLanguageAct = menu.addAction(
1232 self.tr("No Language"))
1233 self.noSpellLanguageAct.setCheckable(True)
1234 self.noSpellLanguageAct.setData("")
1235 self.spellLanguagesActGrp.addAction(self.noSpellLanguageAct)
1236 menu.addSeparator()
1237
1238 for language in sorted(SpellChecker.getAvailableLanguages()):
1239 act = menu.addAction(language)
1240 act.setCheckable(True)
1241 act.setData(language)
1242 self.supportedSpellLanguages[language] = act
1243 self.spellLanguagesActGrp.addAction(act)
1244
1245 menu.triggered.connect(self.__spellLanguagesMenuTriggered)
1246 menu.aboutToShow.connect(self.__showContextMenuSpellLanguages)
1247
1248 return menu
1249
1250 def __initContextMenuMargins(self):
1251 """
1252 Private method used to setup the context menu for the margins.
1253 """
1254 self.marginMenuActs = {}
1255
1256 # bookmark margin
1257 self.bmMarginMenu = QMenu()
1258
1259 self.bmMarginMenu.addAction(
1260 self.tr('Toggle bookmark'), self.menuToggleBookmark)
1261 self.marginMenuActs["NextBookmark"] = self.bmMarginMenu.addAction(
1262 self.tr('Next bookmark'), self.nextBookmark)
1263 self.marginMenuActs["PreviousBookmark"] = self.bmMarginMenu.addAction(
1264 self.tr('Previous bookmark'), self.previousBookmark)
1265 self.marginMenuActs["ClearBookmark"] = self.bmMarginMenu.addAction(
1266 self.tr('Clear all bookmarks'), self.clearBookmarks)
1267
1268 self.bmMarginMenu.aboutToShow.connect(
1269 lambda: self.__showContextMenuMargin(self.bmMarginMenu))
1270
1271 # breakpoint margin
1272 self.bpMarginMenu = QMenu()
1273
1274 self.marginMenuActs["Breakpoint"] = self.bpMarginMenu.addAction(
1275 self.tr('Toggle breakpoint'), self.menuToggleBreakpoint)
1276 self.marginMenuActs["TempBreakpoint"] = self.bpMarginMenu.addAction(
1277 self.tr('Toggle temporary breakpoint'),
1278 self.__menuToggleTemporaryBreakpoint)
1279 self.marginMenuActs["EditBreakpoint"] = self.bpMarginMenu.addAction(
1280 self.tr('Edit breakpoint...'), self.menuEditBreakpoint)
1281 self.marginMenuActs["EnableBreakpoint"] = self.bpMarginMenu.addAction(
1282 self.tr('Enable breakpoint'),
1283 self.__menuToggleBreakpointEnabled)
1284 self.marginMenuActs["NextBreakpoint"] = self.bpMarginMenu.addAction(
1285 self.tr('Next breakpoint'), self.menuNextBreakpoint)
1286 self.marginMenuActs["PreviousBreakpoint"] = (
1287 self.bpMarginMenu.addAction(
1288 self.tr('Previous breakpoint'),
1289 self.menuPreviousBreakpoint)
1290 )
1291 self.marginMenuActs["ClearBreakpoint"] = self.bpMarginMenu.addAction(
1292 self.tr('Clear all breakpoints'), self.__menuClearBreakpoints)
1293
1294 self.bpMarginMenu.aboutToShow.connect(
1295 lambda: self.__showContextMenuMargin(self.bpMarginMenu))
1296
1297 # fold margin
1298 self.foldMarginMenu = QMenu()
1299
1300 self.marginMenuActs["ToggleAllFolds"] = (
1301 self.foldMarginMenu.addAction(
1302 self.tr("Toggle all folds"),
1303 self.foldAll)
1304 )
1305 self.marginMenuActs["ToggleAllFoldsAndChildren"] = (
1306 self.foldMarginMenu.addAction(
1307 self.tr("Toggle all folds (including children)"),
1308 lambda: self.foldAll(True))
1309 )
1310 self.marginMenuActs["ToggleCurrentFold"] = (
1311 self.foldMarginMenu.addAction(
1312 self.tr("Toggle current fold"),
1313 self.toggleCurrentFold)
1314 )
1315 self.foldMarginMenu.addSeparator()
1316 self.marginMenuActs["ExpandChildren"] = (
1317 self.foldMarginMenu.addAction(
1318 self.tr("Expand (including children)"),
1319 self.__contextMenuExpandFoldWithChildren)
1320 )
1321 self.marginMenuActs["CollapseChildren"] = (
1322 self.foldMarginMenu.addAction(
1323 self.tr("Collapse (including children)"),
1324 self.__contextMenuCollapseFoldWithChildren)
1325 )
1326 self.foldMarginMenu.addSeparator()
1327 self.marginMenuActs["ClearAllFolds"] = (
1328 self.foldMarginMenu.addAction(
1329 self.tr("Clear all folds"),
1330 self.clearFolds)
1331 )
1332
1333 self.foldMarginMenu.aboutToShow.connect(
1334 lambda: self.__showContextMenuMargin(self.foldMarginMenu))
1335
1336 # indicator margin
1337 self.indicMarginMenu = QMenu()
1338
1339 self.marginMenuActs["GotoSyntaxError"] = (
1340 self.indicMarginMenu.addAction(
1341 self.tr('Goto syntax error'), self.gotoSyntaxError)
1342 )
1343 self.marginMenuActs["ShowSyntaxError"] = (
1344 self.indicMarginMenu.addAction(
1345 self.tr('Show syntax error message'),
1346 self.__showSyntaxError)
1347 )
1348 self.marginMenuActs["ClearSyntaxError"] = (
1349 self.indicMarginMenu.addAction(
1350 self.tr('Clear syntax error'), self.clearSyntaxError)
1351 )
1352 self.indicMarginMenu.addSeparator()
1353 self.marginMenuActs["NextWarningMarker"] = (
1354 self.indicMarginMenu.addAction(
1355 self.tr("Next warning"), self.nextWarning)
1356 )
1357 self.marginMenuActs["PreviousWarningMarker"] = (
1358 self.indicMarginMenu.addAction(
1359 self.tr("Previous warning"), self.previousWarning)
1360 )
1361 self.marginMenuActs["ShowWarning"] = (
1362 self.indicMarginMenu.addAction(
1363 self.tr('Show warning message'), self.__showWarning)
1364 )
1365 self.marginMenuActs["ClearWarnings"] = (
1366 self.indicMarginMenu.addAction(
1367 self.tr('Clear warnings'), self.clearWarnings)
1368 )
1369 self.indicMarginMenu.addSeparator()
1370 self.marginMenuActs["NextCoverageMarker"] = (
1371 self.indicMarginMenu.addAction(
1372 self.tr('Next uncovered line'), self.nextUncovered)
1373 )
1374 self.marginMenuActs["PreviousCoverageMarker"] = (
1375 self.indicMarginMenu.addAction(
1376 self.tr('Previous uncovered line'), self.previousUncovered)
1377 )
1378 self.indicMarginMenu.addSeparator()
1379 self.marginMenuActs["NextTaskMarker"] = (
1380 self.indicMarginMenu.addAction(
1381 self.tr('Next task'), self.nextTask)
1382 )
1383 self.marginMenuActs["PreviousTaskMarker"] = (
1384 self.indicMarginMenu.addAction(
1385 self.tr('Previous task'), self.previousTask)
1386 )
1387 self.indicMarginMenu.addSeparator()
1388 self.marginMenuActs["NextChangeMarker"] = (
1389 self.indicMarginMenu.addAction(
1390 self.tr('Next change'), self.nextChange)
1391 )
1392 self.marginMenuActs["PreviousChangeMarker"] = (
1393 self.indicMarginMenu.addAction(
1394 self.tr('Previous change'), self.previousChange)
1395 )
1396 self.marginMenuActs["ClearChangeMarkers"] = (
1397 self.indicMarginMenu.addAction(
1398 self.tr('Clear changes'), self.__reinitOnlineChangeTrace)
1399 )
1400
1401 self.indicMarginMenu.aboutToShow.connect(
1402 lambda: self.__showContextMenuMargin(self.indicMarginMenu))
1403
1404 def exportFile(self, exporterFormat):
1405 """
1406 Public method to export the file.
1407
1408 @param exporterFormat format the file should be exported into (string)
1409 """
1410 if exporterFormat:
1411 from . import Exporters
1412 exporter = Exporters.getExporter(exporterFormat, self)
1413 if exporter:
1414 exporter.exportSource()
1415 else:
1416 EricMessageBox.critical(
1417 self,
1418 self.tr("Export source"),
1419 self.tr(
1420 """<p>No exporter available for the """
1421 """export format <b>{0}</b>. Aborting...</p>""")
1422 .format(exporterFormat))
1423 else:
1424 EricMessageBox.critical(
1425 self,
1426 self.tr("Export source"),
1427 self.tr("""No export format given. Aborting..."""))
1428
1429 def __showContextMenuLanguages(self):
1430 """
1431 Private slot handling the aboutToShow signal of the languages context
1432 menu.
1433 """
1434 if self.apiLanguage.startswith("Pygments|"):
1435 self.pygmentsSelAct.setText(
1436 self.tr("Alternatives ({0})").format(
1437 self.getLanguage(normalized=False)))
1438 else:
1439 self.pygmentsSelAct.setText(self.tr("Alternatives"))
1440 self.showMenu.emit("Languages", self.languagesMenu, self)
1441
1442 def __selectPygmentsLexer(self):
1443 """
1444 Private method to select a specific pygments lexer.
1445
1446 @return name of the selected pygments lexer (string)
1447 """
1448 from pygments.lexers import get_all_lexers
1449 lexerList = sorted(lex[0] for lex in get_all_lexers())
1450 try:
1451 lexerSel = lexerList.index(
1452 self.getLanguage(normalized=False, forPygments=True))
1453 except ValueError:
1454 lexerSel = 0
1455 lexerName, ok = QInputDialog.getItem(
1456 self,
1457 self.tr("Pygments Lexer"),
1458 self.tr("Select the Pygments lexer to apply."),
1459 lexerList,
1460 lexerSel,
1461 False)
1462 if ok and lexerName:
1463 return lexerName
1464 else:
1465 return ""
1466
1467 def __languageMenuTriggered(self, act):
1468 """
1469 Private method to handle the selection of a lexer language.
1470
1471 @param act reference to the action that was triggered (QAction)
1472 """
1473 if act == self.noLanguageAct:
1474 self.__resetLanguage()
1475 elif act == self.pygmentsAct:
1476 self.setLanguage("dummy.pygments")
1477 elif act == self.pygmentsSelAct:
1478 language = self.__selectPygmentsLexer()
1479 if language:
1480 self.setLanguage("dummy.pygments", pyname=language)
1481 else:
1482 language = act.data()
1483 if language:
1484 self.filetype = language
1485 self.setLanguage(self.supportedLanguages[language][1])
1486 self.checkSyntax()
1487
1488 self.__docstringGenerator = None
1489
1490 def __languageChanged(self, language, propagate=True):
1491 """
1492 Private slot handling a change of a connected editor's language.
1493
1494 @param language language to be set (string)
1495 @param propagate flag indicating to propagate the change (boolean)
1496 """
1497 if language == '':
1498 self.__resetLanguage(propagate=propagate)
1499 elif language == "Guessed":
1500 self.setLanguage("dummy.pygments",
1501 propagate=propagate)
1502 elif language.startswith("Pygments|"):
1503 pyname = language.split("|", 1)[1]
1504 self.setLanguage("dummy.pygments", pyname=pyname,
1505 propagate=propagate)
1506 else:
1507 self.filetype = language
1508 self.setLanguage(self.supportedLanguages[language][1],
1509 propagate=propagate)
1510 self.checkSyntax()
1511
1512 self.__docstringGenerator = None
1513
1514 def __resetLanguage(self, propagate=True):
1515 """
1516 Private method used to reset the language selection.
1517
1518 @param propagate flag indicating to propagate the change (boolean)
1519 """
1520 if (
1521 self.lexer_ is not None and
1522 (self.lexer_.lexer() == "container" or
1523 self.lexer_.lexer() is None)
1524 ):
1525 with contextlib.suppress(TypeError):
1526 self.SCN_STYLENEEDED.disconnect(self.__styleNeeded)
1527
1528 self.apiLanguage = ""
1529 self.lexer_ = None
1530 self.__lexerReset = True
1531 self.setLexer()
1532 if self.completer is not None:
1533 self.completer.setEnabled(False)
1534 self.completer = None
1535 useMonospaced = self.useMonospaced
1536 self.__setTextDisplay()
1537 self.__setMarginsDisplay()
1538 self.setMonospaced(useMonospaced)
1539 with contextlib.suppress(AttributeError):
1540 self.menuActs["MonospacedFont"].setChecked(self.useMonospaced)
1541
1542 self.__docstringGenerator = None
1543
1544 if not self.inLanguageChanged and propagate:
1545 self.inLanguageChanged = True
1546 self.languageChanged.emit(self.apiLanguage)
1547 self.inLanguageChanged = False
1548
1549 def setLanguage(self, filename, initTextDisplay=True, propagate=True,
1550 pyname=""):
1551 """
1552 Public method to set a lexer language.
1553
1554 @param filename filename used to determine the associated lexer
1555 language (string)
1556 @param initTextDisplay flag indicating an initialization of the text
1557 display is required as well (boolean)
1558 @param propagate flag indicating to propagate the change (boolean)
1559 @param pyname name of the pygments lexer to use (string)
1560 """
1561 # clear all warning and syntax error markers
1562 self.clearSyntaxError()
1563 self.clearWarnings()
1564
1565 self.menuActs["MonospacedFont"].setChecked(False)
1566
1567 self.__lexerReset = False
1568 self.__bindLexer(filename, pyname=pyname)
1569 self.__bindCompleter(filename)
1570 self.recolor()
1571 self.__checkLanguage()
1572
1573 self.__docstringGenerator = None
1574
1575 # set the text display
1576 if initTextDisplay:
1577 self.__setTextDisplay()
1578
1579 # set the auto-completion and call-tips function
1580 self.__setAutoCompletion()
1581 self.__setCallTips()
1582
1583 if not self.inLanguageChanged and propagate:
1584 self.inLanguageChanged = True
1585 self.languageChanged.emit(self.apiLanguage)
1586 self.inLanguageChanged = False
1587
1588 def __checkLanguage(self):
1589 """
1590 Private method to check the selected language of the language submenu.
1591 """
1592 if self.apiLanguage == "":
1593 self.noLanguageAct.setChecked(True)
1594 elif self.apiLanguage == "Guessed":
1595 self.pygmentsAct.setChecked(True)
1596 elif self.apiLanguage.startswith("Pygments|"):
1597 act = self.languagesActGrp.checkedAction()
1598 if act:
1599 act.setChecked(False)
1600 else:
1601 self.supportedLanguages[self.apiLanguage][2].setChecked(True)
1602
1603 def projectLexerAssociationsChanged(self):
1604 """
1605 Public slot to handle changes of the project lexer associations.
1606 """
1607 self.setLanguage(self.fileName)
1608
1609 def __showContextMenuEncodings(self):
1610 """
1611 Private slot handling the aboutToShow signal of the encodings context
1612 menu.
1613 """
1614 self.showMenu.emit("Encodings", self.encodingsMenu, self)
1615
1616 def __encodingsMenuTriggered(self, act):
1617 """
1618 Private method to handle the selection of an encoding.
1619
1620 @param act reference to the action that was triggered (QAction)
1621 """
1622 encoding = act.data()
1623 self.setModified(True)
1624 self.__encodingChanged("{0}-selected".format(encoding))
1625
1626 def __checkEncoding(self):
1627 """
1628 Private method to check the selected encoding of the encodings submenu.
1629 """
1630 with contextlib.suppress(AttributeError, KeyError):
1631 (self.supportedEncodings[self.__normalizedEncoding()]
1632 .setChecked(True))
1633
1634 def __encodingChanged(self, encoding, propagate=True):
1635 """
1636 Private slot to handle a change of the encoding.
1637
1638 @param encoding changed encoding (string)
1639 @param propagate flag indicating to propagate the change (boolean)
1640 """
1641 self.encoding = encoding
1642 self.__checkEncoding()
1643
1644 if not self.inEncodingChanged and propagate:
1645 self.inEncodingChanged = True
1646 self.encodingChanged.emit(self.encoding)
1647 self.inEncodingChanged = False
1648
1649 def __normalizedEncoding(self, encoding=""):
1650 """
1651 Private method to calculate the normalized encoding string.
1652
1653 @param encoding encoding to be normalized (string)
1654 @return normalized encoding (string)
1655 """
1656 if not encoding:
1657 encoding = self.encoding
1658 return (
1659 encoding
1660 .replace("-default", "")
1661 .replace("-guessed", "")
1662 .replace("-selected", "")
1663 )
1664
1665 def __showContextMenuEol(self):
1666 """
1667 Private slot handling the aboutToShow signal of the eol context menu.
1668 """
1669 self.showMenu.emit("Eol", self.eolMenu, self)
1670
1671 def __eolMenuTriggered(self, act):
1672 """
1673 Private method to handle the selection of an eol type.
1674
1675 @param act reference to the action that was triggered (QAction)
1676 """
1677 eol = act.data()
1678 self.setEolModeByEolString(eol)
1679 self.convertEols(self.eolMode())
1680
1681 def __checkEol(self):
1682 """
1683 Private method to check the selected eol type of the eol submenu.
1684 """
1685 with contextlib.suppress(AttributeError, TypeError):
1686 self.supportedEols[self.getLineSeparator()].setChecked(True)
1687
1688 def __eolChanged(self):
1689 """
1690 Private slot to handle a change of the eol mode.
1691 """
1692 self.__checkEol()
1693
1694 if not self.inEolChanged:
1695 self.inEolChanged = True
1696 eol = self.getLineSeparator()
1697 self.eolChanged.emit(eol)
1698 self.inEolChanged = False
1699
1700 def __showContextMenuSpellCheck(self):
1701 """
1702 Private slot handling the aboutToShow signal of the spell check
1703 context menu.
1704 """
1705 spellingAvailable = SpellChecker.isAvailable()
1706 self.menuActs["SpellCheck"].setEnabled(spellingAvailable)
1707 self.menuActs["SpellCheckSelection"].setEnabled(
1708 spellingAvailable and self.hasSelectedText())
1709 self.menuActs["SpellCheckRemove"].setEnabled(
1710 spellingAvailable and self.spellingMenuPos >= 0)
1711 self.menuActs["SpellCheckLanguages"].setEnabled(spellingAvailable)
1712
1713 self.showMenu.emit("SpellCheck", self.spellCheckMenu, self)
1714
1715 def __showContextMenuSpellLanguages(self):
1716 """
1717 Private slot handling the aboutToShow signal of the spell check
1718 languages context menu.
1719 """
1720 self.showMenu.emit("SpellLanguage", self.spellLanguagesMenu, self)
1721
1722 def __spellLanguagesMenuTriggered(self, act):
1723 """
1724 Private method to handle the selection of a spell check language.
1725
1726 @param act reference to the action that was triggered
1727 @type QAction
1728 """
1729 language = act.data()
1730 self.__setSpellingLanguage(language)
1731 self.spellLanguageChanged.emit(language)
1732
1733 def __checkSpellLanguage(self):
1734 """
1735 Private slot to check the selected spell check language action.
1736 """
1737 language = self.getSpellingLanguage()
1738 with contextlib.suppress(AttributeError, KeyError):
1739 self.supportedSpellLanguages[language].setChecked(True)
1740
1741 def __spellLanguageChanged(self, language, propagate=True):
1742 """
1743 Private slot to handle a change of the spell check language.
1744
1745 @param language new spell check language
1746 @type str
1747 @param propagate flag indicating to propagate the change
1748 @type bool
1749 """
1750 self.__setSpellingLanguage(language)
1751 self.__checkSpellLanguage()
1752
1753 if not self.__inSpellLanguageChanged and propagate:
1754 self.__inSpellLanguageChanged = True
1755 self.spellLanguageChanged.emit(language)
1756 self.__inSpellLanguageChanged = False
1757
1758 def __bindLexer(self, filename, pyname=""):
1759 """
1760 Private slot to set the correct lexer depending on language.
1761
1762 @param filename filename used to determine the associated lexer
1763 language (string)
1764 @param pyname name of the pygments lexer to use (string)
1765 """
1766 if (
1767 self.lexer_ is not None and
1768 (self.lexer_.lexer() == "container" or
1769 self.lexer_.lexer() is None)
1770 ):
1771 self.SCN_STYLENEEDED.disconnect(self.__styleNeeded)
1772
1773 language = ""
1774 if not self.filetype:
1775 if filename:
1776 basename = os.path.basename(filename)
1777 if (
1778 self.project.isOpen() and
1779 self.project.isProjectFile(filename)
1780 ):
1781 language = self.project.getEditorLexerAssoc(basename)
1782 if not language:
1783 language = Preferences.getEditorLexerAssoc(basename)
1784 if language == "Text":
1785 # no highlighting for plain text files
1786 self.__resetLanguage()
1787 return
1788
1789 if not language:
1790 bindName = self.__bindName(self.text(0))
1791 if bindName:
1792 language = Preferences.getEditorLexerAssoc(bindName)
1793 if language == "Python":
1794 # correction for Python
1795 pyVer = Utilities.determinePythonVersion(
1796 filename, self.text(0), self)
1797 language = "Python{0}".format(pyVer)
1798 if language in ['Python3', 'MicroPython', 'Cython', 'Ruby',
1799 'JavaScript', 'YAML', 'JSON']:
1800 self.filetype = language
1801 else:
1802 self.filetype = ""
1803 else:
1804 language = self.filetype
1805
1806 if language.startswith("Pygments|"):
1807 pyname = language
1808 self.filetype = language.split("|")[-1]
1809 language = ""
1810
1811 from . import Lexers
1812 self.lexer_ = Lexers.getLexer(language, self, pyname=pyname)
1813 if self.lexer_ is None:
1814 self.setLexer()
1815 self.apiLanguage = ""
1816 return
1817
1818 if pyname:
1819 if pyname.startswith("Pygments|"):
1820 self.apiLanguage = pyname
1821 else:
1822 self.apiLanguage = "Pygments|{0}".format(pyname)
1823 else:
1824 if language == "Protocol":
1825 self.apiLanguage = language
1826 else:
1827 # Change API language for lexer where QScintilla reports
1828 # an abbreviated name.
1829 self.apiLanguage = self.lexer_.language()
1830 if self.apiLanguage == "POV":
1831 self.apiLanguage = "Povray"
1832 elif self.apiLanguage == "PO":
1833 self.apiLanguage = "Gettext"
1834 self.setLexer(self.lexer_)
1835 self.__setMarginsDisplay()
1836 if self.lexer_.lexer() == "container" or self.lexer_.lexer() is None:
1837 self.SCN_STYLENEEDED.connect(self.__styleNeeded)
1838
1839 # get the font for style 0 and set it as the default font
1840 key = (
1841 'Scintilla/Guessed/style0/font'
1842 if pyname and pyname.startswith("Pygments|") else
1843 'Scintilla/{0}/style0/font'.format(self.lexer_.language())
1844 )
1845 fdesc = Preferences.getSettings().value(key)
1846 if fdesc is not None:
1847 font = QFont([fdesc[0]], int(fdesc[1]))
1848 self.lexer_.setDefaultFont(font)
1849 self.lexer_.readSettings(Preferences.getSettings(), "Scintilla")
1850 if self.lexer_.hasSubstyles():
1851 self.lexer_.readSubstyles(self)
1852
1853 # now set the lexer properties
1854 self.lexer_.initProperties()
1855
1856 # initialize the lexer APIs settings
1857 projectType = (
1858 self.project.getProjectType()
1859 if self.project.isOpen() and self.project.isProjectFile(filename)
1860 else ""
1861 )
1862 api = self.vm.getAPIsManager().getAPIs(self.apiLanguage,
1863 projectType=projectType)
1864 if api is not None and not api.isEmpty():
1865 self.lexer_.setAPIs(api.getQsciAPIs())
1866 self.acAPI = True
1867 else:
1868 self.acAPI = False
1869 self.autoCompletionAPIsAvailable.emit(self.acAPI)
1870
1871 self.__setAnnotationStyles()
1872
1873 self.lexer_.setDefaultColor(self.lexer_.color(0))
1874 self.lexer_.setDefaultPaper(self.lexer_.paper(0))
1875
1876 def __styleNeeded(self, position):
1877 """
1878 Private slot to handle the need for more styling.
1879
1880 @param position end position, that needs styling (integer)
1881 """
1882 self.lexer_.styleText(self.getEndStyled(), position)
1883
1884 def getLexer(self):
1885 """
1886 Public method to retrieve a reference to the lexer object.
1887
1888 @return the lexer object (Lexer)
1889 """
1890 return self.lexer_
1891
1892 def getLanguage(self, normalized=True, forPygments=False):
1893 """
1894 Public method to retrieve the language of the editor.
1895
1896 @param normalized flag indicating to normalize some Pygments
1897 lexer names (boolean)
1898 @param forPygments flag indicating to normalize some lexer
1899 names for Pygments (boolean)
1900 @return language of the editor (string)
1901 """
1902 if (
1903 self.apiLanguage == "Guessed" or
1904 self.apiLanguage.startswith("Pygments|")
1905 ):
1906 lang = self.lexer_.name()
1907 if normalized:
1908 # adjust some Pygments lexer names
1909 if lang in ("Python 2.x", "Python"):
1910 lang = "Python3"
1911 elif lang == "Protocol Buffer":
1912 lang = "Protocol"
1913
1914 else:
1915 lang = self.apiLanguage
1916 if forPygments:
1917 # adjust some names to Pygments lexer names
1918 if lang == "Python3":
1919 lang = "Python"
1920 elif lang == "Protocol":
1921 lang = "Protocol Buffer"
1922 return lang
1923
1924 def getApiLanguage(self):
1925 """
1926 Public method to get the API language of the editor.
1927
1928 @return API language
1929 @rtype str
1930 """
1931 return self.apiLanguage
1932
1933 def __bindCompleter(self, filename):
1934 """
1935 Private slot to set the correct typing completer depending on language.
1936
1937 @param filename filename used to determine the associated typing
1938 completer language (string)
1939 """
1940 if self.completer is not None:
1941 self.completer.setEnabled(False)
1942 self.completer = None
1943
1944 filename = os.path.basename(filename)
1945 apiLanguage = Preferences.getEditorLexerAssoc(filename)
1946 if apiLanguage == "":
1947 pyVer = self.__getPyVersion()
1948 if pyVer:
1949 apiLanguage = "Python{0}".format(pyVer)
1950 elif self.isRubyFile():
1951 apiLanguage = "Ruby"
1952
1953 from . import TypingCompleters
1954 self.completer = TypingCompleters.getCompleter(apiLanguage, self)
1955
1956 def getCompleter(self):
1957 """
1958 Public method to retrieve a reference to the completer object.
1959
1960 @return the completer object (CompleterBase)
1961 """
1962 return self.completer
1963
1964 def __modificationChanged(self, m):
1965 """
1966 Private slot to handle the modificationChanged signal.
1967
1968 It emits the signal modificationStatusChanged with parameters
1969 m and self.
1970
1971 @param m modification status
1972 """
1973 if not m and bool(self.fileName) and pathlib.Path(self.fileName).exists():
1974 self.lastModified = pathlib.Path(self.fileName).stat().st_mtime
1975 self.modificationStatusChanged.emit(m, self)
1976 self.undoAvailable.emit(self.isUndoAvailable())
1977 self.redoAvailable.emit(self.isRedoAvailable())
1978
1979 def __cursorPositionChanged(self, line, index):
1980 """
1981 Private slot to handle the cursorPositionChanged signal.
1982
1983 It emits the signal cursorChanged with parameters fileName,
1984 line and pos.
1985
1986 @param line line number of the cursor
1987 @param index position in line of the cursor
1988 """
1989 self.cursorChanged.emit(self.fileName, line + 1, index)
1990
1991 if Preferences.getEditor("MarkOccurrencesEnabled"):
1992 self.__markOccurrencesTimer.stop()
1993 self.__markOccurrencesTimer.start()
1994
1995 if self.lastLine != line:
1996 self.cursorLineChanged.emit(line)
1997
1998 if self.spell is not None:
1999 # do spell checking
2000 doSpelling = True
2001 if self.lastLine == line:
2002 start, end = self.getWordBoundaries(
2003 line, index, useWordChars=False)
2004 if start <= self.lastIndex and self.lastIndex <= end:
2005 doSpelling = False
2006 if doSpelling:
2007 pos = self.positionFromLineIndex(self.lastLine, self.lastIndex)
2008 self.spell.checkWord(pos)
2009
2010 if self.lastLine != line:
2011 self.__markerMap.update()
2012
2013 self.lastLine = line
2014 self.lastIndex = index
2015
2016 def __modificationReadOnly(self):
2017 """
2018 Private slot to handle the modificationAttempted signal.
2019 """
2020 EricMessageBox.warning(
2021 self,
2022 self.tr("Modification of Read Only file"),
2023 self.tr("""You are attempting to change a read only file. """
2024 """Please save to a different file first."""))
2025
2026 def setNoName(self, noName):
2027 """
2028 Public method to set the display string for an unnamed editor.
2029
2030 @param noName display string for this unnamed editor (string)
2031 """
2032 self.noName = noName
2033
2034 def getNoName(self):
2035 """
2036 Public method to get the display string for an unnamed editor.
2037
2038 @return display string for this unnamed editor (string)
2039 """
2040 return self.noName
2041
2042 def getFileName(self):
2043 """
2044 Public method to return the name of the file being displayed.
2045
2046 @return filename of the displayed file (string)
2047 """
2048 return self.fileName
2049
2050 def getFileType(self):
2051 """
2052 Public method to return the type of the file being displayed.
2053
2054 @return type of the displayed file (string)
2055 """
2056 return self.filetype
2057
2058 def getFileTypeByFlag(self):
2059 """
2060 Public method to return the type of the file, if it was set by an
2061 eflag: marker.
2062
2063 @return type of the displayed file, if set by an eflag: marker or an
2064 empty string (string)
2065 """
2066 if self.filetypeByFlag:
2067 return self.filetype
2068 else:
2069 return ""
2070
2071 def determineFileType(self):
2072 """
2073 Public method to determine the file type using various tests.
2074
2075 @return type of the displayed file or an empty string (string)
2076 """
2077 ftype = self.filetype
2078 if not ftype:
2079 pyVer = self.__getPyVersion()
2080 if pyVer:
2081 ftype = "Python{0}".format(pyVer)
2082 elif self.isRubyFile():
2083 ftype = "Ruby"
2084 else:
2085 ftype = ""
2086
2087 return ftype
2088
2089 def getEncoding(self):
2090 """
2091 Public method to return the current encoding.
2092
2093 @return current encoding (string)
2094 """
2095 return self.encoding
2096
2097 def __getPyVersion(self):
2098 """
2099 Private method to return the Python main version or 0 if it's
2100 not a Python file at all.
2101
2102 @return Python version or 0 if it's not a Python file (int)
2103 """
2104 return Utilities.determinePythonVersion(
2105 self.fileName, self.text(0), self)
2106
2107 def isPyFile(self):
2108 """
2109 Public method to return a flag indicating a Python (2 or 3) file.
2110
2111 @return flag indicating a Python3 file (boolean)
2112 """
2113 return self.__getPyVersion() == 3
2114
2115 def isPy3File(self):
2116 """
2117 Public method to return a flag indicating a Python3 file.
2118
2119 @return flag indicating a Python3 file (boolean)
2120 """
2121 return self.__getPyVersion() == 3
2122
2123 def isMicroPythonFile(self):
2124 """
2125 Public method to return a flag indicating a MicroPython file.
2126
2127 @return flag indicating a MicroPython file
2128 @rtype bool
2129 """
2130 if self.filetype == "MicroPython":
2131 return True
2132
2133 return False
2134
2135 def isCythonFile(self):
2136 """
2137 Public method to return a flag indicating a Cython file.
2138
2139 @return flag indicating a Cython file
2140 @rtype bool
2141 """
2142 if self.filetype == "Cython":
2143 return True
2144
2145 return False
2146
2147 def isRubyFile(self):
2148 """
2149 Public method to return a flag indicating a Ruby file.
2150
2151 @return flag indicating a Ruby file (boolean)
2152 """
2153 if self.filetype == "Ruby":
2154 return True
2155
2156 if self.filetype == "":
2157 line0 = self.text(0)
2158 if (
2159 line0.startswith("#!") and
2160 "ruby" in line0
2161 ):
2162 self.filetype = "Ruby"
2163 return True
2164
2165 if (
2166 bool(self.fileName) and
2167 os.path.splitext(self.fileName)[1] in
2168 self.dbs.getExtensions('Ruby')
2169 ):
2170 self.filetype = "Ruby"
2171 return True
2172
2173 return False
2174
2175 def isJavascriptFile(self):
2176 """
2177 Public method to return a flag indicating a Javascript file.
2178
2179 @return flag indicating a Javascript file (boolean)
2180 """
2181 if self.filetype == "JavaScript":
2182 return True
2183
2184 if (
2185 self.filetype == "" and
2186 self.fileName and
2187 os.path.splitext(self.fileName)[1] == ".js"
2188 ):
2189 self.filetype = "JavaScript"
2190 return True
2191
2192 return False
2193
2194 def highlightVisible(self):
2195 """
2196 Public method to make sure that the highlight is visible.
2197 """
2198 if self.lastHighlight is not None:
2199 lineno = self.markerLine(self.lastHighlight)
2200 self.ensureVisible(lineno + 1)
2201
2202 def highlight(self, line=None, error=False, syntaxError=False):
2203 """
2204 Public method to highlight [or de-highlight] a particular line.
2205
2206 @param line line number to highlight (integer)
2207 @param error flag indicating whether the error highlight should be
2208 used (boolean)
2209 @param syntaxError flag indicating a syntax error (boolean)
2210 """
2211 if line is None:
2212 self.lastHighlight = None
2213 if self.lastErrorMarker is not None:
2214 self.markerDeleteHandle(self.lastErrorMarker)
2215 self.lastErrorMarker = None
2216 if self.lastCurrMarker is not None:
2217 self.markerDeleteHandle(self.lastCurrMarker)
2218 self.lastCurrMarker = None
2219 else:
2220 if error:
2221 if self.lastErrorMarker is not None:
2222 self.markerDeleteHandle(self.lastErrorMarker)
2223 self.lastErrorMarker = self.markerAdd(line - 1, self.errorline)
2224 self.lastHighlight = self.lastErrorMarker
2225 else:
2226 if self.lastCurrMarker is not None:
2227 self.markerDeleteHandle(self.lastCurrMarker)
2228 self.lastCurrMarker = self.markerAdd(line - 1,
2229 self.currentline)
2230 self.lastHighlight = self.lastCurrMarker
2231 self.setCursorPosition(line - 1, 0)
2232
2233 def getHighlightPosition(self):
2234 """
2235 Public method to return the position of the highlight bar.
2236
2237 @return line number of the highlight bar (integer)
2238 """
2239 if self.lastHighlight is not None:
2240 return self.markerLine(self.lastHighlight)
2241 else:
2242 return 1
2243
2244 ###########################################################################
2245 ## Breakpoint handling methods below
2246 ###########################################################################
2247
2248 def __modified(self, pos, mtype, text, length, linesAdded, line, foldNow,
2249 foldPrev, token, annotationLinesAdded):
2250 """
2251 Private method to handle changes of the number of lines.
2252
2253 @param pos start position of change (integer)
2254 @param mtype flags identifying the change (integer)
2255 @param text text that is given to the Undo system (string)
2256 @param length length of the change (integer)
2257 @param linesAdded number of added/deleted lines (integer)
2258 @param line line number of a fold level or marker change (integer)
2259 @param foldNow new fold level (integer)
2260 @param foldPrev previous fold level (integer)
2261 @param token ???
2262 @param annotationLinesAdded number of added/deleted annotation lines
2263 (integer)
2264 """
2265 if (
2266 mtype & (self.SC_MOD_INSERTTEXT | self.SC_MOD_DELETETEXT) and
2267 linesAdded != 0 and
2268 self.breaks
2269 ):
2270 bps = [] # list of breakpoints
2271 for handle, (ln, cond, temp, enabled, ignorecount) in (
2272 self.breaks.items()
2273 ):
2274 line = self.markerLine(handle) + 1
2275 if ln != line:
2276 bps.append((ln, line))
2277 self.breaks[handle] = (line, cond, temp, enabled,
2278 ignorecount)
2279 self.inLinesChanged = True
2280 for ln, line in sorted(bps, reverse=linesAdded > 0):
2281 index1 = self.breakpointModel.getBreakPointIndex(
2282 self.fileName, ln)
2283 index2 = self.breakpointModel.index(index1.row(), 1)
2284 self.breakpointModel.setData(index2, line)
2285 self.inLinesChanged = False
2286
2287 def __restoreBreakpoints(self):
2288 """
2289 Private method to restore the breakpoints.
2290 """
2291 for handle in list(self.breaks.keys()):
2292 self.markerDeleteHandle(handle)
2293 self.__addBreakPoints(
2294 QModelIndex(), 0, self.breakpointModel.rowCount() - 1)
2295 self.__markerMap.update()
2296
2297 def __deleteBreakPoints(self, parentIndex, start, end):
2298 """
2299 Private slot to delete breakpoints.
2300
2301 @param parentIndex index of parent item (QModelIndex)
2302 @param start start row (integer)
2303 @param end end row (integer)
2304 """
2305 for row in range(start, end + 1):
2306 index = self.breakpointModel.index(row, 0, parentIndex)
2307 fn, lineno = self.breakpointModel.getBreakPointByIndex(index)[0:2]
2308 if fn == self.fileName:
2309 self.clearBreakpoint(lineno)
2310
2311 def __changeBreakPoints(self, startIndex, endIndex):
2312 """
2313 Private slot to set changed breakpoints.
2314
2315 @param startIndex start index of the breakpoints being changed
2316 (QModelIndex)
2317 @param endIndex end index of the breakpoints being changed
2318 (QModelIndex)
2319 """
2320 if not self.inLinesChanged:
2321 self.__addBreakPoints(QModelIndex(), startIndex.row(),
2322 endIndex.row())
2323
2324 def __breakPointDataAboutToBeChanged(self, startIndex, endIndex):
2325 """
2326 Private slot to handle the dataAboutToBeChanged signal of the
2327 breakpoint model.
2328
2329 @param startIndex start index of the rows to be changed (QModelIndex)
2330 @param endIndex end index of the rows to be changed (QModelIndex)
2331 """
2332 self.__deleteBreakPoints(QModelIndex(), startIndex.row(),
2333 endIndex.row())
2334
2335 def __addBreakPoints(self, parentIndex, start, end):
2336 """
2337 Private slot to add breakpoints.
2338
2339 @param parentIndex index of parent item (QModelIndex)
2340 @param start start row (integer)
2341 @param end end row (integer)
2342 """
2343 for row in range(start, end + 1):
2344 index = self.breakpointModel.index(row, 0, parentIndex)
2345 fn, line, cond, temp, enabled, ignorecount = (
2346 self.breakpointModel.getBreakPointByIndex(index)[:6]
2347 )
2348 if fn == self.fileName:
2349 self.newBreakpointWithProperties(
2350 line, (cond, temp, enabled, ignorecount))
2351
2352 def clearBreakpoint(self, line):
2353 """
2354 Public method to clear a breakpoint.
2355
2356 Note: This doesn't clear the breakpoint in the debugger,
2357 it just deletes it from the editor internal list of breakpoints.
2358
2359 @param line line number of the breakpoint (integer)
2360 """
2361 if self.inLinesChanged:
2362 return
2363
2364 for handle in self.breaks:
2365 if self.markerLine(handle) == line - 1:
2366 break
2367 else:
2368 # not found, simply ignore it
2369 return
2370
2371 del self.breaks[handle]
2372 self.markerDeleteHandle(handle)
2373 self.__markerMap.update()
2374
2375 def newBreakpointWithProperties(self, line, properties):
2376 """
2377 Public method to set a new breakpoint and its properties.
2378
2379 @param line line number of the breakpoint (integer)
2380 @param properties properties for the breakpoint (tuple)
2381 (condition, temporary flag, enabled flag, ignore count)
2382 """
2383 if not properties[2]:
2384 marker = self.dbreakpoint
2385 elif properties[0]:
2386 marker = properties[1] and self.tcbreakpoint or self.cbreakpoint
2387 else:
2388 marker = properties[1] and self.tbreakpoint or self.breakpoint
2389
2390 if self.markersAtLine(line - 1) & self.breakpointMask == 0:
2391 handle = self.markerAdd(line - 1, marker)
2392 self.breaks[handle] = (line,) + properties
2393 self.breakpointToggled.emit(self)
2394 self.__markerMap.update()
2395
2396 def __toggleBreakpoint(self, line, temporary=False):
2397 """
2398 Private method to toggle a breakpoint.
2399
2400 @param line line number of the breakpoint (integer)
2401 @param temporary flag indicating a temporary breakpoint (boolean)
2402 """
2403 for handle in self.breaks:
2404 if self.markerLine(handle) == line - 1:
2405 # delete breakpoint or toggle it to the next state
2406 index = self.breakpointModel.getBreakPointIndex(
2407 self.fileName, line)
2408 if (
2409 Preferences.getDebugger("ThreeStateBreakPoints") and
2410 not self.breakpointModel.isBreakPointTemporaryByIndex(
2411 index)
2412 ):
2413 self.breakpointModel.deleteBreakPointByIndex(index)
2414 self.__addBreakPoint(line, True)
2415 else:
2416 self.breakpointModel.deleteBreakPointByIndex(index)
2417 self.breakpointToggled.emit(self)
2418 break
2419 else:
2420 self.__addBreakPoint(line, temporary)
2421
2422 def __addBreakPoint(self, line, temporary):
2423 """
2424 Private method to add a new breakpoint.
2425
2426 @param line line number of the breakpoint (integer)
2427 @param temporary flag indicating a temporary breakpoint (boolean)
2428 """
2429 if self.fileName and self.isPyFile():
2430 linestarts = PythonDisViewer.linestarts(self.text())
2431 if line not in linestarts:
2432 if Preferences.getDebugger("IntelligentBreakpoints"):
2433 # change line to the next one starting an instruction block
2434 index = bisect.bisect(linestarts, line)
2435 with contextlib.suppress(IndexError):
2436 line = linestarts[index]
2437 self.__toggleBreakpoint(line, temporary=temporary)
2438 else:
2439 EricMessageBox.warning(
2440 self,
2441 self.tr("Add Breakpoint"),
2442 self.tr("No Python byte code will be created for the"
2443 " selected line. No break point will be set!")
2444 )
2445 return
2446
2447 self.breakpointModel.addBreakPoint(
2448 self.fileName, line, ('', temporary, True, 0))
2449 self.breakpointToggled.emit(self)
2450
2451 def __toggleBreakpointEnabled(self, line):
2452 """
2453 Private method to toggle a breakpoints enabled status.
2454
2455 @param line line number of the breakpoint (integer)
2456 """
2457 for handle in self.breaks:
2458 if self.markerLine(handle) == line - 1:
2459 index = self.breakpointModel.getBreakPointIndex(
2460 self.fileName, line)
2461 self.breakpointModel.setBreakPointEnabledByIndex(
2462 index, not self.breaks[handle][3])
2463 break
2464
2465 def curLineHasBreakpoint(self):
2466 """
2467 Public method to check for the presence of a breakpoint at the current
2468 line.
2469
2470 @return flag indicating the presence of a breakpoint (boolean)
2471 """
2472 line, _ = self.getCursorPosition()
2473 return self.markersAtLine(line) & self.breakpointMask != 0
2474
2475 def getBreakpointLines(self):
2476 """
2477 Public method to get the lines containing a breakpoint.
2478
2479 @return list of lines containing a breakpoint (list of integer)
2480 """
2481 lines = []
2482 line = -1
2483 while True:
2484 line = self.markerFindNext(line + 1, self.breakpointMask)
2485 if line < 0:
2486 break
2487 else:
2488 lines.append(line)
2489 return lines
2490
2491 def hasBreakpoints(self):
2492 """
2493 Public method to check for the presence of breakpoints.
2494
2495 @return flag indicating the presence of breakpoints (boolean)
2496 """
2497 return len(self.breaks) > 0
2498
2499 def __menuToggleTemporaryBreakpoint(self):
2500 """
2501 Private slot to handle the 'Toggle temporary breakpoint' context menu
2502 action.
2503 """
2504 if self.line < 0:
2505 self.line, index = self.getCursorPosition()
2506 self.line += 1
2507 self.__toggleBreakpoint(self.line, 1)
2508 self.line = -1
2509
2510 def menuToggleBreakpoint(self):
2511 """
2512 Public slot to handle the 'Toggle breakpoint' context menu action.
2513 """
2514 if self.line < 0:
2515 self.line, index = self.getCursorPosition()
2516 self.line += 1
2517 self.__toggleBreakpoint(self.line)
2518 self.line = -1
2519
2520 def __menuToggleBreakpointEnabled(self):
2521 """
2522 Private slot to handle the 'Enable/Disable breakpoint' context menu
2523 action.
2524 """
2525 if self.line < 0:
2526 self.line, index = self.getCursorPosition()
2527 self.line += 1
2528 self.__toggleBreakpointEnabled(self.line)
2529 self.line = -1
2530
2531 def menuEditBreakpoint(self, line=None):
2532 """
2533 Public slot to handle the 'Edit breakpoint' context menu action.
2534
2535 @param line linenumber of the breakpoint to edit
2536 """
2537 if line is not None:
2538 self.line = line - 1
2539 if self.line < 0:
2540 self.line, index = self.getCursorPosition()
2541
2542 for handle in self.breaks:
2543 if self.markerLine(handle) == self.line:
2544 ln, cond, temp, enabled, ignorecount = self.breaks[handle]
2545 index = self.breakpointModel.getBreakPointIndex(self.fileName,
2546 ln)
2547 if not index.isValid():
2548 return
2549
2550 # get recently used breakpoint conditions
2551 rs = Preferences.Prefs.rsettings.value(
2552 recentNameBreakpointConditions)
2553 condHistory = (
2554 Preferences.toList(rs)[
2555 :Preferences.getDebugger("RecentNumber")]
2556 if rs is not None else
2557 []
2558 )
2559
2560 from Debugger.EditBreakpointDialog import EditBreakpointDialog
2561 dlg = EditBreakpointDialog(
2562 (self.fileName, ln),
2563 (cond, temp, enabled, ignorecount),
2564 condHistory, self, modal=True)
2565 if dlg.exec() == QDialog.DialogCode.Accepted:
2566 cond, temp, enabled, ignorecount = dlg.getData()
2567 self.breakpointModel.setBreakPointByIndex(
2568 index, self.fileName, ln,
2569 (cond, temp, enabled, ignorecount))
2570
2571 if cond:
2572 # save the recently used breakpoint condition
2573 if cond in condHistory:
2574 condHistory.remove(cond)
2575 condHistory.insert(0, cond)
2576 Preferences.Prefs.rsettings.setValue(
2577 recentNameBreakpointConditions, condHistory)
2578 Preferences.Prefs.rsettings.sync()
2579
2580 break
2581
2582 self.line = -1
2583
2584 def menuNextBreakpoint(self):
2585 """
2586 Public slot to handle the 'Next breakpoint' context menu action.
2587 """
2588 line, index = self.getCursorPosition()
2589 if line == self.lines() - 1:
2590 line = 0
2591 else:
2592 line += 1
2593 bpline = self.markerFindNext(line, self.breakpointMask)
2594 if bpline < 0:
2595 # wrap around
2596 bpline = self.markerFindNext(0, self.breakpointMask)
2597 if bpline >= 0:
2598 self.setCursorPosition(bpline, 0)
2599 self.ensureLineVisible(bpline)
2600
2601 def menuPreviousBreakpoint(self):
2602 """
2603 Public slot to handle the 'Previous breakpoint' context menu action.
2604 """
2605 line, index = self.getCursorPosition()
2606 if line == 0:
2607 line = self.lines() - 1
2608 else:
2609 line -= 1
2610 bpline = self.markerFindPrevious(line, self.breakpointMask)
2611 if bpline < 0:
2612 # wrap around
2613 bpline = self.markerFindPrevious(
2614 self.lines() - 1, self.breakpointMask)
2615 if bpline >= 0:
2616 self.setCursorPosition(bpline, 0)
2617 self.ensureLineVisible(bpline)
2618
2619 def __menuClearBreakpoints(self):
2620 """
2621 Private slot to handle the 'Clear all breakpoints' context menu action.
2622 """
2623 self.__clearBreakpoints(self.fileName)
2624
2625 def __clearBreakpoints(self, fileName):
2626 """
2627 Private slot to clear all breakpoints.
2628
2629 @param fileName name of the file (string)
2630 """
2631 idxList = []
2632 for (ln, _, _, _, _) in self.breaks.values():
2633 index = self.breakpointModel.getBreakPointIndex(fileName, ln)
2634 if index.isValid():
2635 idxList.append(index)
2636 if idxList:
2637 self.breakpointModel.deleteBreakPoints(idxList)
2638
2639 ###########################################################################
2640 ## Bookmark handling methods below
2641 ###########################################################################
2642
2643 def toggleBookmark(self, line):
2644 """
2645 Public method to toggle a bookmark.
2646
2647 @param line line number of the bookmark (integer)
2648 """
2649 for handle in self.bookmarks:
2650 if self.markerLine(handle) == line - 1:
2651 self.bookmarks.remove(handle)
2652 self.markerDeleteHandle(handle)
2653 break
2654 else:
2655 # set a new bookmark
2656 handle = self.markerAdd(line - 1, self.bookmark)
2657 self.bookmarks.append(handle)
2658 self.bookmarkToggled.emit(self)
2659 self.__markerMap.update()
2660
2661 def getBookmarks(self):
2662 """
2663 Public method to retrieve the bookmarks.
2664
2665 @return sorted list of all lines containing a bookmark
2666 (list of integer)
2667 """
2668 bmlist = []
2669 for handle in self.bookmarks:
2670 bmlist.append(self.markerLine(handle) + 1)
2671
2672 bmlist.sort()
2673 return bmlist
2674
2675 def getBookmarkLines(self):
2676 """
2677 Public method to get the lines containing a bookmark.
2678
2679 @return list of lines containing a bookmark (list of integer)
2680 """
2681 lines = []
2682 line = -1
2683 while True:
2684 line = self.markerFindNext(line + 1, 1 << self.bookmark)
2685 if line < 0:
2686 break
2687 else:
2688 lines.append(line)
2689 return lines
2690
2691 def hasBookmarks(self):
2692 """
2693 Public method to check for the presence of bookmarks.
2694
2695 @return flag indicating the presence of bookmarks (boolean)
2696 """
2697 return len(self.bookmarks) > 0
2698
2699 def menuToggleBookmark(self):
2700 """
2701 Public slot to handle the 'Toggle bookmark' context menu action.
2702 """
2703 if self.line < 0:
2704 self.line, index = self.getCursorPosition()
2705 self.line += 1
2706 self.toggleBookmark(self.line)
2707 self.line = -1
2708
2709 def nextBookmark(self):
2710 """
2711 Public slot to handle the 'Next bookmark' context menu action.
2712 """
2713 line, index = self.getCursorPosition()
2714 if line == self.lines() - 1:
2715 line = 0
2716 else:
2717 line += 1
2718 bmline = self.markerFindNext(line, 1 << self.bookmark)
2719 if bmline < 0:
2720 # wrap around
2721 bmline = self.markerFindNext(0, 1 << self.bookmark)
2722 if bmline >= 0:
2723 self.setCursorPosition(bmline, 0)
2724 self.ensureLineVisible(bmline)
2725
2726 def previousBookmark(self):
2727 """
2728 Public slot to handle the 'Previous bookmark' context menu action.
2729 """
2730 line, index = self.getCursorPosition()
2731 if line == 0:
2732 line = self.lines() - 1
2733 else:
2734 line -= 1
2735 bmline = self.markerFindPrevious(line, 1 << self.bookmark)
2736 if bmline < 0:
2737 # wrap around
2738 bmline = self.markerFindPrevious(
2739 self.lines() - 1, 1 << self.bookmark)
2740 if bmline >= 0:
2741 self.setCursorPosition(bmline, 0)
2742 self.ensureLineVisible(bmline)
2743
2744 def clearBookmarks(self):
2745 """
2746 Public slot to handle the 'Clear all bookmarks' context menu action.
2747 """
2748 for handle in self.bookmarks:
2749 self.markerDeleteHandle(handle)
2750 self.bookmarks.clear()
2751 self.bookmarkToggled.emit(self)
2752 self.__markerMap.update()
2753
2754 ###########################################################################
2755 ## Printing methods below
2756 ###########################################################################
2757
2758 def printFile(self):
2759 """
2760 Public slot to print the text.
2761 """
2762 from .Printer import Printer
2763 printer = Printer(mode=QPrinter.PrinterMode.HighResolution)
2764 sb = ericApp().getObject("UserInterface").statusBar()
2765 printDialog = QPrintDialog(printer, self)
2766 if self.hasSelectedText():
2767 printDialog.setOption(
2768 QAbstractPrintDialog.PrintDialogOption.PrintSelection,
2769 True)
2770 if printDialog.exec() == QDialog.DialogCode.Accepted:
2771 sb.showMessage(self.tr('Printing...'))
2772 QApplication.processEvents()
2773 fn = self.getFileName()
2774 if fn is not None:
2775 printer.setDocName(os.path.basename(fn))
2776 else:
2777 printer.setDocName(self.noName)
2778 if (
2779 printDialog.printRange() ==
2780 QAbstractPrintDialog.PrintRange.Selection
2781 ):
2782 # get the selection
2783 fromLine, fromIndex, toLine, toIndex = self.getSelection()
2784 if toIndex == 0:
2785 toLine -= 1
2786 # QScintilla seems to print one line more than told
2787 res = printer.printRange(self, fromLine, toLine - 1)
2788 else:
2789 res = printer.printRange(self)
2790 if res:
2791 sb.showMessage(self.tr('Printing completed'), 2000)
2792 else:
2793 sb.showMessage(self.tr('Error while printing'), 2000)
2794 QApplication.processEvents()
2795 else:
2796 sb.showMessage(self.tr('Printing aborted'), 2000)
2797 QApplication.processEvents()
2798
2799 def printPreviewFile(self):
2800 """
2801 Public slot to show a print preview of the text.
2802 """
2803 from PyQt6.QtPrintSupport import QPrintPreviewDialog
2804 from .Printer import Printer
2805
2806 printer = Printer(mode=QPrinter.PrinterMode.HighResolution)
2807 fn = self.getFileName()
2808 if fn is not None:
2809 printer.setDocName(os.path.basename(fn))
2810 else:
2811 printer.setDocName(self.noName)
2812 preview = QPrintPreviewDialog(printer, self)
2813 preview.paintRequested.connect(self.__printPreview)
2814 preview.exec()
2815
2816 def __printPreview(self, printer):
2817 """
2818 Private slot to generate a print preview.
2819
2820 @param printer reference to the printer object
2821 (QScintilla.Printer.Printer)
2822 """
2823 printer.printRange(self)
2824
2825 ###########################################################################
2826 ## Task handling methods below
2827 ###########################################################################
2828
2829 def getTaskLines(self):
2830 """
2831 Public method to get the lines containing a task.
2832
2833 @return list of lines containing a task (list of integer)
2834 """
2835 lines = []
2836 line = -1
2837 while True:
2838 line = self.markerFindNext(line + 1, 1 << self.taskmarker)
2839 if line < 0:
2840 break
2841 else:
2842 lines.append(line)
2843 return lines
2844
2845 def hasTaskMarkers(self):
2846 """
2847 Public method to determine, if this editor contains any task markers.
2848
2849 @return flag indicating the presence of task markers (boolean)
2850 """
2851 return self.__hasTaskMarkers
2852
2853 def nextTask(self):
2854 """
2855 Public slot to handle the 'Next task' context menu action.
2856 """
2857 line, index = self.getCursorPosition()
2858 if line == self.lines() - 1:
2859 line = 0
2860 else:
2861 line += 1
2862 taskline = self.markerFindNext(line, 1 << self.taskmarker)
2863 if taskline < 0:
2864 # wrap around
2865 taskline = self.markerFindNext(0, 1 << self.taskmarker)
2866 if taskline >= 0:
2867 self.setCursorPosition(taskline, 0)
2868 self.ensureLineVisible(taskline)
2869
2870 def previousTask(self):
2871 """
2872 Public slot to handle the 'Previous task' context menu action.
2873 """
2874 line, index = self.getCursorPosition()
2875 if line == 0:
2876 line = self.lines() - 1
2877 else:
2878 line -= 1
2879 taskline = self.markerFindPrevious(line, 1 << self.taskmarker)
2880 if taskline < 0:
2881 # wrap around
2882 taskline = self.markerFindPrevious(
2883 self.lines() - 1, 1 << self.taskmarker)
2884 if taskline >= 0:
2885 self.setCursorPosition(taskline, 0)
2886 self.ensureLineVisible(taskline)
2887
2888 def extractTasks(self):
2889 """
2890 Public slot to extract all tasks.
2891 """
2892 from Tasks.Task import Task
2893 markers = {
2894 taskType: Preferences.getTasks(markersName).split()
2895 for taskType, markersName in Task.TaskType2MarkersName.items()
2896 }
2897 txtList = self.text().split(self.getLineSeparator())
2898
2899 # clear all task markers and tasks
2900 self.markerDeleteAll(self.taskmarker)
2901 self.taskViewer.clearFileTasks(self.fileName)
2902 self.__hasTaskMarkers = False
2903
2904 # now search tasks and record them
2905 for lineIndex, line in enumerate(txtList):
2906 shouldBreak = False
2907
2908 if line.endswith("__NO-TASK__"):
2909 # ignore potential task marker
2910 continue
2911
2912 for taskType, taskMarkers in markers.items():
2913 for taskMarker in taskMarkers:
2914 index = line.find(taskMarker)
2915 if index > -1:
2916 task = line[index:]
2917 self.markerAdd(lineIndex, self.taskmarker)
2918 self.taskViewer.addFileTask(
2919 task, self.fileName, lineIndex + 1, taskType)
2920 self.__hasTaskMarkers = True
2921 shouldBreak = True
2922 break
2923 if shouldBreak:
2924 break
2925 self.taskMarkersUpdated.emit(self)
2926 self.__markerMap.update()
2927
2928 ###########################################################################
2929 ## Change tracing methods below
2930 ###########################################################################
2931
2932 def __createChangeMarkerPixmap(self, key, size=16, width=4):
2933 """
2934 Private method to create a pixmap for the change markers.
2935
2936 @param key key of the color to use (string)
2937 @param size size of the pixmap (integer)
2938 @param width width of the marker line (integer)
2939 @return create pixmap (QPixmap)
2940 """
2941 pixmap = QPixmap(size, size)
2942 pixmap.fill(Qt.GlobalColor.transparent)
2943 painter = QPainter(pixmap)
2944 painter.fillRect(size - 4, 0, 4, size,
2945 Preferences.getEditorColour(key))
2946 painter.end()
2947 return pixmap
2948
2949 def __initOnlineChangeTrace(self):
2950 """
2951 Private slot to initialize the online change trace.
2952 """
2953 self.__hasChangeMarkers = False
2954 self.__oldText = self.text()
2955 self.__lastSavedText = self.text()
2956 self.__onlineChangeTraceTimer = QTimer(self)
2957 self.__onlineChangeTraceTimer.setSingleShot(True)
2958 self.__onlineChangeTraceTimer.setInterval(
2959 Preferences.getEditor("OnlineChangeTraceInterval"))
2960 self.__onlineChangeTraceTimer.timeout.connect(
2961 self.__onlineChangeTraceTimerTimeout)
2962 self.textChanged.connect(self.__resetOnlineChangeTraceTimer)
2963
2964 def __reinitOnlineChangeTrace(self):
2965 """
2966 Private slot to re-initialize the online change trace.
2967 """
2968 self.__oldText = self.text()
2969 self.__lastSavedText = self.text()
2970 self.__deleteAllChangeMarkers()
2971
2972 def __resetOnlineChangeTraceTimer(self):
2973 """
2974 Private method to reset the online syntax check timer.
2975 """
2976 if Preferences.getEditor("OnlineChangeTrace"):
2977 self.__onlineChangeTraceTimer.stop()
2978 self.__onlineChangeTraceTimer.start()
2979
2980 def __onlineChangeTraceTimerTimeout(self):
2981 """
2982 Private slot to mark added and changed lines.
2983 """
2984 self.__deleteAllChangeMarkers()
2985
2986 # step 1: mark saved changes
2987 oldL = self.__oldText.splitlines()
2988 newL = self.__lastSavedText.splitlines()
2989 matcher = difflib.SequenceMatcher(None, oldL, newL)
2990
2991 for token, _, _, j1, j2 in matcher.get_opcodes():
2992 if token in ["insert", "replace"]:
2993 for lineNo in range(j1, j2):
2994 self.markerAdd(lineNo, self.__changeMarkerSaved)
2995 self.__hasChangeMarkers = True
2996
2997 # step 2: mark unsaved changes
2998 oldL = self.__lastSavedText.splitlines()
2999 newL = self.text().splitlines()
3000 matcher = difflib.SequenceMatcher(None, oldL, newL)
3001
3002 for token, _, _, j1, j2 in matcher.get_opcodes():
3003 if token in ["insert", "replace"]:
3004 for lineNo in range(j1, j2):
3005 self.markerAdd(lineNo, self.__changeMarkerUnsaved)
3006 self.__hasChangeMarkers = True
3007
3008 if self.__hasChangeMarkers:
3009 self.changeMarkersUpdated.emit(self)
3010 self.__markerMap.update()
3011
3012 def __resetOnlineChangeTraceInfo(self):
3013 """
3014 Private slot to reset the online change trace info.
3015 """
3016 self.__lastSavedText = self.text()
3017 self.__deleteAllChangeMarkers()
3018
3019 # mark saved changes
3020 oldL = self.__oldText.splitlines()
3021 newL = self.__lastSavedText.splitlines()
3022 matcher = difflib.SequenceMatcher(None, oldL, newL)
3023
3024 for token, _, _, j1, j2 in matcher.get_opcodes():
3025 if token in ["insert", "replace"]:
3026 for lineNo in range(j1, j2):
3027 self.markerAdd(lineNo, self.__changeMarkerSaved)
3028 self.__hasChangeMarkers = True
3029
3030 if self.__hasChangeMarkers:
3031 self.changeMarkersUpdated.emit(self)
3032 self.__markerMap.update()
3033
3034 def __deleteAllChangeMarkers(self):
3035 """
3036 Private slot to delete all change markers.
3037 """
3038 self.markerDeleteAll(self.__changeMarkerUnsaved)
3039 self.markerDeleteAll(self.__changeMarkerSaved)
3040 self.__hasChangeMarkers = False
3041 self.changeMarkersUpdated.emit(self)
3042 self.__markerMap.update()
3043
3044 def getChangeLines(self):
3045 """
3046 Public method to get the lines containing a change.
3047
3048 @return list of lines containing a change (list of integer)
3049 """
3050 lines = []
3051 line = -1
3052 while True:
3053 line = self.markerFindNext(line + 1, self.changeMarkersMask)
3054 if line < 0:
3055 break
3056 else:
3057 lines.append(line)
3058 return lines
3059
3060 def hasChangeMarkers(self):
3061 """
3062 Public method to determine, if this editor contains any change markers.
3063
3064 @return flag indicating the presence of change markers (boolean)
3065 """
3066 return self.__hasChangeMarkers
3067
3068 def nextChange(self):
3069 """
3070 Public slot to handle the 'Next change' context menu action.
3071 """
3072 line, index = self.getCursorPosition()
3073 if line == self.lines() - 1:
3074 line = 0
3075 else:
3076 line += 1
3077 changeline = self.markerFindNext(line, self.changeMarkersMask)
3078 if changeline < 0:
3079 # wrap around
3080 changeline = self.markerFindNext(0, self.changeMarkersMask)
3081 if changeline >= 0:
3082 self.setCursorPosition(changeline, 0)
3083 self.ensureLineVisible(changeline)
3084
3085 def previousChange(self):
3086 """
3087 Public slot to handle the 'Previous change' context menu action.
3088 """
3089 line, index = self.getCursorPosition()
3090 if line == 0:
3091 line = self.lines() - 1
3092 else:
3093 line -= 1
3094 changeline = self.markerFindPrevious(line, self.changeMarkersMask)
3095 if changeline < 0:
3096 # wrap around
3097 changeline = self.markerFindPrevious(
3098 self.lines() - 1, self.changeMarkersMask)
3099 if changeline >= 0:
3100 self.setCursorPosition(changeline, 0)
3101 self.ensureLineVisible(changeline)
3102
3103 ###########################################################################
3104 ## Flags handling methods below
3105 ###########################################################################
3106
3107 def __processFlags(self):
3108 """
3109 Private method to extract flags and process them.
3110
3111 @return list of change flags (list of string)
3112 """
3113 txt = self.text()
3114 flags = Utilities.extractFlags(txt)
3115
3116 changedFlags = []
3117
3118 # Flag 1: FileType
3119 if "FileType" in flags:
3120 oldFiletype = self.filetype
3121 if isinstance(flags["FileType"], str):
3122 self.filetype = flags["FileType"]
3123 self.filetypeByFlag = True
3124 if oldFiletype != self.filetype:
3125 changedFlags.append("FileType")
3126 else:
3127 if self.filetype != "" and self.filetypeByFlag:
3128 self.filetype = ""
3129 self.filetypeByFlag = False
3130 self.__bindName(txt.splitlines()[0])
3131 changedFlags.append("FileType")
3132
3133 return changedFlags
3134
3135 ###########################################################################
3136 ## File handling methods below
3137 ###########################################################################
3138
3139 def checkDirty(self):
3140 """
3141 Public method to check dirty status and open a message window.
3142
3143 @return flag indicating successful reset of the dirty flag (boolean)
3144 """
3145 if self.isModified():
3146 fn = self.fileName
3147 if fn is None:
3148 fn = self.noName
3149 res = EricMessageBox.okToClearData(
3150 self,
3151 self.tr("File Modified"),
3152 self.tr("<p>The file <b>{0}</b> has unsaved changes.</p>")
3153 .format(fn),
3154 self.saveFile)
3155 if res:
3156 self.vm.setEditorName(self, self.fileName)
3157 return res
3158
3159 return True
3160
3161 def revertToUnmodified(self):
3162 """
3163 Public method to revert back to the last saved state.
3164 """
3165 undo_ = True
3166 while self.isModified():
3167 if undo_:
3168 # try undo first
3169 if self.isUndoAvailable():
3170 self.undo()
3171 else:
3172 undo_ = False
3173 else:
3174 # try redo next
3175 if self.isRedoAvailable():
3176 self.redo()
3177 else:
3178 break
3179 # Couldn't find the unmodified state
3180
3181 def readFile(self, fn, createIt=False, encoding=""):
3182 """
3183 Public slot to read the text from a file.
3184
3185 @param fn filename to read from (string)
3186 @param createIt flag indicating the creation of a new file, if the
3187 given one doesn't exist (boolean)
3188 @param encoding encoding to be used to read the file (string)
3189 (Note: this parameter overrides encoding detection)
3190 """
3191 self.__loadEditorConfig(fileName=fn)
3192
3193 try:
3194 with EricOverrideCursor():
3195 if createIt and not os.path.exists(fn):
3196 with open(fn, "w"):
3197 pass
3198 if encoding == "":
3199 encoding = self.__getEditorConfig("DefaultEncoding",
3200 nodefault=True)
3201 if encoding:
3202 txt, self.encoding = Utilities.readEncodedFileWithEncoding(
3203 fn, encoding)
3204 else:
3205 txt, self.encoding = Utilities.readEncodedFile(fn)
3206 except (UnicodeDecodeError, OSError) as why:
3207 EricMessageBox.critical(
3208 self.vm,
3209 self.tr('Open File'),
3210 self.tr('<p>The file <b>{0}</b> could not be opened.</p>'
3211 '<p>Reason: {1}</p>')
3212 .format(fn, str(why)))
3213 raise
3214
3215 with EricOverrideCursor():
3216 modified = False
3217
3218 self.setText(txt)
3219
3220 # get eric specific flags
3221 self.__processFlags()
3222
3223 # perform automatic EOL conversion
3224 if (
3225 self.__getEditorConfig("EOLMode", nodefault=True) or
3226 Preferences.getEditor("AutomaticEOLConversion")
3227 ):
3228 self.convertEols(self.eolMode())
3229 else:
3230 fileEol = self.detectEolString(txt)
3231 self.setEolModeByEolString(fileEol)
3232
3233 self.extractTasks()
3234
3235 self.setModified(modified)
3236 self.lastModified = pathlib.Path(fn).stat().st_mtime
3237
3238 def __convertTabs(self):
3239 """
3240 Private slot to convert tabulators to spaces.
3241 """
3242 if (
3243 (not self.__getEditorConfig("TabForIndentation")) and
3244 Preferences.getEditor("ConvertTabsOnLoad") and
3245 not (self.lexer_ and
3246 self.lexer_.alwaysKeepTabs())
3247 ):
3248 txt = self.text()
3249 txtExpanded = txt.expandtabs(self.__getEditorConfig("TabWidth"))
3250 if txtExpanded != txt:
3251 self.beginUndoAction()
3252 self.setText(txt)
3253 self.endUndoAction()
3254
3255 self.setModified(True)
3256
3257 def __removeTrailingWhitespace(self):
3258 """
3259 Private method to remove trailing whitespace.
3260 """
3261 searchRE = r"[ \t]+$" # whitespace at the end of a line
3262
3263 ok = self.findFirstTarget(searchRE, True, False, False, 0, 0)
3264 self.beginUndoAction()
3265 while ok:
3266 self.replaceTarget("")
3267 ok = self.findNextTarget()
3268 self.endUndoAction()
3269
3270 def writeFile(self, fn, backup=True):
3271 """
3272 Public slot to write the text to a file.
3273
3274 @param fn filename to write to (string)
3275 @param backup flag indicating to save a backup (boolean)
3276 @return flag indicating success (boolean)
3277 """
3278 config = self.__loadEditorConfigObject(fn)
3279
3280 eol = self.__getEditorConfig("EOLMode", nodefault=True, config=config)
3281 if eol is not None:
3282 self.convertEols(eol)
3283
3284 if self.__getEditorConfig("StripTrailingWhitespace", config=config):
3285 self.__removeTrailingWhitespace()
3286
3287 txt = self.text()
3288
3289 if self.__getEditorConfig("InsertFinalNewline", config=config):
3290 eol = self.getLineSeparator()
3291 if eol:
3292 if len(txt) >= len(eol):
3293 if txt[-len(eol):] != eol:
3294 txt += eol
3295 else:
3296 txt += eol
3297
3298 # create a backup file, if the option is set
3299 createBackup = backup and Preferences.getEditor("CreateBackupFile")
3300 if createBackup:
3301 if os.path.islink(fn):
3302 fn = os.path.realpath(fn)
3303 bfn = '{0}~'.format(fn)
3304 try:
3305 permissions = os.stat(fn).st_mode
3306 perms_valid = True
3307 except OSError:
3308 # if there was an error, ignore it
3309 perms_valid = False
3310 with contextlib.suppress(OSError):
3311 os.remove(bfn)
3312 with contextlib.suppress(OSError):
3313 os.rename(fn, bfn)
3314
3315 # now write text to the file fn
3316 try:
3317 editorConfigEncoding = self.__getEditorConfig(
3318 "DefaultEncoding", nodefault=True, config=config)
3319 self.encoding = Utilities.writeEncodedFile(
3320 fn, txt, self.encoding, forcedEncoding=editorConfigEncoding)
3321 if createBackup and perms_valid:
3322 os.chmod(fn, permissions)
3323 return True
3324 except (OSError, Utilities.CodingError, UnicodeError) as why:
3325 EricMessageBox.critical(
3326 self,
3327 self.tr('Save File'),
3328 self.tr('<p>The file <b>{0}</b> could not be saved.<br/>'
3329 'Reason: {1}</p>')
3330 .format(fn, str(why)))
3331 return False
3332
3333 def __getSaveFileName(self, path=None):
3334 """
3335 Private method to get the name of the file to be saved.
3336
3337 @param path directory to save the file in (string)
3338 @return file name (string)
3339 """
3340 # save to project, if a project is loaded
3341 if self.project.isOpen():
3342 if (
3343 self.fileName and
3344 self.project.startswithProjectPath(self.fileName)
3345 ):
3346 path = os.path.dirname(self.fileName)
3347 elif not self.fileName:
3348 path = self.project.getProjectPath()
3349
3350 if not path and self.fileName:
3351 path = os.path.dirname(self.fileName)
3352 if not path:
3353 path = (
3354 Preferences.getMultiProject("Workspace") or
3355 Utilities.getHomeDir()
3356 )
3357
3358 from . import Lexers
3359 if self.fileName:
3360 filterPattern = "(*{0})".format(
3361 os.path.splitext(self.fileName)[1])
3362 for fileFilter in Lexers.getSaveFileFiltersList(True):
3363 if filterPattern in fileFilter:
3364 defaultFilter = fileFilter
3365 break
3366 else:
3367 defaultFilter = Preferences.getEditor("DefaultSaveFilter")
3368 else:
3369 defaultFilter = Preferences.getEditor("DefaultSaveFilter")
3370 fn, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
3371 self,
3372 self.tr("Save File"),
3373 path,
3374 Lexers.getSaveFileFiltersList(True, True),
3375 defaultFilter,
3376 EricFileDialog.DontConfirmOverwrite)
3377
3378 if fn:
3379 if fn.endswith("."):
3380 fn = fn[:-1]
3381
3382 fpath = pathlib.Path(fn)
3383 if not fpath.suffix:
3384 ex = selectedFilter.split("(*")[1].split(")")[0]
3385 if ex:
3386 fpath = fpath.with_suffix(ex)
3387 if fpath.exists():
3388 res = EricMessageBox.yesNo(
3389 self,
3390 self.tr("Save File"),
3391 self.tr("<p>The file <b>{0}</b> already exists."
3392 " Overwrite it?</p>").format(fpath),
3393 icon=EricMessageBox.Warning)
3394 if not res:
3395 return ""
3396
3397 return str(fpath)
3398
3399 return ""
3400
3401 def saveFileCopy(self, path=None):
3402 """
3403 Public method to save a copy of the file.
3404
3405 @param path directory to save the file in (string)
3406 @return flag indicating success (boolean)
3407 """
3408 fn = self.__getSaveFileName(path)
3409 if not fn:
3410 return False
3411
3412 res = self.writeFile(fn)
3413 if (
3414 res and
3415 self.project.isOpen() and
3416 self.project.startswithProjectPath(fn)
3417 ):
3418 # save to project, if a project is loaded
3419 self.project.appendFile(fn)
3420
3421 return res
3422
3423 def saveFile(self, saveas=False, path=None):
3424 """
3425 Public method to save the text to a file.
3426
3427 @param saveas flag indicating a 'save as' action (boolean)
3428 @param path directory to save the file in (string)
3429 @return flag indicating success (boolean)
3430 """
3431 if not saveas and not self.isModified():
3432 return False # do nothing if text wasn't changed
3433
3434 newName = None
3435 if saveas or self.fileName == "":
3436 saveas = True
3437
3438 fn = self.__getSaveFileName(path)
3439 if not fn:
3440 return False
3441
3442 newName = fn
3443
3444 # save to project, if a project is loaded
3445 if (
3446 self.project.isOpen() and
3447 self.project.startswithProjectPath(fn)
3448 ):
3449 editorConfigEol = self.__getEditorConfig(
3450 "EOLMode", nodefault=True,
3451 config=self.__loadEditorConfigObject(fn))
3452 if editorConfigEol is not None:
3453 self.setEolMode(editorConfigEol)
3454 else:
3455 self.setEolModeByEolString(self.project.getEolString())
3456 self.convertEols(self.eolMode())
3457 else:
3458 fn = self.fileName
3459
3460 self.__loadEditorConfig(fn)
3461 self.editorAboutToBeSaved.emit(self.fileName)
3462 if self.writeFile(fn):
3463 if saveas:
3464 self.__clearBreakpoints(self.fileName)
3465 self.__setFileName(fn)
3466 self.setModified(False)
3467 self.setReadOnly(False)
3468 self.setWindowTitle(self.fileName)
3469 # get eric specific flags
3470 changedFlags = self.__processFlags()
3471 if not self.__lexerReset and "FileType" in changedFlags:
3472 self.setLanguage(self.fileName)
3473
3474 if saveas:
3475 self.isResourcesFile = self.fileName.endswith(".qrc")
3476 self.__initContextMenu()
3477 self.editorRenamed.emit(self.fileName)
3478
3479 # save to project, if a project is loaded
3480 if (
3481 self.project.isOpen() and
3482 self.project.startswithProjectPath(fn)
3483 ):
3484 self.project.appendFile(fn)
3485 self.addedToProject()
3486
3487 self.setLanguage(self.fileName)
3488
3489 self.lastModified = pathlib.Path(fn).stat().st_mtime
3490 if newName is not None:
3491 self.vm.addToRecentList(newName)
3492 self.editorSaved.emit(self.fileName)
3493 self.checkSyntax()
3494 self.extractTasks()
3495 self.__resetOnlineChangeTraceInfo()
3496 self.__checkEncoding()
3497 return True
3498 else:
3499 self.lastModified = (
3500 pathlib.Path(fn).stat().st_mtime
3501 if pathlib.Path(fn).exists() else
3502 0
3503 )
3504 return False
3505
3506 def saveFileAs(self, path=None, toProject=False):
3507 """
3508 Public slot to save a file with a new name.
3509
3510 @param path directory to save the file in (string)
3511 @param toProject flag indicating a save to project operation
3512 (boolean)
3513 @return tuple of two values (boolean, string) giving a success
3514 indicator and the name of the saved file
3515 """
3516 return self.saveFile(True, path)
3517
3518 def handleRenamed(self, fn):
3519 """
3520 Public slot to handle the editorRenamed signal.
3521
3522 @param fn filename to be set for the editor (string).
3523 """
3524 self.__clearBreakpoints(fn)
3525
3526 self.__setFileName(fn)
3527 self.setWindowTitle(self.fileName)
3528
3529 self.__loadEditorConfig()
3530
3531 if self.lexer_ is None:
3532 self.setLanguage(self.fileName)
3533
3534 self.lastModified = pathlib.Path(fn).stat().st_mtime
3535 self.vm.setEditorName(self, self.fileName)
3536 self.__updateReadOnly(True)
3537
3538 def fileRenamed(self, fn):
3539 """
3540 Public slot to handle the editorRenamed signal.
3541
3542 @param fn filename to be set for the editor (string).
3543 """
3544 self.handleRenamed(fn)
3545 if not self.inFileRenamed:
3546 self.inFileRenamed = True
3547 self.editorRenamed.emit(self.fileName)
3548 self.inFileRenamed = False
3549
3550 ###########################################################################
3551 ## Utility methods below
3552 ###########################################################################
3553
3554 def ensureVisible(self, line, expand=False):
3555 """
3556 Public slot to ensure, that the specified line is visible.
3557
3558 @param line line number to make visible
3559 @type int
3560 @param expand flag indicating to expand all folds
3561 @type bool
3562 """
3563 self.ensureLineVisible(line - 1)
3564 if expand:
3565 self.SendScintilla(QsciScintilla.SCI_FOLDCHILDREN, line - 1,
3566 QsciScintilla.SC_FOLDACTION_EXPAND)
3567
3568 def ensureVisibleTop(self, line, expand=False):
3569 """
3570 Public slot to ensure, that the specified line is visible at the top
3571 of the editor.
3572
3573 @param line line number to make visible
3574 @type int
3575 @param expand flag indicating to expand all folds
3576 @type bool
3577 """
3578 self.ensureVisible(line)
3579 self.setFirstVisibleLine(line - 1)
3580 self.ensureCursorVisible()
3581 if expand:
3582 self.SendScintilla(QsciScintilla.SCI_FOLDCHILDREN, line - 1,
3583 QsciScintilla.SC_FOLDACTION_EXPAND)
3584
3585 def __marginClicked(self, margin, line, modifiers):
3586 """
3587 Private slot to handle the marginClicked signal.
3588
3589 @param margin id of the clicked margin (integer)
3590 @param line line number of the click (integer)
3591 @param modifiers keyboard modifiers (Qt.KeyboardModifiers)
3592 """
3593 if margin == self.__bmMargin:
3594 self.toggleBookmark(line + 1)
3595 elif margin == self.__bpMargin:
3596 self.__toggleBreakpoint(line + 1)
3597 elif margin == self.__indicMargin:
3598 if self.markersAtLine(line) & (1 << self.syntaxerror):
3599 self.__showSyntaxError(line)
3600 elif self.markersAtLine(line) & (1 << self.warning):
3601 self.__showWarning(line)
3602
3603 def handleMonospacedEnable(self):
3604 """
3605 Public slot to handle the Use Monospaced Font context menu entry.
3606 """
3607 if self.menuActs["MonospacedFont"].isChecked():
3608 if not self.lexer_:
3609 self.setMonospaced(True)
3610 else:
3611 if self.lexer_:
3612 self.lexer_.readSettings(
3613 Preferences.getSettings(), "Scintilla")
3614 if self.lexer_.hasSubstyles():
3615 self.lexer_.readSubstyles(self)
3616 self.lexer_.initProperties()
3617 self.setMonospaced(False)
3618 self.__setMarginsDisplay()
3619
3620 def getWordBoundaries(self, line, index, useWordChars=True):
3621 """
3622 Public method to get the word boundaries at a position.
3623
3624 @param line number of line to look at (int)
3625 @param index position to look at (int)
3626 @param useWordChars flag indicating to use the wordCharacters
3627 method (boolean)
3628 @return tuple with start and end indexes of the word at the position
3629 (integer, integer)
3630 """
3631 wc = self.wordCharacters()
3632 if wc is None or not useWordChars:
3633 pattern = r"\b[\w_]+\b"
3634 else:
3635 wc = re.sub(r'\w', "", wc)
3636 pattern = r"\b[\w{0}]+\b".format(re.escape(wc))
3637 rx = (
3638 re.compile(pattern)
3639 if self.caseSensitive() else
3640 re.compile(pattern, re.IGNORECASE)
3641 )
3642
3643 text = self.text(line)
3644 for match in rx.finditer(text):
3645 start, end = match.span()
3646 if start <= index <= end:
3647 return (start, end)
3648
3649 return (index, index)
3650
3651 def getWord(self, line, index, direction=0, useWordChars=True):
3652 """
3653 Public method to get the word at a position.
3654
3655 @param line number of line to look at (int)
3656 @param index position to look at (int)
3657 @param direction direction to look in (0 = whole word, 1 = left,
3658 2 = right)
3659 @param useWordChars flag indicating to use the wordCharacters
3660 method (boolean)
3661 @return the word at that position (string)
3662 """
3663 start, end = self.getWordBoundaries(line, index, useWordChars)
3664 if direction == 1:
3665 end = index
3666 elif direction == 2:
3667 start = index
3668 if end > start:
3669 text = self.text(line)
3670 word = text[start:end]
3671 else:
3672 word = ''
3673 return word
3674
3675 def getWordLeft(self, line, index):
3676 """
3677 Public method to get the word to the left of a position.
3678
3679 @param line number of line to look at (int)
3680 @param index position to look at (int)
3681 @return the word to the left of that position (string)
3682 """
3683 return self.getWord(line, index, 1)
3684
3685 def getWordRight(self, line, index):
3686 """
3687 Public method to get the word to the right of a position.
3688
3689 @param line number of line to look at (int)
3690 @param index position to look at (int)
3691 @return the word to the right of that position (string)
3692 """
3693 return self.getWord(line, index, 2)
3694
3695 def getCurrentWord(self):
3696 """
3697 Public method to get the word at the current position.
3698
3699 @return the word at that current position (string)
3700 """
3701 line, index = self.getCursorPosition()
3702 return self.getWord(line, index)
3703
3704 def getCurrentWordBoundaries(self):
3705 """
3706 Public method to get the word boundaries at the current position.
3707
3708 @return tuple with start and end indexes of the current word
3709 (integer, integer)
3710 """
3711 line, index = self.getCursorPosition()
3712 return self.getWordBoundaries(line, index)
3713
3714 def selectWord(self, line, index):
3715 """
3716 Public method to select the word at a position.
3717
3718 @param line number of line to look at (int)
3719 @param index position to look at (int)
3720 """
3721 start, end = self.getWordBoundaries(line, index)
3722 self.setSelection(line, start, line, end)
3723
3724 def selectCurrentWord(self):
3725 """
3726 Public method to select the current word.
3727 """
3728 line, index = self.getCursorPosition()
3729 self.selectWord(line, index)
3730
3731 def __getCharacter(self, pos):
3732 """
3733 Private method to get the character to the left of the current position
3734 in the current line.
3735
3736 @param pos position to get character at (integer)
3737 @return requested character or "", if there are no more (string) and
3738 the next position (i.e. pos - 1)
3739 """
3740 if pos <= 0:
3741 return "", pos
3742
3743 pos = self.positionBefore(pos)
3744 ch = self.charAt(pos)
3745
3746 # Don't go past the end of the previous line
3747 if ch in ('\n', '\r'):
3748 return "", pos
3749
3750 return ch, pos
3751
3752 def getSearchText(self, selectionOnly=False):
3753 """
3754 Public method to determine the selection or the current word for the
3755 next search operation.
3756
3757 @param selectionOnly flag indicating that only selected text should be
3758 returned (boolean)
3759 @return selection or current word (string)
3760 """
3761 if self.hasSelectedText():
3762 text = self.selectedText()
3763 if '\r' in text or '\n' in text:
3764 # the selection contains at least a newline, it is
3765 # unlikely to be the expression to search for
3766 return ''
3767
3768 return text
3769
3770 if not selectionOnly:
3771 # no selected text, determine the word at the current position
3772 return self.getCurrentWord()
3773
3774 return ''
3775
3776 def setSearchIndicator(self, startPos, indicLength):
3777 """
3778 Public method to set a search indicator for the given range.
3779
3780 @param startPos start position of the indicator (integer)
3781 @param indicLength length of the indicator (integer)
3782 """
3783 self.setIndicatorRange(self.searchIndicator, startPos, indicLength)
3784 line = self.lineIndexFromPosition(startPos)[0]
3785 if line not in self.__searchIndicatorLines:
3786 self.__searchIndicatorLines.append(line)
3787
3788 def clearSearchIndicators(self):
3789 """
3790 Public method to clear all search indicators.
3791 """
3792 self.clearAllIndicators(self.searchIndicator)
3793 self.__markedText = ""
3794 self.__searchIndicatorLines = []
3795 self.__markerMap.update()
3796
3797 def __markOccurrences(self):
3798 """
3799 Private method to mark all occurrences of the current word.
3800 """
3801 word = self.getCurrentWord()
3802 if not word:
3803 self.clearSearchIndicators()
3804 return
3805
3806 if self.__markedText == word:
3807 return
3808
3809 self.clearSearchIndicators()
3810 ok = self.findFirstTarget(word, False, self.caseSensitive(), True,
3811 0, 0)
3812 while ok:
3813 tgtPos, tgtLen = self.getFoundTarget()
3814 self.setSearchIndicator(tgtPos, tgtLen)
3815 ok = self.findNextTarget()
3816 self.__markedText = word
3817 self.__markerMap.update()
3818
3819 def getSearchIndicatorLines(self):
3820 """
3821 Public method to get the lines containing a search indicator.
3822
3823 @return list of lines containing a search indicator (list of integer)
3824 """
3825 return self.__searchIndicatorLines[:]
3826
3827 def updateMarkerMap(self):
3828 """
3829 Public method to initiate an update of the marker map.
3830 """
3831 self.__markerMap.update()
3832
3833 ###########################################################################
3834 ## Highlighting marker handling methods below
3835 ###########################################################################
3836
3837 def setHighlight(self, startLine, startIndex, endLine, endIndex):
3838 """
3839 Public method to set a text highlight.
3840
3841 @param startLine line of the highlight start
3842 @type int
3843 @param startIndex index of the highlight start
3844 @type int
3845 @param endLine line of the highlight end
3846 @type int
3847 @param endIndex index of the highlight end
3848 @type int
3849 """
3850 self.setIndicator(self.highlightIndicator, startLine, startIndex,
3851 endLine, endIndex)
3852
3853 def clearAllHighlights(self):
3854 """
3855 Public method to clear all highlights.
3856 """
3857 self.clearAllIndicators(self.highlightIndicator)
3858
3859 def clearHighlight(self, startLine, startIndex, endLine, endIndex):
3860 """
3861 Public method to clear a text highlight.
3862
3863 @param startLine line of the highlight start
3864 @type int
3865 @param startIndex index of the highlight start
3866 @type int
3867 @param endLine line of the highlight end
3868 @type int
3869 @param endIndex index of the highlight end
3870 @type int
3871 """
3872 self.clearIndicator(self.highlightIndicator, startLine, startIndex,
3873 endLine, endIndex)
3874
3875 ###########################################################################
3876 ## Comment handling methods below
3877 ###########################################################################
3878
3879 def __isCommentedLine(self, line, commentStr):
3880 """
3881 Private method to check, if the given line is a comment line as
3882 produced by the configured comment rules.
3883
3884 @param line text of the line to check (string)
3885 @param commentStr comment string to check against (string)
3886 @return flag indicating a commented line (boolean)
3887 """
3888 if Preferences.getEditor("CommentColumn0"):
3889 return line.startswith(commentStr)
3890 else:
3891 return line.strip().startswith(commentStr)
3892
3893 def toggleCommentBlock(self):
3894 """
3895 Public slot to toggle the comment of a block.
3896
3897 If the line of the cursor or the selection is not commented, it will
3898 be commented. If it is commented, the comment block will be removed.
3899 The later works independent of the current selection.
3900 """
3901 if self.lexer_ is None or not self.lexer_.canBlockComment():
3902 return
3903
3904 commentStr = self.lexer_.commentStr()
3905 line, index = self.getCursorPosition()
3906
3907 # check if line starts with our comment string (i.e. was commented
3908 # by our comment...() slots
3909 if (
3910 self.hasSelectedText() and
3911 self.__isCommentedLine(self.text(self.getSelection()[0]),
3912 commentStr)
3913 ):
3914 self.uncommentLineOrSelection()
3915 elif not self.__isCommentedLine(self.text(line), commentStr):
3916 # it doesn't, so comment the line or selection
3917 self.commentLineOrSelection()
3918 else:
3919 # determine the start of the comment block
3920 begline = line
3921 while (
3922 begline > 0 and
3923 self.__isCommentedLine(self.text(begline - 1), commentStr)
3924 ):
3925 begline -= 1
3926 # determine the end of the comment block
3927 endline = line
3928 lines = self.lines()
3929 while (
3930 endline < lines and
3931 self.__isCommentedLine(self.text(endline + 1), commentStr)
3932 ):
3933 endline += 1
3934
3935 self.setSelection(begline, 0, endline, self.lineLength(endline))
3936 self.uncommentLineOrSelection()
3937
3938 # reset the cursor
3939 self.setCursorPosition(line, index - len(commentStr))
3940
3941 def commentLine(self):
3942 """
3943 Public slot to comment the current line.
3944 """
3945 if self.lexer_ is None or not self.lexer_.canBlockComment():
3946 return
3947
3948 line, index = self.getCursorPosition()
3949 self.beginUndoAction()
3950 if Preferences.getEditor("CommentColumn0"):
3951 self.insertAt(self.lexer_.commentStr(), line, 0)
3952 else:
3953 lineText = self.text(line)
3954 pos = len(lineText.replace(lineText.lstrip(" \t"), ""))
3955 self.insertAt(self.lexer_.commentStr(), line, pos)
3956 self.endUndoAction()
3957
3958 def uncommentLine(self):
3959 """
3960 Public slot to uncomment the current line.
3961 """
3962 if self.lexer_ is None or not self.lexer_.canBlockComment():
3963 return
3964
3965 commentStr = self.lexer_.commentStr()
3966 line, index = self.getCursorPosition()
3967
3968 # check if line starts with our comment string (i.e. was commented
3969 # by our comment...() slots
3970 if not self.__isCommentedLine(self.text(line), commentStr):
3971 return
3972
3973 # now remove the comment string
3974 self.beginUndoAction()
3975 if Preferences.getEditor("CommentColumn0"):
3976 self.setSelection(line, 0, line, len(commentStr))
3977 else:
3978 lineText = self.text(line)
3979 pos = len(lineText.replace(lineText.lstrip(" \t"), ""))
3980 self.setSelection(line, pos, line, pos + len(commentStr))
3981 self.removeSelectedText()
3982 self.endUndoAction()
3983
3984 def commentSelection(self):
3985 """
3986 Public slot to comment the current selection.
3987 """
3988 if self.lexer_ is None or not self.lexer_.canBlockComment():
3989 return
3990
3991 if not self.hasSelectedText():
3992 return
3993
3994 commentStr = self.lexer_.commentStr()
3995
3996 # get the selection boundaries
3997 lineFrom, indexFrom, lineTo, indexTo = self.getSelection()
3998 endLine = lineTo if indexTo else lineTo - 1
3999
4000 self.beginUndoAction()
4001 # iterate over the lines
4002 for line in range(lineFrom, endLine + 1):
4003 if Preferences.getEditor("CommentColumn0"):
4004 self.insertAt(commentStr, line, 0)
4005 else:
4006 lineText = self.text(line)
4007 pos = len(lineText.replace(lineText.lstrip(" \t"), ""))
4008 self.insertAt(commentStr, line, pos)
4009
4010 # change the selection accordingly
4011 self.setSelection(lineFrom, 0, endLine + 1, 0)
4012 self.endUndoAction()
4013
4014 def uncommentSelection(self):
4015 """
4016 Public slot to uncomment the current selection.
4017 """
4018 if self.lexer_ is None or not self.lexer_.canBlockComment():
4019 return
4020
4021 if not self.hasSelectedText():
4022 return
4023
4024 commentStr = self.lexer_.commentStr()
4025
4026 # get the selection boundaries
4027 lineFrom, indexFrom, lineTo, indexTo = self.getSelection()
4028 endLine = lineTo if indexTo else lineTo - 1
4029
4030 self.beginUndoAction()
4031 # iterate over the lines
4032 for line in range(lineFrom, endLine + 1):
4033 # check if line starts with our comment string (i.e. was commented
4034 # by our comment...() slots
4035 if not self.__isCommentedLine(self.text(line), commentStr):
4036 continue
4037
4038 if Preferences.getEditor("CommentColumn0"):
4039 self.setSelection(line, 0, line, len(commentStr))
4040 else:
4041 lineText = self.text(line)
4042 pos = len(lineText.replace(lineText.lstrip(" \t"), ""))
4043 self.setSelection(line, pos, line, pos + len(commentStr))
4044 self.removeSelectedText()
4045
4046 # adjust selection start
4047 if line == lineFrom:
4048 indexFrom -= len(commentStr)
4049 if indexFrom < 0:
4050 indexFrom = 0
4051
4052 # adjust selection end
4053 if line == lineTo:
4054 indexTo -= len(commentStr)
4055 if indexTo < 0:
4056 indexTo = 0
4057
4058 # change the selection accordingly
4059 self.setSelection(lineFrom, indexFrom, lineTo, indexTo)
4060 self.endUndoAction()
4061
4062 def commentLineOrSelection(self):
4063 """
4064 Public slot to comment the current line or current selection.
4065 """
4066 if self.hasSelectedText():
4067 self.commentSelection()
4068 else:
4069 self.commentLine()
4070
4071 def uncommentLineOrSelection(self):
4072 """
4073 Public slot to uncomment the current line or current selection.
4074 """
4075 if self.hasSelectedText():
4076 self.uncommentSelection()
4077 else:
4078 self.uncommentLine()
4079
4080 def streamCommentLine(self):
4081 """
4082 Public slot to stream comment the current line.
4083 """
4084 if self.lexer_ is None or not self.lexer_.canStreamComment():
4085 return
4086
4087 commentStr = self.lexer_.streamCommentStr()
4088 line, index = self.getCursorPosition()
4089
4090 self.beginUndoAction()
4091 self.insertAt(commentStr['end'], line, self.lineLength(line))
4092 self.insertAt(commentStr['start'], line, 0)
4093 self.endUndoAction()
4094
4095 def streamCommentSelection(self):
4096 """
4097 Public slot to comment the current selection.
4098 """
4099 if self.lexer_ is None or not self.lexer_.canStreamComment():
4100 return
4101
4102 if not self.hasSelectedText():
4103 return
4104
4105 commentStr = self.lexer_.streamCommentStr()
4106
4107 # get the selection boundaries
4108 lineFrom, indexFrom, lineTo, indexTo = self.getSelection()
4109 if indexTo == 0:
4110 endLine = lineTo - 1
4111 endIndex = self.lineLength(endLine)
4112 else:
4113 endLine = lineTo
4114 endIndex = indexTo
4115
4116 self.beginUndoAction()
4117 self.insertAt(commentStr['end'], endLine, endIndex)
4118 self.insertAt(commentStr['start'], lineFrom, indexFrom)
4119
4120 # change the selection accordingly
4121 if indexTo > 0:
4122 indexTo += len(commentStr['end'])
4123 if lineFrom == endLine:
4124 indexTo += len(commentStr['start'])
4125 self.setSelection(lineFrom, indexFrom, lineTo, indexTo)
4126 self.endUndoAction()
4127
4128 def streamCommentLineOrSelection(self):
4129 """
4130 Public slot to stream comment the current line or current selection.
4131 """
4132 if self.hasSelectedText():
4133 self.streamCommentSelection()
4134 else:
4135 self.streamCommentLine()
4136
4137 def boxCommentLine(self):
4138 """
4139 Public slot to box comment the current line.
4140 """
4141 if self.lexer_ is None or not self.lexer_.canBoxComment():
4142 return
4143
4144 commentStr = self.lexer_.boxCommentStr()
4145 line, index = self.getCursorPosition()
4146
4147 eol = self.getLineSeparator()
4148 self.beginUndoAction()
4149 self.insertAt(eol, line, self.lineLength(line))
4150 self.insertAt(commentStr['end'], line + 1, 0)
4151 self.insertAt(commentStr['middle'], line, 0)
4152 self.insertAt(eol, line, 0)
4153 self.insertAt(commentStr['start'], line, 0)
4154 self.endUndoAction()
4155
4156 def boxCommentSelection(self):
4157 """
4158 Public slot to box comment the current selection.
4159 """
4160 if self.lexer_ is None or not self.lexer_.canBoxComment():
4161 return
4162
4163 if not self.hasSelectedText():
4164 return
4165
4166 commentStr = self.lexer_.boxCommentStr()
4167
4168 # get the selection boundaries
4169 lineFrom, indexFrom, lineTo, indexTo = self.getSelection()
4170 endLine = lineTo if indexTo else lineTo - 1
4171
4172 self.beginUndoAction()
4173 # iterate over the lines
4174 for line in range(lineFrom, endLine + 1):
4175 self.insertAt(commentStr['middle'], line, 0)
4176
4177 # now do the comments before and after the selection
4178 eol = self.getLineSeparator()
4179 self.insertAt(eol, endLine, self.lineLength(endLine))
4180 self.insertAt(commentStr['end'], endLine + 1, 0)
4181 self.insertAt(eol, lineFrom, 0)
4182 self.insertAt(commentStr['start'], lineFrom, 0)
4183
4184 # change the selection accordingly
4185 self.setSelection(lineFrom, 0, endLine + 3, 0)
4186 self.endUndoAction()
4187
4188 def boxCommentLineOrSelection(self):
4189 """
4190 Public slot to box comment the current line or current selection.
4191 """
4192 if self.hasSelectedText():
4193 self.boxCommentSelection()
4194 else:
4195 self.boxCommentLine()
4196
4197 ###########################################################################
4198 ## Indentation handling methods below
4199 ###########################################################################
4200
4201 def __indentLine(self, indent=True):
4202 """
4203 Private method to indent or unindent the current line.
4204
4205 @param indent flag indicating an indent operation (boolean)
4206 <br />If the flag is true, an indent operation is performed.
4207 Otherwise the current line is unindented.
4208 """
4209 line, index = self.getCursorPosition()
4210 self.beginUndoAction()
4211 if indent:
4212 self.indent(line)
4213 else:
4214 self.unindent(line)
4215 self.endUndoAction()
4216 if indent:
4217 self.setCursorPosition(line, index + self.indentationWidth())
4218 else:
4219 self.setCursorPosition(line, index - self.indentationWidth())
4220
4221 def __indentSelection(self, indent=True):
4222 """
4223 Private method to indent or unindent the current selection.
4224
4225 @param indent flag indicating an indent operation (boolean)
4226 <br />If the flag is true, an indent operation is performed.
4227 Otherwise the current line is unindented.
4228 """
4229 if not self.hasSelectedText():
4230 return
4231
4232 # get the selection
4233 lineFrom, indexFrom, lineTo, indexTo = self.getSelection()
4234 endLine = lineTo if indexTo else lineTo - 1
4235
4236 self.beginUndoAction()
4237 # iterate over the lines
4238 for line in range(lineFrom, endLine + 1):
4239 if indent:
4240 self.indent(line)
4241 else:
4242 self.unindent(line)
4243 self.endUndoAction()
4244 if indent:
4245 if indexTo == 0:
4246 self.setSelection(
4247 lineFrom, indexFrom + self.indentationWidth(),
4248 lineTo, 0)
4249 else:
4250 self.setSelection(
4251 lineFrom, indexFrom + self.indentationWidth(),
4252 lineTo, indexTo + self.indentationWidth())
4253 else:
4254 indexStart = indexFrom - self.indentationWidth()
4255 if indexStart < 0:
4256 indexStart = 0
4257 indexEnd = indexTo - self.indentationWidth()
4258 if indexEnd < 0:
4259 indexEnd = 0
4260 self.setSelection(lineFrom, indexStart, lineTo, indexEnd)
4261
4262 def indentLineOrSelection(self):
4263 """
4264 Public slot to indent the current line or current selection.
4265 """
4266 if self.hasSelectedText():
4267 self.__indentSelection(True)
4268 else:
4269 self.__indentLine(True)
4270
4271 def unindentLineOrSelection(self):
4272 """
4273 Public slot to unindent the current line or current selection.
4274 """
4275 if self.hasSelectedText():
4276 self.__indentSelection(False)
4277 else:
4278 self.__indentLine(False)
4279
4280 def smartIndentLineOrSelection(self):
4281 """
4282 Public slot to indent current line smartly.
4283 """
4284 if self.hasSelectedText():
4285 if self.lexer_ and self.lexer_.hasSmartIndent():
4286 self.lexer_.smartIndentSelection(self)
4287 else:
4288 self.__indentSelection(True)
4289 else:
4290 if self.lexer_ and self.lexer_.hasSmartIndent():
4291 self.lexer_.smartIndentLine(self)
4292 else:
4293 self.__indentLine(True)
4294
4295 def gotoLine(self, line, pos=1, firstVisible=False, expand=False):
4296 """
4297 Public slot to jump to the beginning of a line.
4298
4299 @param line line number to go to
4300 @type int
4301 @param pos position in line to go to
4302 @type int
4303 @param firstVisible flag indicating to make the line the first
4304 visible line
4305 @type bool
4306 @param expand flag indicating to expand all folds
4307 @type bool
4308 """
4309 self.setCursorPosition(line - 1, pos - 1)
4310 if firstVisible:
4311 self.ensureVisibleTop(line, expand)
4312 else:
4313 self.ensureVisible(line, expand)
4314
4315 def __textChanged(self):
4316 """
4317 Private slot to handle a change of the editor text.
4318
4319 This slot defers the handling to the next time the event loop
4320 is run in order to ensure, that cursor position has been updated
4321 by the underlying Scintilla editor.
4322 """
4323 QTimer.singleShot(0, self.__saveLastEditPosition)
4324
4325 def __saveLastEditPosition(self):
4326 """
4327 Private slot to record the last edit position.
4328 """
4329 self.__lastEditPosition = self.getCursorPosition()
4330 self.lastEditPositionAvailable.emit()
4331
4332 def isLastEditPositionAvailable(self):
4333 """
4334 Public method to check, if a last edit position is available.
4335
4336 @return flag indicating availability (boolean)
4337 """
4338 return self.__lastEditPosition is not None
4339
4340 def gotoLastEditPosition(self):
4341 """
4342 Public method to move the cursor to the last edit position.
4343 """
4344 self.setCursorPosition(*self.__lastEditPosition)
4345 self.ensureVisible(self.__lastEditPosition[0])
4346
4347 def gotoMethodClass(self, goUp=False):
4348 """
4349 Public method to go to the next Python method or class definition.
4350
4351 @param goUp flag indicating the move direction (boolean)
4352 """
4353 if self.isPyFile() or self.isRubyFile():
4354 lineNo = self.getCursorPosition()[0]
4355 line = self.text(lineNo)
4356 if line.strip().startswith(("class ", "def ", "module ")):
4357 if goUp:
4358 lineNo -= 1
4359 else:
4360 lineNo += 1
4361 while True:
4362 if goUp and lineNo < 0:
4363 self.setCursorPosition(0, 0)
4364 self.ensureVisible(0)
4365 return
4366 elif not goUp and lineNo == self.lines():
4367 lineNo = self.lines() - 1
4368 self.setCursorPosition(lineNo, self.lineLength(lineNo))
4369 self.ensureVisible(lineNo)
4370 return
4371
4372 line = self.text(lineNo)
4373 if line.strip().startswith(("class ", "def ", "module ")):
4374 # try 'def ' first because it occurs more often
4375 first = line.find("def ")
4376 if first > -1:
4377 first += 4
4378 else:
4379 first = line.find("class ")
4380 if first > -1:
4381 first += 6
4382 else:
4383 first = line.find("module ") + 7
4384 match = re.search("[:(]", line)
4385 if match:
4386 end = match.start()
4387 else:
4388 end = self.lineLength(lineNo) - 1
4389 self.setSelection(lineNo, first, lineNo, end)
4390 self.ensureVisible(lineNo)
4391 return
4392
4393 if goUp:
4394 lineNo -= 1
4395 else:
4396 lineNo += 1
4397
4398 ###########################################################################
4399 ## Setup methods below
4400 ###########################################################################
4401
4402 def readSettings(self):
4403 """
4404 Public slot to read the settings into our lexer.
4405 """
4406 # read the lexer settings and reinit the properties
4407 if self.lexer_ is not None:
4408 self.lexer_.readSettings(Preferences.getSettings(), "Scintilla")
4409 if self.lexer_.hasSubstyles():
4410 self.lexer_.readSubstyles(self)
4411 self.lexer_.initProperties()
4412
4413 self.lexer_.setDefaultColor(self.lexer_.color(0))
4414 self.lexer_.setDefaultPaper(self.lexer_.paper(0))
4415
4416 self.__bindLexer(self.fileName)
4417 self.recolor()
4418
4419 # read the typing completer settings
4420 if self.completer is not None:
4421 self.completer.readSettings()
4422
4423 # set the line marker colours or pixmap
4424 if Preferences.getEditor("LineMarkersBackground"):
4425 self.markerDefine(QsciScintilla.MarkerSymbol.Background,
4426 self.currentline)
4427 self.markerDefine(QsciScintilla.MarkerSymbol.Background,
4428 self.errorline)
4429 self.__setLineMarkerColours()
4430 else:
4431 self.markerDefine(
4432 UI.PixmapCache.getPixmap("currentLineMarker"),
4433 self.currentline)
4434 self.markerDefine(
4435 UI.PixmapCache.getPixmap("errorLineMarker"),
4436 self.errorline)
4437
4438 # set the text display
4439 self.__setTextDisplay()
4440
4441 # set margin 0 and 2 configuration
4442 self.__setMarginsDisplay()
4443
4444 # set the auto-completion function
4445 self.__acCache.setSize(
4446 Preferences.getEditor("AutoCompletionCacheSize"))
4447 self.__acCache.setMaximumCacheTime(
4448 Preferences.getEditor("AutoCompletionCacheTime"))
4449 self.__acCacheEnabled = Preferences.getEditor(
4450 "AutoCompletionCacheEnabled")
4451 acTimeout = Preferences.getEditor("AutoCompletionTimeout")
4452 if acTimeout != self.__acTimer.interval:
4453 self.__acTimer.setInterval(acTimeout)
4454 self.__setAutoCompletion()
4455
4456 # set the calltips function
4457 self.__setCallTips()
4458
4459 # set the autosave flags
4460 self.autosaveEnabled = Preferences.getEditor("AutosaveInterval") > 0
4461
4462 if Preferences.getEditor("MiniContextMenu") != self.miniMenu:
4463 # regenerate context menu
4464 self.__initContextMenu()
4465 else:
4466 # set checked context menu items
4467 self.menuActs["AutoCompletionEnable"].setChecked(
4468 self.autoCompletionThreshold() != -1)
4469 self.menuActs["MonospacedFont"].setChecked(
4470 self.useMonospaced)
4471 self.menuActs["AutosaveEnable"].setChecked(
4472 self.autosaveEnabled and not self.autosaveManuallyDisabled)
4473
4474 # regenerate the margins context menu(s)
4475 self.__initContextMenuMargins()
4476
4477 if Preferences.getEditor("MarkOccurrencesEnabled"):
4478 self.__markOccurrencesTimer.setInterval(
4479 Preferences.getEditor("MarkOccurrencesTimeout"))
4480 else:
4481 self.__markOccurrencesTimer.stop()
4482 self.clearSearchIndicators()
4483
4484 if Preferences.getEditor("OnlineSyntaxCheck"):
4485 self.__onlineSyntaxCheckTimer.setInterval(
4486 Preferences.getEditor("OnlineSyntaxCheckInterval") * 1000)
4487 else:
4488 self.__onlineSyntaxCheckTimer.stop()
4489
4490 if Preferences.getEditor("OnlineChangeTrace"):
4491 self.__onlineChangeTraceTimer.setInterval(
4492 Preferences.getEditor("OnlineChangeTraceInterval"))
4493 else:
4494 self.__onlineChangeTraceTimer.stop()
4495 self.__deleteAllChangeMarkers()
4496 self.markerDefine(self.__createChangeMarkerPixmap(
4497 "OnlineChangeTraceMarkerUnsaved"), self.__changeMarkerUnsaved)
4498 self.markerDefine(self.__createChangeMarkerPixmap(
4499 "OnlineChangeTraceMarkerSaved"), self.__changeMarkerSaved)
4500
4501 # refresh the annotations display
4502 self.__refreshAnnotations()
4503
4504 self.__markerMap.setMapPosition(
4505 Preferences.getEditor("ShowMarkerMapOnRight"))
4506 self.__markerMap.initColors()
4507
4508 self.setLanguage(self.fileName, propagate=False)
4509
4510 self.settingsRead.emit()
4511
4512 def __setLineMarkerColours(self):
4513 """
4514 Private method to set the line marker colours.
4515 """
4516 self.setMarkerForegroundColor(
4517 Preferences.getEditorColour("CurrentMarker"), self.currentline)
4518 self.setMarkerBackgroundColor(
4519 Preferences.getEditorColour("CurrentMarker"), self.currentline)
4520 self.setMarkerForegroundColor(
4521 Preferences.getEditorColour("ErrorMarker"), self.errorline)
4522 self.setMarkerBackgroundColor(
4523 Preferences.getEditorColour("ErrorMarker"), self.errorline)
4524
4525 def __setMarginsDisplay(self):
4526 """
4527 Private method to configure margins 0 and 2.
4528 """
4529 # set the settings for all margins
4530 self.setMarginsFont(Preferences.getEditorOtherFonts("MarginsFont"))
4531 self.setMarginsForegroundColor(
4532 Preferences.getEditorColour("MarginsForeground"))
4533 self.setMarginsBackgroundColor(
4534 Preferences.getEditorColour("MarginsBackground"))
4535
4536 # reset standard margins settings
4537 for margin in range(5):
4538 self.setMarginLineNumbers(margin, False)
4539 self.setMarginMarkerMask(margin, 0)
4540 self.setMarginWidth(margin, 0)
4541 self.setMarginSensitivity(margin, False)
4542
4543 # set marker margin(s) settings
4544 self.__bmMargin = 0
4545 self.__linenoMargin = 1
4546 self.__bpMargin = 2
4547 self.__foldMargin = 3
4548 self.__indicMargin = 4
4549
4550 marginBmMask = (1 << self.bookmark)
4551 self.setMarginWidth(self.__bmMargin, 16)
4552 self.setMarginSensitivity(self.__bmMargin, True)
4553 self.setMarginMarkerMask(self.__bmMargin, marginBmMask)
4554
4555 marginBpMask = (
4556 (1 << self.breakpoint) |
4557 (1 << self.cbreakpoint) |
4558 (1 << self.tbreakpoint) |
4559 (1 << self.tcbreakpoint) |
4560 (1 << self.dbreakpoint)
4561 )
4562 self.setMarginWidth(self.__bpMargin, 16)
4563 self.setMarginSensitivity(self.__bpMargin, True)
4564 self.setMarginMarkerMask(self.__bpMargin, marginBpMask)
4565
4566 marginIndicMask = (
4567 (1 << self.syntaxerror) |
4568 (1 << self.notcovered) |
4569 (1 << self.taskmarker) |
4570 (1 << self.warning) |
4571 (1 << self.__changeMarkerUnsaved) |
4572 (1 << self.__changeMarkerSaved) |
4573 (1 << self.currentline) |
4574 (1 << self.errorline)
4575 )
4576 self.setMarginWidth(self.__indicMargin, 16)
4577 self.setMarginSensitivity(self.__indicMargin, True)
4578 self.setMarginMarkerMask(self.__indicMargin, marginIndicMask)
4579
4580 # set linenumber margin settings
4581 linenoMargin = Preferences.getEditor("LinenoMargin")
4582 self.setMarginLineNumbers(self.__linenoMargin, linenoMargin)
4583 if linenoMargin:
4584 self.__resizeLinenoMargin()
4585 else:
4586 self.setMarginWidth(self.__linenoMargin, 0)
4587
4588 # set folding margin settings
4589 if Preferences.getEditor("FoldingMargin"):
4590 self.setMarginWidth(self.__foldMargin, 16)
4591 folding = Preferences.getEditor("FoldingStyle")
4592 self.setFolding(folding, self.__foldMargin)
4593 self.setFoldMarginColors(
4594 Preferences.getEditorColour("FoldmarginBackground"),
4595 Preferences.getEditorColour("FoldmarginBackground"))
4596 self.setFoldMarkersColors(
4597 Preferences.getEditorColour("FoldMarkersForeground"),
4598 Preferences.getEditorColour("FoldMarkersBackground"))
4599 else:
4600 self.setMarginWidth(self.__foldMargin, 0)
4601 self.setFolding(QsciScintilla.FoldStyle.NoFoldStyle.value,
4602 self.__foldMargin)
4603
4604 def __resizeLinenoMargin(self):
4605 """
4606 Private slot to resize the line numbers margin.
4607 """
4608 linenoMargin = Preferences.getEditor("LinenoMargin")
4609 if linenoMargin:
4610 self.setMarginWidth(
4611 self.__linenoMargin, '8' * (len(str(self.lines())) + 1))
4612
4613 def __setTabAndIndent(self):
4614 """
4615 Private method to set indentation size and style and tab width.
4616 """
4617 self.setTabWidth(self.__getEditorConfig("TabWidth"))
4618 self.setIndentationWidth(self.__getEditorConfig("IndentWidth"))
4619 if self.lexer_ and self.lexer_.alwaysKeepTabs():
4620 self.setIndentationsUseTabs(True)
4621 else:
4622 self.setIndentationsUseTabs(
4623 self.__getEditorConfig("TabForIndentation"))
4624
4625 def __setTextDisplay(self):
4626 """
4627 Private method to configure the text display.
4628 """
4629 self.__setTabAndIndent()
4630
4631 self.setTabIndents(Preferences.getEditor("TabIndents"))
4632 self.setBackspaceUnindents(Preferences.getEditor("TabIndents"))
4633 self.setIndentationGuides(Preferences.getEditor("IndentationGuides"))
4634 self.setIndentationGuidesBackgroundColor(
4635 Preferences.getEditorColour("IndentationGuidesBackground"))
4636 self.setIndentationGuidesForegroundColor(
4637 Preferences.getEditorColour("IndentationGuidesForeground"))
4638 if Preferences.getEditor("ShowWhitespace"):
4639 self.setWhitespaceVisibility(
4640 QsciScintilla.WhitespaceVisibility.WsVisible)
4641 with contextlib.suppress(AttributeError):
4642 self.setWhitespaceForegroundColor(
4643 Preferences.getEditorColour("WhitespaceForeground"))
4644 self.setWhitespaceBackgroundColor(
4645 Preferences.getEditorColour("WhitespaceBackground"))
4646 self.setWhitespaceSize(
4647 Preferences.getEditor("WhitespaceSize"))
4648 else:
4649 self.setWhitespaceVisibility(
4650 QsciScintilla.WhitespaceVisibility.WsInvisible)
4651 self.setEolVisibility(Preferences.getEditor("ShowEOL"))
4652 self.setAutoIndent(Preferences.getEditor("AutoIndentation"))
4653 if Preferences.getEditor("BraceHighlighting"):
4654 self.setBraceMatching(QsciScintilla.BraceMatch.SloppyBraceMatch)
4655 else:
4656 self.setBraceMatching(QsciScintilla.BraceMatch.NoBraceMatch)
4657 self.setMatchedBraceForegroundColor(
4658 Preferences.getEditorColour("MatchingBrace"))
4659 self.setMatchedBraceBackgroundColor(
4660 Preferences.getEditorColour("MatchingBraceBack"))
4661 self.setUnmatchedBraceForegroundColor(
4662 Preferences.getEditorColour("NonmatchingBrace"))
4663 self.setUnmatchedBraceBackgroundColor(
4664 Preferences.getEditorColour("NonmatchingBraceBack"))
4665 if Preferences.getEditor("CustomSelectionColours"):
4666 self.setSelectionBackgroundColor(
4667 Preferences.getEditorColour("SelectionBackground"))
4668 else:
4669 self.setSelectionBackgroundColor(
4670 QApplication.palette().color(QPalette.ColorRole.Highlight))
4671 if Preferences.getEditor("ColourizeSelText"):
4672 self.resetSelectionForegroundColor()
4673 elif Preferences.getEditor("CustomSelectionColours"):
4674 self.setSelectionForegroundColor(
4675 Preferences.getEditorColour("SelectionForeground"))
4676 else:
4677 self.setSelectionForegroundColor(
4678 QApplication.palette().color(
4679 QPalette.ColorRole.HighlightedText))
4680 self.setSelectionToEol(Preferences.getEditor("ExtendSelectionToEol"))
4681 self.setCaretForegroundColor(
4682 Preferences.getEditorColour("CaretForeground"))
4683 self.setCaretLineBackgroundColor(
4684 Preferences.getEditorColour("CaretLineBackground"))
4685 self.setCaretLineVisible(Preferences.getEditor("CaretLineVisible"))
4686 self.setCaretLineAlwaysVisible(
4687 Preferences.getEditor("CaretLineAlwaysVisible"))
4688 self.caretWidth = Preferences.getEditor("CaretWidth")
4689 self.setCaretWidth(self.caretWidth)
4690 self.caretLineFrameWidth = Preferences.getEditor("CaretLineFrameWidth")
4691 self.setCaretLineFrameWidth(self.caretLineFrameWidth)
4692 self.useMonospaced = Preferences.getEditor("UseMonospacedFont")
4693 self.setMonospaced(self.useMonospaced)
4694 edgeMode = Preferences.getEditor("EdgeMode")
4695 edge = QsciScintilla.EdgeMode(edgeMode)
4696 self.setEdgeMode(edge)
4697 if edgeMode:
4698 self.setEdgeColumn(Preferences.getEditor("EdgeColumn"))
4699 self.setEdgeColor(Preferences.getEditorColour("Edge"))
4700
4701 wrapVisualFlag = Preferences.getEditor("WrapVisualFlag")
4702 self.setWrapMode(Preferences.getEditor("WrapLongLinesMode"))
4703 self.setWrapVisualFlags(wrapVisualFlag, wrapVisualFlag)
4704 self.setWrapIndentMode(Preferences.getEditor("WrapIndentMode"))
4705 self.setWrapStartIndent(Preferences.getEditor("WrapStartIndent"))
4706
4707 self.zoomTo(Preferences.getEditor("ZoomFactor"))
4708
4709 self.searchIndicator = QsciScintilla.INDIC_CONTAINER
4710 self.indicatorDefine(
4711 self.searchIndicator, QsciScintilla.INDIC_BOX,
4712 Preferences.getEditorColour("SearchMarkers"))
4713 if (
4714 not Preferences.getEditor("SearchMarkersEnabled") and
4715 not Preferences.getEditor("QuickSearchMarkersEnabled") and
4716 not Preferences.getEditor("MarkOccurrencesEnabled")
4717 ):
4718 self.clearAllIndicators(self.searchIndicator)
4719
4720 self.spellingIndicator = QsciScintilla.INDIC_CONTAINER + 1
4721 self.indicatorDefine(
4722 self.spellingIndicator, QsciScintilla.INDIC_SQUIGGLE,
4723 Preferences.getEditorColour("SpellingMarkers"))
4724 self.__setSpelling()
4725
4726 self.highlightIndicator = QsciScintilla.INDIC_CONTAINER + 2
4727 self.indicatorDefine(
4728 self.highlightIndicator, QsciScintilla.INDIC_FULLBOX,
4729 Preferences.getEditorColour("HighlightMarker"))
4730
4731 self.setCursorFlashTime(QApplication.cursorFlashTime())
4732
4733 with contextlib.suppress(AttributeError):
4734 if Preferences.getEditor("AnnotationsEnabled"):
4735 self.setAnnotationDisplay(
4736 QsciScintilla.AnnotationDisplay.AnnotationBoxed)
4737 else:
4738 self.setAnnotationDisplay(
4739 QsciScintilla.AnnotationDisplay.AnnotationHidden)
4740 self.__setAnnotationStyles()
4741
4742 if Preferences.getEditor("OverrideEditAreaColours"):
4743 self.setColor(Preferences.getEditorColour("EditAreaForeground"))
4744 self.setPaper(Preferences.getEditorColour("EditAreaBackground"))
4745
4746 self.setVirtualSpaceOptions(
4747 Preferences.getEditor("VirtualSpaceOptions"))
4748
4749 if Preferences.getEditor("MouseHoverHelp"):
4750 self.SendScintilla(QsciScintilla.SCI_SETMOUSEDWELLTIME,
4751 Preferences.getEditor("MouseHoverTimeout"))
4752 else:
4753 self.SendScintilla(QsciScintilla.SCI_SETMOUSEDWELLTIME,
4754 QsciScintilla.SC_TIME_FOREVER)
4755
4756 # to avoid errors due to line endings by pasting
4757 self.SendScintilla(QsciScintilla.SCI_SETPASTECONVERTENDINGS, True)
4758
4759 self.__markerMap.setEnabled(True)
4760
4761 def __setEolMode(self):
4762 """
4763 Private method to configure the eol mode of the editor.
4764 """
4765 if (
4766 self.fileName and
4767 self.project.isOpen() and
4768 self.project.isProjectFile(self.fileName)
4769 ):
4770 eolMode = self.__getEditorConfig("EOLMode", nodefault=True)
4771 if eolMode is None:
4772 eolStr = self.project.getEolString()
4773 self.setEolModeByEolString(eolStr)
4774 else:
4775 self.setEolMode(eolMode)
4776 else:
4777 eolMode = self.__getEditorConfig("EOLMode")
4778 self.setEolMode(eolMode)
4779 self.__eolChanged()
4780
4781 def __setAutoCompletion(self):
4782 """
4783 Private method to configure the autocompletion function.
4784 """
4785 if self.lexer_:
4786 self.setAutoCompletionFillupsEnabled(
4787 Preferences.getEditor("AutoCompletionFillups"))
4788 self.setAutoCompletionCaseSensitivity(
4789 Preferences.getEditor("AutoCompletionCaseSensitivity"))
4790 self.setAutoCompletionReplaceWord(
4791 Preferences.getEditor("AutoCompletionReplaceWord"))
4792 self.setAutoCompletionThreshold(0)
4793 if Preferences.getEditor("AutoCompletionShowSingle"):
4794 self.setAutoCompletionUseSingle(
4795 QsciScintilla.AutoCompletionUseSingle.AcusAlways)
4796 else:
4797 self.setAutoCompletionUseSingle(
4798 QsciScintilla.AutoCompletionUseSingle.AcusNever)
4799 autoCompletionSource = Preferences.getEditor("AutoCompletionSource")
4800 if (
4801 autoCompletionSource ==
4802 QsciScintilla.AutoCompletionSource.AcsDocument
4803 ):
4804 self.setAutoCompletionSource(
4805 QsciScintilla.AutoCompletionSource.AcsDocument)
4806 elif (
4807 autoCompletionSource == QsciScintilla.AutoCompletionSource.AcsAPIs
4808 ):
4809 self.setAutoCompletionSource(
4810 QsciScintilla.AutoCompletionSource.AcsAPIs)
4811 else:
4812 self.setAutoCompletionSource(
4813 QsciScintilla.AutoCompletionSource.AcsAll)
4814
4815 self.setAutoCompletionWidgetSize(
4816 Preferences.getEditor("AutoCompletionMaxChars"),
4817 Preferences.getEditor("AutoCompletionMaxLines")
4818 )
4819
4820 def __setCallTips(self):
4821 """
4822 Private method to configure the calltips function.
4823 """
4824 self.setCallTipsBackgroundColor(
4825 Preferences.getEditorColour("CallTipsBackground"))
4826 self.setCallTipsForegroundColor(
4827 Preferences.getEditorColour("CallTipsForeground"))
4828 self.setCallTipsHighlightColor(
4829 Preferences.getEditorColour("CallTipsHighlight"))
4830 self.setCallTipsVisible(Preferences.getEditor("CallTipsVisible"))
4831 calltipsStyle = Preferences.getEditor("CallTipsStyle")
4832 with contextlib.suppress(AttributeError):
4833 self.setCallTipsPosition(
4834 Preferences.getEditor("CallTipsPosition"))
4835
4836 if Preferences.getEditor("CallTipsEnabled"):
4837 if calltipsStyle == QsciScintilla.CallTipsStyle.CallTipsNoContext:
4838 self.setCallTipsStyle(
4839 QsciScintilla.CallTipsStyle.CallTipsNoContext)
4840 elif (
4841 calltipsStyle ==
4842 QsciScintilla.CallTipsStyle.CallTipsNoAutoCompletionContext
4843 ):
4844 self.setCallTipsStyle(
4845 QsciScintilla.CallTipsStyle
4846 .CallTipsNoAutoCompletionContext)
4847 else:
4848 self.setCallTipsStyle(
4849 QsciScintilla.CallTipsStyle.CallTipsContext)
4850 else:
4851 self.setCallTipsStyle(QsciScintilla.CallTipsStyle.CallTipsNone)
4852
4853 ###########################################################################
4854 ## Autocompletion handling methods below
4855 ###########################################################################
4856
4857 def canAutoCompleteFromAPIs(self):
4858 """
4859 Public method to check for API availablity.
4860
4861 @return flag indicating autocompletion from APIs is available (boolean)
4862 """
4863 return self.acAPI
4864
4865 def autoCompleteQScintilla(self):
4866 """
4867 Public method to perform an autocompletion using QScintilla methods.
4868 """
4869 self.__acText = ' ' # Prevent long running ACs to add results
4870 self.__acWatchdog.stop()
4871 if self.__acCompletions:
4872 return
4873
4874 acs = Preferences.getEditor("AutoCompletionSource")
4875 if acs == QsciScintilla.AutoCompletionSource.AcsDocument:
4876 self.autoCompleteFromDocument()
4877 elif acs == QsciScintilla.AutoCompletionSource.AcsAPIs:
4878 self.autoCompleteFromAPIs()
4879 elif acs == QsciScintilla.AutoCompletionSource.AcsAll:
4880 self.autoCompleteFromAll()
4881 else:
4882 EricMessageBox.information(
4883 self,
4884 self.tr("Autocompletion"),
4885 self.tr(
4886 """Autocompletion is not available because"""
4887 """ there is no autocompletion source set."""))
4888
4889 def setAutoCompletionEnabled(self, enable):
4890 """
4891 Public method to enable/disable autocompletion.
4892
4893 @param enable flag indicating the desired autocompletion status
4894 (boolean)
4895 """
4896 if enable:
4897 autoCompletionSource = Preferences.getEditor(
4898 "AutoCompletionSource")
4899 if (
4900 autoCompletionSource ==
4901 QsciScintilla.AutoCompletionSource.AcsDocument
4902 ):
4903 self.setAutoCompletionSource(
4904 QsciScintilla.AutoCompletionSource.AcsDocument)
4905 elif (
4906 autoCompletionSource ==
4907 QsciScintilla.AutoCompletionSource.AcsAPIs
4908 ):
4909 self.setAutoCompletionSource(
4910 QsciScintilla.AutoCompletionSource.AcsAPIs)
4911 else:
4912 self.setAutoCompletionSource(
4913 QsciScintilla.AutoCompletionSource.AcsAll)
4914
4915 def __toggleAutoCompletionEnable(self):
4916 """
4917 Private slot to handle the Enable Autocompletion context menu entry.
4918 """
4919 if self.menuActs["AutoCompletionEnable"].isChecked():
4920 self.setAutoCompletionEnabled(True)
4921 else:
4922 self.setAutoCompletionEnabled(False)
4923
4924 #################################################################
4925 ## Support for autocompletion hook methods
4926 #################################################################
4927
4928 def __charAdded(self, charNumber):
4929 """
4930 Private slot called to handle the user entering a character.
4931
4932 @param charNumber value of the character entered (integer)
4933 """
4934 char = chr(charNumber)
4935 # update code documentation viewer
4936 if (
4937 char == "(" and
4938 Preferences.getDocuViewer("ShowInfoOnOpenParenthesis")
4939 ):
4940 self.vm.showEditorInfo(self)
4941
4942 self.__delayedDocstringMenuPopup(self.getCursorPosition())
4943
4944 if self.isListActive():
4945 if self.__isStartChar(char):
4946 self.cancelList()
4947 self.autoComplete(auto=True, context=True)
4948 return
4949 elif char == '(':
4950 self.cancelList()
4951 else:
4952 self.__acTimer.stop()
4953
4954 if (
4955 self.callTipsStyle() !=
4956 QsciScintilla.CallTipsStyle.CallTipsNone and
4957 self.lexer_ is not None and chr(charNumber) in '()'
4958 ):
4959 self.callTip()
4960
4961 if not self.isCallTipActive():
4962 char = chr(charNumber)
4963 if self.__isStartChar(char):
4964 self.autoComplete(auto=True, context=True)
4965 return
4966
4967 line, col = self.getCursorPosition()
4968 txt = self.getWordLeft(line, col)
4969 if len(txt) >= Preferences.getEditor("AutoCompletionThreshold"):
4970 self.autoComplete(auto=True, context=False)
4971 return
4972
4973 def __isStartChar(self, ch):
4974 """
4975 Private method to check, if a character is an autocompletion start
4976 character.
4977
4978 @param ch character to be checked (one character string)
4979 @return flag indicating the result (boolean)
4980 """
4981 if self.lexer_ is None:
4982 return False
4983
4984 wseps = self.lexer_.autoCompletionWordSeparators()
4985 return any(wsep.endswith(ch) for wsep in wseps)
4986
4987 def __autocompletionCancelled(self):
4988 """
4989 Private slot to handle the cancellation of an auto-completion list.
4990 """
4991 self.__acWatchdog.stop()
4992
4993 self.__acText = ""
4994
4995 #################################################################
4996 ## auto-completion hook interfaces
4997 #################################################################
4998
4999 def addCompletionListHook(self, key, func, asynchroneous=False):
5000 """
5001 Public method to set an auto-completion list provider.
5002
5003 @param key name of the provider
5004 @type str
5005 @param func function providing completion list. func
5006 should be a function taking a reference to the editor and
5007 a boolean indicating to complete a context. It should return
5008 the possible completions as a list of strings.
5009 @type function(editor, bool) -> list of str in case async is False
5010 and function(editor, bool, str) returning nothing in case async
5011 is True
5012 @param asynchroneous flag indicating an asynchroneous function
5013 @type bool
5014 """
5015 if (
5016 key in self.__completionListHookFunctions or
5017 key in self.__completionListAsyncHookFunctions
5018 ):
5019 # it was already registered
5020 EricMessageBox.warning(
5021 self,
5022 self.tr("Auto-Completion Provider"),
5023 self.tr("""The completion list provider '{0}' was already"""
5024 """ registered. Ignoring duplicate request.""")
5025 .format(key))
5026 return
5027
5028 if asynchroneous:
5029 self.__completionListAsyncHookFunctions[key] = func
5030 else:
5031 self.__completionListHookFunctions[key] = func
5032
5033 def removeCompletionListHook(self, key):
5034 """
5035 Public method to remove a previously registered completion list
5036 provider.
5037
5038 @param key name of the provider
5039 @type str
5040 """
5041 if key in self.__completionListHookFunctions:
5042 del self.__completionListHookFunctions[key]
5043 elif key in self.__completionListAsyncHookFunctions:
5044 del self.__completionListAsyncHookFunctions[key]
5045
5046 def getCompletionListHook(self, key):
5047 """
5048 Public method to get the registered completion list provider.
5049
5050 @param key name of the provider
5051 @type str
5052 @return function providing completion list
5053 @rtype function or None
5054 """
5055 return (self.__completionListHookFunctions.get(key) or
5056 self.__completionListAsyncHookFunctions.get(key))
5057
5058 def autoComplete(self, auto=False, context=True):
5059 """
5060 Public method to start auto-completion.
5061
5062 @param auto flag indicating a call from the __charAdded method
5063 (boolean)
5064 @param context flag indicating to complete a context (boolean)
5065 """
5066 if auto and not Preferences.getEditor("AutoCompletionEnabled"):
5067 # auto-completion is disabled
5068 return
5069
5070 if self.isListActive():
5071 self.cancelList()
5072
5073 if (
5074 self.__completionListHookFunctions or
5075 self.__completionListAsyncHookFunctions
5076 ):
5077 # Avoid delayed auto-completion after cursor repositioning
5078 self.__acText = self.__getAcText()
5079 if auto and Preferences.getEditor("AutoCompletionTimeout"):
5080 self.__acTimer.stop()
5081 self.__acContext = context
5082 self.__acTimer.start()
5083 else:
5084 self.__autoComplete(auto, context)
5085 elif (
5086 not auto or
5087 (self.autoCompletionSource() !=
5088 QsciScintilla.AutoCompletionSource.AcsNone)
5089 ):
5090 self.autoCompleteQScintilla()
5091
5092 def __getAcText(self):
5093 """
5094 Private method to get the text from cursor position for autocompleting.
5095
5096 @return text left of cursor position
5097 @rtype str
5098 """
5099 line, col = self.getCursorPosition()
5100 text = self.text(line)
5101 try:
5102 acText = (
5103 self.getWordLeft(line, col - 1) + text[col - 1]
5104 if self.__isStartChar(text[col - 1]) else
5105 self.getWordLeft(line, col)
5106 )
5107 except IndexError:
5108 acText = ""
5109
5110 return acText
5111
5112 def __autoComplete(self, auto=True, context=None):
5113 """
5114 Private method to start auto-completion via plug-ins.
5115
5116 @param auto flag indicating a call from the __charAdded method
5117 (boolean)
5118 @param context flag indicating to complete a context
5119 @type bool or None
5120 """
5121 self.__acCompletions.clear()
5122 self.__acCompletionsFinished = 0
5123
5124 # Suppress empty completions
5125 if auto and self.__acText == '':
5126 return
5127
5128 completions = (
5129 self.__acCache.get(self.__acText)
5130 if self.__acCacheEnabled else
5131 None
5132 )
5133 if completions is not None:
5134 # show list with cached entries
5135 if self.isListActive():
5136 self.cancelList()
5137
5138 self.__showCompletionsList(completions)
5139 else:
5140 if context is None:
5141 context = self.__acContext
5142
5143 for key in self.__completionListAsyncHookFunctions:
5144 self.__completionListAsyncHookFunctions[key](
5145 self, context, self.__acText)
5146
5147 for key in self.__completionListHookFunctions:
5148 completions = self.__completionListHookFunctions[key](
5149 self, context)
5150 self.completionsListReady(completions, self.__acText)
5151
5152 if Preferences.getEditor("AutoCompletionScintillaOnFail"):
5153 self.__acWatchdog.start()
5154
5155 def completionsListReady(self, completions, acText):
5156 """
5157 Public method to show the completions determined by a completions
5158 provider.
5159
5160 @param completions list of possible completions
5161 @type list of str or set of str
5162 @param acText text to be completed
5163 @type str
5164 """
5165 currentWord = self.__getAcText() or ' '
5166 # process the list only, if not already obsolete ...
5167 if acText != self.__acText or not self.__acText.endswith(currentWord):
5168 # Suppress auto-completion done by QScintilla as fallback
5169 self.__acWatchdog.stop()
5170 return
5171
5172 self.__acCompletions.update(set(completions))
5173
5174 self.__acCompletionsFinished += 1
5175 # Got all results from auto completer?
5176 if self.__acCompletionsFinished >= (
5177 len(self.__completionListAsyncHookFunctions) +
5178 len(self.__completionListHookFunctions)
5179 ):
5180 self.__acWatchdog.stop()
5181
5182 # Autocomplete with QScintilla if no results present
5183 if (
5184 Preferences.getEditor("AutoCompletionScintillaOnFail") and
5185 not self.__acCompletions
5186 ):
5187 self.autoCompleteQScintilla()
5188 return
5189
5190 # ... or completions are not empty
5191 if not bool(completions):
5192 return
5193
5194 if self.isListActive():
5195 self.cancelList()
5196
5197 if self.__acCompletions:
5198 if self.__acCacheEnabled:
5199 self.__acCache.add(acText, set(self.__acCompletions))
5200 self.__showCompletionsList(self.__acCompletions)
5201
5202 def __showCompletionsList(self, completions):
5203 """
5204 Private method to show the completions list.
5205
5206 @param completions completions to be shown
5207 @type list of str or set of str
5208 """
5209 acCompletions = (
5210 sorted(completions,
5211 key=self.__replaceLeadingUnderscores)
5212 if Preferences.getEditor("AutoCompletionReversedList") else
5213 sorted(completions)
5214 )
5215 self.showUserList(EditorAutoCompletionListID, acCompletions)
5216
5217 def __replaceLeadingUnderscores(self, txt):
5218 """
5219 Private method to replace the first two underlines for invers sorting.
5220
5221 @param txt completion text
5222 @type str
5223 @return modified completion text
5224 @rtype str
5225 """
5226 if txt.startswith('_'):
5227 return txt[:2].replace('_', '~') + txt[2:]
5228 else:
5229 return txt
5230
5231 def __clearCompletionsCache(self):
5232 """
5233 Private method to clear the auto-completions cache.
5234 """
5235 self.__acCache.clear()
5236
5237 def __completionListSelected(self, listId, txt):
5238 """
5239 Private slot to handle the selection from the completion list.
5240
5241 @param listId the ID of the user list (should be 1 or 2) (integer)
5242 @param txt the selected text (string)
5243 """
5244 # custom completions via plug-ins
5245 if listId == EditorAutoCompletionListID:
5246 lst = txt.split()
5247 if len(lst) > 1:
5248 txt = lst[0]
5249
5250 self.beginUndoAction()
5251 if Preferences.getEditor("AutoCompletionReplaceWord"):
5252 self.selectCurrentWord()
5253 self.removeSelectedText()
5254 line, col = self.getCursorPosition()
5255 else:
5256 line, col = self.getCursorPosition()
5257 wLeft = self.getWordLeft(line, col)
5258 if not txt.startswith(wLeft):
5259 self.selectCurrentWord()
5260 self.removeSelectedText()
5261 line, col = self.getCursorPosition()
5262 elif wLeft:
5263 txt = txt[len(wLeft):]
5264
5265 if txt and txt[0] in "'\"":
5266 # New in jedi 0.16: AC of dict keys
5267 txt = txt[1:]
5268 self.insert(txt)
5269 self.endUndoAction()
5270 self.setCursorPosition(line, col + len(txt))
5271
5272 # template completions
5273 elif listId == TemplateCompletionListID:
5274 self.__applyTemplate(txt, self.getLanguage())
5275
5276 # 'goto reference' completions
5277 elif listId == ReferencesListID:
5278 with contextlib.suppress(ValueError, IndexError):
5279 index = self.__referencesList.index(txt)
5280 filename, line, column = self.__referencesPositionsList[index]
5281 self.vm.openSourceFile(
5282 filename, lineno=line, pos=column, addNext=True)
5283
5284 def canProvideDynamicAutoCompletion(self):
5285 """
5286 Public method to test the dynamic auto-completion availability.
5287
5288 @return flag indicating the availability of dynamic auto-completion
5289 (boolean)
5290 """
5291 return (self.acAPI or
5292 bool(self.__completionListHookFunctions) or
5293 bool(self.__completionListAsyncHookFunctions))
5294
5295 #################################################################
5296 ## call-tip hook interfaces
5297 #################################################################
5298
5299 def addCallTipHook(self, key, func):
5300 """
5301 Public method to set a calltip provider.
5302
5303 @param key name of the provider
5304 @type str
5305 @param func function providing calltips. func
5306 should be a function taking a reference to the editor,
5307 a position into the text and the amount of commas to the
5308 left of the cursor. It should return the possible
5309 calltips as a list of strings.
5310 @type function(editor, int, int) -> list of str
5311 """
5312 if key in self.__ctHookFunctions:
5313 # it was already registered
5314 EricMessageBox.warning(
5315 self,
5316 self.tr("Call-Tips Provider"),
5317 self.tr("""The call-tips provider '{0}' was already"""
5318 """ registered. Ignoring duplicate request.""")
5319 .format(key))
5320 return
5321
5322 self.__ctHookFunctions[key] = func
5323
5324 def removeCallTipHook(self, key):
5325 """
5326 Public method to remove a previously registered calltip provider.
5327
5328 @param key name of the provider
5329 @type str
5330 """
5331 if key in self.__ctHookFunctions:
5332 del self.__ctHookFunctions[key]
5333
5334 def getCallTipHook(self, key):
5335 """
5336 Public method to get the registered calltip provider.
5337
5338 @param key name of the provider
5339 @type str
5340 @return function providing calltips
5341 @rtype function or None
5342 """
5343 if key in self.__ctHookFunctions:
5344 return self.__ctHookFunctions[key]
5345 else:
5346 return None
5347
5348 def canProvideCallTipps(self):
5349 """
5350 Public method to test the calltips availability.
5351
5352 @return flag indicating the availability of calltips (boolean)
5353 """
5354 return (self.acAPI or
5355 bool(self.__ctHookFunctions))
5356
5357 def callTip(self):
5358 """
5359 Public method to show calltips.
5360 """
5361 if bool(self.__ctHookFunctions):
5362 self.__callTip()
5363 else:
5364 super().callTip()
5365
5366 def __callTip(self):
5367 """
5368 Private method to show call tips provided by a plugin.
5369 """
5370 pos = self.currentPosition()
5371
5372 # move backward to the start of the current calltip working out
5373 # which argument to highlight
5374 commas = 0
5375 found = False
5376 ch, pos = self.__getCharacter(pos)
5377 while ch:
5378 if ch == ',':
5379 commas += 1
5380 elif ch == ')':
5381 depth = 1
5382
5383 # ignore everything back to the start of the corresponding
5384 # parenthesis
5385 ch, pos = self.__getCharacter(pos)
5386 while ch:
5387 if ch == ')':
5388 depth += 1
5389 elif ch == '(':
5390 depth -= 1
5391 if depth == 0:
5392 break
5393 ch, pos = self.__getCharacter(pos)
5394 elif ch == '(':
5395 found = True
5396 break
5397
5398 ch, pos = self.__getCharacter(pos)
5399
5400 self.cancelCallTips()
5401
5402 if not found:
5403 return
5404
5405 callTips = []
5406 if self.__ctHookFunctions:
5407 for key in self.__ctHookFunctions:
5408 callTips.extend(self.__ctHookFunctions[key](self, pos, commas))
5409 callTips = list(set(callTips))
5410 callTips.sort()
5411 else:
5412 # try QScintilla calltips
5413 super().callTip()
5414 return
5415 if len(callTips) == 0:
5416 if Preferences.getEditor("CallTipsScintillaOnFail"):
5417 # try QScintilla calltips
5418 super().callTip()
5419 return
5420
5421 ctshift = 0
5422 for ct in callTips:
5423 shift = ct.index("(")
5424 if ctshift < shift:
5425 ctshift = shift
5426
5427 cv = self.callTipsVisible()
5428 ct = (
5429 # this is just a safe guard
5430 self._encodeString("\n".join(callTips[:cv]))
5431 if cv > 0 else
5432 # until here and unindent below
5433 self._encodeString("\n".join(callTips))
5434 )
5435
5436 self.SendScintilla(QsciScintilla.SCI_CALLTIPSHOW,
5437 self.__adjustedCallTipPosition(ctshift, pos), ct)
5438 if b'\n' in ct:
5439 return
5440
5441 # Highlight the current argument
5442 if commas == 0:
5443 astart = ct.find(b'(')
5444 else:
5445 astart = ct.find(b',')
5446 commas -= 1
5447 while astart != -1 and commas > 0:
5448 astart = ct.find(b',', astart + 1)
5449 commas -= 1
5450
5451 if astart == -1:
5452 return
5453
5454 depth = 0
5455 for aend in range(astart + 1, len(ct)):
5456 ch = ct[aend:aend + 1]
5457
5458 if ch == b',' and depth == 0:
5459 break
5460 elif ch == b'(':
5461 depth += 1
5462 elif ch == b')':
5463 if depth == 0:
5464 break
5465
5466 depth -= 1
5467
5468 if astart != aend:
5469 self.SendScintilla(QsciScintilla.SCI_CALLTIPSETHLT,
5470 astart + 1, aend)
5471
5472 def __adjustedCallTipPosition(self, ctshift, pos):
5473 """
5474 Private method to calculate an adjusted position for showing calltips.
5475
5476 @param ctshift amount the calltip shall be shifted (integer)
5477 @param pos position into the text (integer)
5478 @return new position for the calltip (integer)
5479 """
5480 ct = pos
5481 if ctshift:
5482 ctmin = self.SendScintilla(
5483 QsciScintilla.SCI_POSITIONFROMLINE,
5484 self.SendScintilla(QsciScintilla.SCI_LINEFROMPOSITION, ct))
5485 if ct - ctshift < ctmin:
5486 ct = ctmin
5487 else:
5488 ct -= ctshift
5489 return ct
5490
5491 #################################################################
5492 ## Methods needed by the code documentation viewer
5493 #################################################################
5494
5495 def __showCodeInfo(self):
5496 """
5497 Private slot to handle the context menu action to show code info.
5498 """
5499 self.vm.showEditorInfo(self)
5500
5501 #################################################################
5502 ## Methods needed by the context menu
5503 #################################################################
5504
5505 def __marginNumber(self, xPos):
5506 """
5507 Private method to calculate the margin number based on a x position.
5508
5509 @param xPos x position (integer)
5510 @return margin number (integer, -1 for no margin)
5511 """
5512 width = 0
5513 for margin in range(5):
5514 width += self.marginWidth(margin)
5515 if xPos <= width:
5516 return margin
5517 return -1
5518
5519 def contextMenuEvent(self, evt):
5520 """
5521 Protected method implementing the context menu event.
5522
5523 @param evt the context menu event (QContextMenuEvent)
5524 """
5525 evt.accept()
5526 if self.__marginNumber(evt.x()) == -1:
5527 self.spellingMenuPos = self.positionFromPoint(evt.pos())
5528 if (
5529 self.spellingMenuPos >= 0 and
5530 self.spell is not None and
5531 self.hasIndicator(self.spellingIndicator,
5532 self.spellingMenuPos)
5533 ):
5534 self.spellingMenu.popup(evt.globalPos())
5535 else:
5536 self.menu.popup(evt.globalPos())
5537 else:
5538 self.line = self.lineAt(evt.pos())
5539 if self.__marginNumber(evt.x()) in [self.__bmMargin,
5540 self.__linenoMargin]:
5541 self.bmMarginMenu.popup(evt.globalPos())
5542 elif self.__marginNumber(evt.x()) == self.__bpMargin:
5543 self.bpMarginMenu.popup(evt.globalPos())
5544 elif self.__marginNumber(evt.x()) == self.__indicMargin:
5545 self.indicMarginMenu.popup(evt.globalPos())
5546 elif self.__marginNumber(evt.x()) == self.__foldMargin:
5547 self.foldMarginMenu.popup(evt.globalPos())
5548
5549 def __showContextMenu(self):
5550 """
5551 Private slot handling the aboutToShow signal of the context menu.
5552 """
5553 self.menuActs["Reopen"].setEnabled(
5554 not self.isModified() and bool(self.fileName))
5555 self.menuActs["Save"].setEnabled(self.isModified())
5556 self.menuActs["Undo"].setEnabled(self.isUndoAvailable())
5557 self.menuActs["Redo"].setEnabled(self.isRedoAvailable())
5558 self.menuActs["Revert"].setEnabled(self.isModified())
5559 self.menuActs["Cut"].setEnabled(self.hasSelectedText())
5560 self.menuActs["Copy"].setEnabled(self.hasSelectedText())
5561 if self.menuActs["ExecuteSelection"] is not None:
5562 self.menuActs["ExecuteSelection"].setEnabled(
5563 self.hasSelectedText())
5564 self.menuActs["Paste"].setEnabled(self.canPaste())
5565 if not self.isResourcesFile:
5566 if self.fileName and self.isPyFile():
5567 self.menuActs["Show"].setEnabled(True)
5568 else:
5569 self.menuActs["Show"].setEnabled(False)
5570 if (
5571 self.fileName and
5572 (self.isPyFile() or self.isRubyFile())
5573 ):
5574 self.menuActs["Diagrams"].setEnabled(True)
5575 else:
5576 self.menuActs["Diagrams"].setEnabled(False)
5577 if not self.miniMenu:
5578 if self.lexer_ is not None:
5579 self.menuActs["Comment"].setEnabled(
5580 self.lexer_.canBlockComment())
5581 self.menuActs["Uncomment"].setEnabled(
5582 self.lexer_.canBlockComment())
5583 else:
5584 self.menuActs["Comment"].setEnabled(False)
5585 self.menuActs["Uncomment"].setEnabled(False)
5586
5587 cline = self.getCursorPosition()[0]
5588 line = self.text(cline)
5589 self.menuActs["Docstring"].setEnabled(
5590 self.getDocstringGenerator().isFunctionStart(line))
5591
5592 self.menuActs["TypingAidsEnabled"].setEnabled(
5593 self.completer is not None)
5594 self.menuActs["TypingAidsEnabled"].setChecked(
5595 self.completer is not None and self.completer.isEnabled())
5596
5597 if not self.isResourcesFile:
5598 self.menuActs["calltip"].setEnabled(self.canProvideCallTipps())
5599 self.menuActs["codeInfo"].setEnabled(
5600 self.vm.isEditorInfoSupported(self.getLanguage()))
5601
5602 self.menuActs["MonospacedFont"].setEnabled(self.lexer_ is None)
5603
5604 splitOrientation = self.vm.getSplitOrientation()
5605 if splitOrientation == Qt.Orientation.Horizontal:
5606 self.menuActs["NewSplit"].setIcon(
5607 UI.PixmapCache.getIcon("splitHorizontal"))
5608 else:
5609 self.menuActs["NewSplit"].setIcon(
5610 UI.PixmapCache.getIcon("splitVertical"))
5611
5612 self.menuActs["Tools"].setEnabled(not self.toolsMenu.isEmpty())
5613
5614 self.showMenu.emit("Main", self.menu, self)
5615
5616 def __showContextMenuAutocompletion(self):
5617 """
5618 Private slot called before the autocompletion menu is shown.
5619 """
5620 self.menuActs["acDynamic"].setEnabled(
5621 self.canProvideDynamicAutoCompletion())
5622 self.menuActs["acClearCache"].setEnabled(
5623 self.canProvideDynamicAutoCompletion())
5624 self.menuActs["acAPI"].setEnabled(self.acAPI)
5625 self.menuActs["acAPIDocument"].setEnabled(self.acAPI)
5626
5627 self.showMenu.emit("Autocompletion", self.autocompletionMenu, self)
5628
5629 def __showContextMenuShow(self):
5630 """
5631 Private slot called before the show menu is shown.
5632 """
5633 prEnable = False
5634 coEnable = False
5635
5636 # first check if the file belongs to a project
5637 if (
5638 self.project.isOpen() and
5639 self.project.isProjectSource(self.fileName)
5640 ):
5641 fn = self.project.getMainScript(True)
5642 if fn is not None:
5643 prEnable = (
5644 self.project.isPy3Project() and
5645 bool(Utilities.getProfileFileNames(fn))
5646 )
5647 coEnable = (
5648 self.project.isPy3Project() and
5649 bool(Utilities.getCoverageFileNames(fn))
5650 )
5651
5652 # now check ourselves
5653 fn = self.getFileName()
5654 if fn is not None:
5655 prEnable |= (
5656 self.project.isPy3Project() and
5657 bool(Utilities.getProfileFileName(fn))
5658 )
5659 coEnable |= (
5660 self.project.isPy3Project() and
5661 bool(Utilities.getCoverageFileName(fn))
5662 )
5663
5664 coEnable |= bool(self.__coverageFile)
5665
5666 # now check for syntax errors
5667 if self.hasSyntaxErrors():
5668 coEnable = False
5669
5670 self.profileMenuAct.setEnabled(prEnable)
5671 self.coverageMenuAct.setEnabled(coEnable)
5672 self.coverageShowAnnotationMenuAct.setEnabled(
5673 coEnable and len(self.notcoveredMarkers) == 0)
5674 self.coverageHideAnnotationMenuAct.setEnabled(
5675 len(self.notcoveredMarkers) > 0)
5676
5677 self.showMenu.emit("Show", self.menuShow, self)
5678
5679 def __showContextMenuGraphics(self):
5680 """
5681 Private slot handling the aboutToShow signal of the diagrams context
5682 menu.
5683 """
5684 if (
5685 self.project.isOpen() and
5686 self.project.isProjectSource(self.fileName)
5687 ):
5688 self.applicationDiagramMenuAct.setEnabled(True)
5689 else:
5690 self.applicationDiagramMenuAct.setEnabled(False)
5691
5692 self.showMenu.emit("Graphics", self.graphicsMenu, self)
5693
5694 def __showContextMenuMargin(self, menu):
5695 """
5696 Private slot handling the aboutToShow signal of the margins context
5697 menu.
5698
5699 @param menu reference to the menu to be shown
5700 @type QMenu
5701 """
5702 if menu is self.bpMarginMenu:
5703 supportsDebugger = bool(self.fileName and self.isPyFile())
5704 hasBreakpoints = bool(self.breaks)
5705 hasBreakpoint = bool(
5706 self.markersAtLine(self.line) & self.breakpointMask)
5707
5708 self.marginMenuActs["Breakpoint"].setEnabled(supportsDebugger)
5709 self.marginMenuActs["TempBreakpoint"].setEnabled(supportsDebugger)
5710 self.marginMenuActs["NextBreakpoint"].setEnabled(
5711 supportsDebugger and hasBreakpoints)
5712 self.marginMenuActs["PreviousBreakpoint"].setEnabled(
5713 supportsDebugger and hasBreakpoints)
5714 self.marginMenuActs["ClearBreakpoint"].setEnabled(
5715 supportsDebugger and hasBreakpoints)
5716 self.marginMenuActs["EditBreakpoint"].setEnabled(
5717 supportsDebugger and hasBreakpoint)
5718 self.marginMenuActs["EnableBreakpoint"].setEnabled(
5719 supportsDebugger and hasBreakpoint)
5720 if supportsDebugger:
5721 if self.markersAtLine(self.line) & (1 << self.dbreakpoint):
5722 self.marginMenuActs["EnableBreakpoint"].setText(
5723 self.tr('Enable breakpoint'))
5724 else:
5725 self.marginMenuActs["EnableBreakpoint"].setText(
5726 self.tr('Disable breakpoint'))
5727
5728 if menu is self.bmMarginMenu:
5729 hasBookmarks = bool(self.bookmarks)
5730
5731 self.marginMenuActs["NextBookmark"].setEnabled(hasBookmarks)
5732 self.marginMenuActs["PreviousBookmark"].setEnabled(hasBookmarks)
5733 self.marginMenuActs["ClearBookmark"].setEnabled(hasBookmarks)
5734
5735 if menu is self.foldMarginMenu:
5736 isFoldHeader = bool(self.SendScintilla(
5737 QsciScintilla.SCI_GETFOLDLEVEL, self.line) &
5738 QsciScintilla.SC_FOLDLEVELHEADERFLAG)
5739
5740 self.marginMenuActs["ExpandChildren"].setEnabled(isFoldHeader)
5741 self.marginMenuActs["CollapseChildren"].setEnabled(isFoldHeader)
5742
5743 if menu is self.indicMarginMenu:
5744 hasSyntaxErrors = bool(self.syntaxerrors)
5745 hasWarnings = bool(self.warnings)
5746 hasNotCoveredMarkers = bool(self.notcoveredMarkers)
5747
5748 self.marginMenuActs["GotoSyntaxError"].setEnabled(hasSyntaxErrors)
5749 self.marginMenuActs["ClearSyntaxError"].setEnabled(hasSyntaxErrors)
5750 if (
5751 hasSyntaxErrors and
5752 self.markersAtLine(self.line) & (1 << self.syntaxerror)
5753 ):
5754 self.marginMenuActs["ShowSyntaxError"].setEnabled(True)
5755 else:
5756 self.marginMenuActs["ShowSyntaxError"].setEnabled(False)
5757
5758 self.marginMenuActs["NextWarningMarker"].setEnabled(hasWarnings)
5759 self.marginMenuActs["PreviousWarningMarker"].setEnabled(
5760 hasWarnings)
5761 self.marginMenuActs["ClearWarnings"].setEnabled(hasWarnings)
5762 if (
5763 hasWarnings and
5764 self.markersAtLine(self.line) & (1 << self.warning)
5765 ):
5766 self.marginMenuActs["ShowWarning"].setEnabled(True)
5767 else:
5768 self.marginMenuActs["ShowWarning"].setEnabled(False)
5769
5770 self.marginMenuActs["NextCoverageMarker"].setEnabled(
5771 hasNotCoveredMarkers)
5772 self.marginMenuActs["PreviousCoverageMarker"].setEnabled(
5773 hasNotCoveredMarkers)
5774
5775 self.marginMenuActs["PreviousTaskMarker"].setEnabled(
5776 self.__hasTaskMarkers)
5777 self.marginMenuActs["NextTaskMarker"].setEnabled(
5778 self.__hasTaskMarkers)
5779
5780 self.marginMenuActs["PreviousChangeMarker"].setEnabled(
5781 self.__hasChangeMarkers)
5782 self.marginMenuActs["NextChangeMarker"].setEnabled(
5783 self.__hasChangeMarkers)
5784 self.marginMenuActs["ClearChangeMarkers"].setEnabled(
5785 self.__hasChangeMarkers)
5786
5787 self.showMenu.emit("Margin", menu, self)
5788
5789 def __showContextMenuChecks(self):
5790 """
5791 Private slot handling the aboutToShow signal of the checks context
5792 menu.
5793 """
5794 self.showMenu.emit("Checks", self.checksMenu, self)
5795
5796 def __showContextMenuTools(self):
5797 """
5798 Private slot handling the aboutToShow signal of the tools context
5799 menu.
5800 """
5801 self.showMenu.emit("Tools", self.toolsMenu, self)
5802
5803 def __reopenWithEncodingMenuTriggered(self, act):
5804 """
5805 Private method to handle the rereading of the file with a selected
5806 encoding.
5807
5808 @param act reference to the action that was triggered (QAction)
5809 """
5810 encoding = act.data()
5811 self.readFile(self.fileName, encoding=encoding)
5812 self.__convertTabs()
5813 self.__checkEncoding()
5814
5815 def __contextSave(self):
5816 """
5817 Private slot handling the save context menu entry.
5818 """
5819 ok = self.saveFile()
5820 if ok:
5821 self.vm.setEditorName(self, self.fileName)
5822
5823 def __contextSaveAs(self):
5824 """
5825 Private slot handling the save as context menu entry.
5826 """
5827 ok = self.saveFileAs()
5828 if ok:
5829 self.vm.setEditorName(self, self.fileName)
5830
5831 def __contextSaveCopy(self):
5832 """
5833 Private slot handling the save copy context menu entry.
5834 """
5835 self.saveFileCopy()
5836
5837 def __contextClose(self):
5838 """
5839 Private slot handling the close context menu entry.
5840 """
5841 self.vm.closeEditor(self)
5842
5843 def __newView(self):
5844 """
5845 Private slot to create a new view to an open document.
5846 """
5847 self.vm.newEditorView(self.fileName, self, self.filetype)
5848
5849 def __newViewNewSplit(self):
5850 """
5851 Private slot to create a new view to an open document.
5852 """
5853 self.vm.addSplit()
5854 self.vm.newEditorView(self.fileName, self, self.filetype)
5855
5856 def __selectAll(self):
5857 """
5858 Private slot handling the select all context menu action.
5859 """
5860 self.selectAll(True)
5861
5862 def __deselectAll(self):
5863 """
5864 Private slot handling the deselect all context menu action.
5865 """
5866 self.selectAll(False)
5867
5868 def joinLines(self):
5869 """
5870 Public slot to join the current line with the next one.
5871 """
5872 curLine = self.getCursorPosition()[0]
5873 if curLine == self.lines() - 1:
5874 return
5875
5876 line0Text = self.text(curLine)
5877 line1Text = self.text(curLine + 1)
5878 if line1Text in ["", "\r", "\n", "\r\n"]:
5879 return
5880
5881 if (
5882 line0Text.rstrip("\r\n\\ \t").endswith(("'", '"')) and
5883 line1Text.lstrip().startswith(("'", '"'))
5884 ):
5885 # merging multi line strings
5886 startChars = "\r\n\\ \t'\""
5887 endChars = " \t'\""
5888 else:
5889 startChars = "\r\n\\ \t"
5890 endChars = " \t"
5891
5892 # determine start index
5893 startIndex = len(line0Text)
5894 while startIndex > 0 and line0Text[startIndex - 1] in startChars:
5895 startIndex -= 1
5896 if startIndex == 0:
5897 return
5898
5899 # determine end index
5900 endIndex = 0
5901 while line1Text[endIndex] in endChars:
5902 endIndex += 1
5903
5904 self.setSelection(curLine, startIndex, curLine + 1, endIndex)
5905 self.beginUndoAction()
5906 self.removeSelectedText()
5907 self.insertAt(" ", curLine, startIndex)
5908 self.endUndoAction()
5909
5910 def shortenEmptyLines(self):
5911 """
5912 Public slot to compress lines consisting solely of whitespace
5913 characters.
5914 """
5915 searchRE = r"^[ \t]+$"
5916
5917 ok = self.findFirstTarget(searchRE, True, False, False, 0, 0)
5918 self.beginUndoAction()
5919 while ok:
5920 self.replaceTarget("")
5921 ok = self.findNextTarget()
5922 self.endUndoAction()
5923
5924 def __autosaveEnable(self):
5925 """
5926 Private slot handling the autosave enable context menu action.
5927 """
5928 if self.menuActs["AutosaveEnable"].isChecked():
5929 self.autosaveManuallyDisabled = False
5930 else:
5931 self.autosaveManuallyDisabled = True
5932
5933 def shouldAutosave(self):
5934 """
5935 Public slot to check the autosave flags.
5936
5937 @return flag indicating this editor should be saved (boolean)
5938 """
5939 return (
5940 bool(self.fileName) and
5941 not self.autosaveManuallyDisabled and
5942 not self.isReadOnly()
5943 )
5944
5945 def checkSyntax(self):
5946 """
5947 Public method to perform an automatic syntax check of the file.
5948 """
5949 fileType = self.filetype
5950 if fileType == "MicroPython":
5951 # adjustment for MicroPython
5952 fileType = "Python3"
5953
5954 if (
5955 self.syntaxCheckService is None or
5956 fileType not in self.syntaxCheckService.getLanguages()
5957 ):
5958 return
5959
5960 if Preferences.getEditor("AutoCheckSyntax"):
5961 if Preferences.getEditor("OnlineSyntaxCheck"):
5962 self.__onlineSyntaxCheckTimer.stop()
5963
5964 self.syntaxCheckService.syntaxCheck(
5965 fileType, self.fileName or "(Unnamed)", self.text())
5966
5967 def __processSyntaxCheckError(self, fn, msg):
5968 """
5969 Private slot to report an error message of a syntax check.
5970
5971 @param fn filename of the file
5972 @type str
5973 @param msg error message
5974 @type str
5975 """
5976 if fn != self.fileName and (
5977 bool(self.fileName) or fn != "(Unnamed)"):
5978 return
5979
5980 self.clearSyntaxError()
5981 self.clearFlakesWarnings()
5982
5983 self.toggleWarning(0, 0, True, msg)
5984
5985 self.updateVerticalScrollBar()
5986
5987 def __processSyntaxCheckResult(self, fn, problems):
5988 """
5989 Private slot to report the resulting messages of a syntax check.
5990
5991 @param fn filename of the checked file (str)
5992 @param problems dictionary with the keys 'error' and 'warnings' which
5993 hold a list containing details about the error/ warnings
5994 (file name, line number, column, codestring (only at syntax
5995 errors), the message) (dict)
5996 """
5997 # Check if it's the requested file, otherwise ignore signal
5998 if fn != self.fileName and (
5999 bool(self.fileName) or fn != "(Unnamed)"):
6000 return
6001
6002 self.clearSyntaxError()
6003 self.clearFlakesWarnings()
6004
6005 error = problems.get('error')
6006 if error:
6007 _fn, lineno, col, code, msg = error
6008 self.toggleSyntaxError(lineno, col, True, msg)
6009
6010 warnings = problems.get('warnings', [])
6011 for _fn, lineno, col, _code, msg in warnings:
6012 self.toggleWarning(lineno, col, True, msg)
6013
6014 self.updateVerticalScrollBar()
6015
6016 def __initOnlineSyntaxCheck(self):
6017 """
6018 Private slot to initialize the online syntax check.
6019 """
6020 self.__onlineSyntaxCheckTimer = QTimer(self)
6021 self.__onlineSyntaxCheckTimer.setSingleShot(True)
6022 self.__onlineSyntaxCheckTimer.setInterval(
6023 Preferences.getEditor("OnlineSyntaxCheckInterval") * 1000)
6024 self.__onlineSyntaxCheckTimer.timeout.connect(self.checkSyntax)
6025 self.textChanged.connect(self.__resetOnlineSyntaxCheckTimer)
6026
6027 def __resetOnlineSyntaxCheckTimer(self):
6028 """
6029 Private method to reset the online syntax check timer.
6030 """
6031 if Preferences.getEditor("OnlineSyntaxCheck"):
6032 self.__onlineSyntaxCheckTimer.stop()
6033 self.__onlineSyntaxCheckTimer.start()
6034
6035 def __showCodeMetrics(self):
6036 """
6037 Private method to handle the code metrics context menu action.
6038 """
6039 if not self.checkDirty():
6040 return
6041
6042 from DataViews.CodeMetricsDialog import CodeMetricsDialog
6043 self.codemetrics = CodeMetricsDialog()
6044 self.codemetrics.show()
6045 self.codemetrics.start(self.fileName)
6046
6047 def __getCodeCoverageFile(self):
6048 """
6049 Private method to get the file name of the file containing coverage
6050 info.
6051
6052 @return file name of the coverage file
6053 @rtype str
6054 """
6055 files = set()
6056
6057 if bool(self.__coverageFile):
6058 # return the path of a previously used coverage file
6059 return self.__coverageFile
6060
6061 # first check if the file belongs to a project and there is
6062 # a project coverage file
6063 if (
6064 self.project.isOpen() and
6065 self.project.isProjectSource(self.fileName)
6066 ):
6067 pfn = self.project.getMainScript(True)
6068 if pfn is not None:
6069 files |= set(Utilities.getCoverageFileNames(pfn))
6070
6071 # now check, if there are coverage files belonging to ourselves
6072 fn = self.getFileName()
6073 if fn is not None:
6074 files |= set(Utilities.getCoverageFileNames(fn))
6075
6076 files = list(files)
6077 if files:
6078 if len(files) > 1:
6079 cfn, ok = QInputDialog.getItem(
6080 self,
6081 self.tr("Code Coverage"),
6082 self.tr("Please select a coverage file"),
6083 files,
6084 0, False)
6085 if not ok:
6086 return ""
6087 else:
6088 cfn = files[0]
6089 else:
6090 cfn = None
6091
6092 return cfn
6093
6094 def __showCodeCoverage(self):
6095 """
6096 Private method to handle the code coverage context menu action.
6097 """
6098 fn = self.__getCodeCoverageFile()
6099 self.__coverageFile = fn
6100 if fn:
6101 from DataViews.PyCoverageDialog import PyCoverageDialog
6102 self.codecoverage = PyCoverageDialog()
6103 self.codecoverage.show()
6104 self.codecoverage.start(fn, self.fileName)
6105
6106 def refreshCoverageAnnotations(self):
6107 """
6108 Public method to refresh the code coverage annotations.
6109 """
6110 if self.showingNotcoveredMarkers:
6111 self.codeCoverageShowAnnotations(silent=True)
6112
6113 def codeCoverageShowAnnotations(self, silent=False, coverageFile=None):
6114 """
6115 Public method to handle the show code coverage annotations context
6116 menu action.
6117
6118 @param silent flag indicating to not show any dialog (defaults to
6119 False)
6120 @type bool (optional)
6121 @param coverageFile path of the file containing the code coverage data
6122 (defaults to None)
6123 @type str (optional)
6124 """
6125 self.__codeCoverageHideAnnotations()
6126
6127 fn = (
6128 coverageFile
6129 if bool(coverageFile) else
6130 self.__getCodeCoverageFile()
6131 )
6132 self.__coverageFile = fn
6133
6134 if fn:
6135 from coverage import Coverage
6136 cover = Coverage(data_file=fn)
6137 cover.load()
6138 missing = cover.analysis2(self.fileName)[3]
6139 if missing:
6140 for line in missing:
6141 handle = self.markerAdd(line - 1, self.notcovered)
6142 self.notcoveredMarkers.append(handle)
6143 self.coverageMarkersShown.emit(True)
6144 self.__markerMap.update()
6145 else:
6146 if not silent:
6147 EricMessageBox.information(
6148 self,
6149 self.tr("Show Code Coverage Annotations"),
6150 self.tr("""All lines have been covered."""))
6151 self.showingNotcoveredMarkers = True
6152 else:
6153 if not silent:
6154 EricMessageBox.warning(
6155 self,
6156 self.tr("Show Code Coverage Annotations"),
6157 self.tr("""There is no coverage file available."""))
6158
6159 def __codeCoverageHideAnnotations(self):
6160 """
6161 Private method to handle the hide code coverage annotations context
6162 menu action.
6163 """
6164 for handle in self.notcoveredMarkers:
6165 self.markerDeleteHandle(handle)
6166 self.notcoveredMarkers.clear()
6167 self.coverageMarkersShown.emit(False)
6168 self.showingNotcoveredMarkers = False
6169 self.__markerMap.update()
6170
6171 def getCoverageLines(self):
6172 """
6173 Public method to get the lines containing a coverage marker.
6174
6175 @return list of lines containing a coverage marker (list of integer)
6176 """
6177 lines = []
6178 line = -1
6179 while True:
6180 line = self.markerFindNext(line + 1, 1 << self.notcovered)
6181 if line < 0:
6182 break
6183 else:
6184 lines.append(line)
6185 return lines
6186
6187 def hasCoverageMarkers(self):
6188 """
6189 Public method to test, if there are coverage markers.
6190
6191 @return flag indicating the presence of coverage markers (boolean)
6192 """
6193 return len(self.notcoveredMarkers) > 0
6194
6195 def nextUncovered(self):
6196 """
6197 Public slot to handle the 'Next uncovered' context menu action.
6198 """
6199 line, index = self.getCursorPosition()
6200 if line == self.lines() - 1:
6201 line = 0
6202 else:
6203 line += 1
6204 ucline = self.markerFindNext(line, 1 << self.notcovered)
6205 if ucline < 0:
6206 # wrap around
6207 ucline = self.markerFindNext(0, 1 << self.notcovered)
6208 if ucline >= 0:
6209 self.setCursorPosition(ucline, 0)
6210 self.ensureLineVisible(ucline)
6211
6212 def previousUncovered(self):
6213 """
6214 Public slot to handle the 'Previous uncovered' context menu action.
6215 """
6216 line, index = self.getCursorPosition()
6217 if line == 0:
6218 line = self.lines() - 1
6219 else:
6220 line -= 1
6221 ucline = self.markerFindPrevious(line, 1 << self.notcovered)
6222 if ucline < 0:
6223 # wrap around
6224 ucline = self.markerFindPrevious(
6225 self.lines() - 1, 1 << self.notcovered)
6226 if ucline >= 0:
6227 self.setCursorPosition(ucline, 0)
6228 self.ensureLineVisible(ucline)
6229
6230 def __showProfileData(self):
6231 """
6232 Private method to handle the show profile data context menu action.
6233 """
6234 files = set()
6235
6236 # first check if the file belongs to a project and there is
6237 # a project profile file
6238 if (
6239 self.project.isOpen() and
6240 self.project.isProjectSource(self.fileName)
6241 ):
6242 fn = self.project.getMainScript(True)
6243 if fn is not None:
6244 files |= set(Utilities.getProfileFileNames(fn))
6245
6246 # now check, if there are profile files belonging to ourselves
6247 fn = self.getFileName()
6248 if fn is not None:
6249 files |= set(Utilities.getProfileFileNames(fn))
6250
6251 files = list(files)
6252 if files:
6253 if len(files) > 1:
6254 fn, ok = QInputDialog.getItem(
6255 self,
6256 self.tr("Profile Data"),
6257 self.tr("Please select a profile file"),
6258 files,
6259 0, False)
6260 if not ok:
6261 return
6262 else:
6263 fn = files[0]
6264 else:
6265 return
6266
6267 from DataViews.PyProfileDialog import PyProfileDialog
6268 self.profiledata = PyProfileDialog()
6269 self.profiledata.show()
6270 self.profiledata.start(fn, self.fileName)
6271
6272 def __lmBbookmarks(self):
6273 """
6274 Private method to handle the 'LMB toggles bookmark' context menu
6275 action.
6276 """
6277 self.marginMenuActs["LMBbookmarks"].setChecked(True)
6278 self.marginMenuActs["LMBbreakpoints"].setChecked(False)
6279
6280 def __lmBbreakpoints(self):
6281 """
6282 Private method to handle the 'LMB toggles breakpoint' context menu
6283 action.
6284 """
6285 self.marginMenuActs["LMBbookmarks"].setChecked(True)
6286 self.marginMenuActs["LMBbreakpoints"].setChecked(False)
6287
6288 ###########################################################################
6289 ## Syntax error handling methods below
6290 ###########################################################################
6291
6292 def toggleSyntaxError(self, line, index, error, msg="", show=False):
6293 """
6294 Public method to toggle a syntax error indicator.
6295
6296 @param line line number of the syntax error (integer)
6297 @param index index number of the syntax error (integer)
6298 @param error flag indicating if the error marker should be
6299 set or deleted (boolean)
6300 @param msg error message (string)
6301 @param show flag indicating to set the cursor to the error position
6302 (boolean)
6303 """
6304 if line == 0:
6305 line = 1
6306 # hack to show a syntax error marker, if line is reported to be 0
6307 if error:
6308 # set a new syntax error marker
6309 markers = self.markersAtLine(line - 1)
6310 index += self.indentation(line - 1)
6311 if not (markers & (1 << self.syntaxerror)):
6312 handle = self.markerAdd(line - 1, self.syntaxerror)
6313 self.syntaxerrors[handle] = [(msg, index)]
6314 self.syntaxerrorToggled.emit(self)
6315 else:
6316 for handle in list(self.syntaxerrors.keys()):
6317 if (
6318 self.markerLine(handle) == line - 1 and
6319 (msg, index) not in self.syntaxerrors[handle]
6320 ):
6321 self.syntaxerrors[handle].append((msg, index))
6322 if show:
6323 self.setCursorPosition(line - 1, index)
6324 self.ensureLineVisible(line - 1)
6325 else:
6326 for handle in list(self.syntaxerrors.keys()):
6327 if self.markerLine(handle) == line - 1:
6328 del self.syntaxerrors[handle]
6329 self.markerDeleteHandle(handle)
6330 self.syntaxerrorToggled.emit(self)
6331
6332 self.__setAnnotation(line - 1)
6333 self.__markerMap.update()
6334
6335 def getSyntaxErrors(self):
6336 """
6337 Public method to retrieve the syntax error markers.
6338
6339 @return sorted list of all lines containing a syntax error
6340 (list of integer)
6341 """
6342 selist = []
6343 for handle in list(self.syntaxerrors.keys()):
6344 selist.append(self.markerLine(handle) + 1)
6345
6346 selist.sort()
6347 return selist
6348
6349 def getSyntaxErrorLines(self):
6350 """
6351 Public method to get the lines containing a syntax error.
6352
6353 @return list of lines containing a syntax error (list of integer)
6354 """
6355 lines = []
6356 line = -1
6357 while True:
6358 line = self.markerFindNext(line + 1, 1 << self.syntaxerror)
6359 if line < 0:
6360 break
6361 else:
6362 lines.append(line)
6363 return lines
6364
6365 def hasSyntaxErrors(self):
6366 """
6367 Public method to check for the presence of syntax errors.
6368
6369 @return flag indicating the presence of syntax errors (boolean)
6370 """
6371 return len(self.syntaxerrors) > 0
6372
6373 def gotoSyntaxError(self):
6374 """
6375 Public slot to handle the 'Goto syntax error' context menu action.
6376 """
6377 seline = self.markerFindNext(0, 1 << self.syntaxerror)
6378 if seline >= 0:
6379 index = 0
6380 for handle in self.syntaxerrors:
6381 if self.markerLine(handle) == seline:
6382 index = self.syntaxerrors[handle][0][1]
6383 self.setCursorPosition(seline, index)
6384 self.ensureLineVisible(seline)
6385
6386 def clearSyntaxError(self):
6387 """
6388 Public slot to handle the 'Clear all syntax error' context menu action.
6389 """
6390 for handle in list(self.syntaxerrors.keys()):
6391 line = self.markerLine(handle) + 1
6392 self.toggleSyntaxError(line, 0, False)
6393
6394 self.syntaxerrors.clear()
6395 self.syntaxerrorToggled.emit(self)
6396
6397 def __showSyntaxError(self, line=-1):
6398 """
6399 Private slot to handle the 'Show syntax error message'
6400 context menu action.
6401
6402 @param line line number to show the syntax error for (integer)
6403 """
6404 if line == -1:
6405 line = self.line
6406
6407 for handle in list(self.syntaxerrors.keys()):
6408 if self.markerLine(handle) == line:
6409 errors = [e[0] for e in self.syntaxerrors[handle]]
6410 EricMessageBox.critical(
6411 self,
6412 self.tr("Syntax Error"),
6413 "\n".join(errors))
6414 break
6415 else:
6416 EricMessageBox.critical(
6417 self,
6418 self.tr("Syntax Error"),
6419 self.tr("No syntax error message available."))
6420
6421 ###########################################################################
6422 ## VCS conflict marker handling methods below
6423 ###########################################################################
6424
6425 def getVcsConflictMarkerLines(self):
6426 """
6427 Public method to determine the lines containing a VCS conflict marker.
6428
6429 @return list of line numbers containg a VCS conflict marker
6430 @rtype list of int
6431 """
6432 conflictMarkerLines = []
6433
6434 regExp = re.compile("|".join(Editor.VcsConflictMarkerLineRegExpList),
6435 re.MULTILINE)
6436 matches = [m for m in regExp.finditer(self.text())]
6437 for match in matches:
6438 line, _ = self.lineIndexFromPosition(match.start())
6439 conflictMarkerLines.append(line)
6440
6441 return conflictMarkerLines
6442
6443 ###########################################################################
6444 ## Warning handling methods below
6445 ###########################################################################
6446
6447 def toggleWarning(
6448 self, line, col, warning, msg="", warningType=WarningCode):
6449 """
6450 Public method to toggle a warning indicator.
6451
6452 Note: This method is used to set pyflakes and code style warnings.
6453
6454 @param line line number of the warning
6455 @param col column of the warning
6456 @param warning flag indicating if the warning marker should be
6457 set or deleted (boolean)
6458 @param msg warning message (string)
6459 @param warningType type of warning message (integer)
6460 """
6461 if line == 0:
6462 line = 1
6463 # hack to show a warning marker, if line is reported to be 0
6464 if warning:
6465 # set/amend a new warning marker
6466 warn = (msg, warningType)
6467 markers = self.markersAtLine(line - 1)
6468 if not (markers & (1 << self.warning)):
6469 handle = self.markerAdd(line - 1, self.warning)
6470 self.warnings[handle] = [warn]
6471 self.syntaxerrorToggled.emit(self)
6472 else:
6473 for handle in list(self.warnings.keys()):
6474 if (
6475 self.markerLine(handle) == line - 1 and
6476 warn not in self.warnings[handle]
6477 ):
6478 self.warnings[handle].append(warn)
6479 else:
6480 for handle in list(self.warnings.keys()):
6481 if self.markerLine(handle) == line - 1:
6482 del self.warnings[handle]
6483 self.markerDeleteHandle(handle)
6484 self.syntaxerrorToggled.emit(self)
6485
6486 self.__setAnnotation(line - 1)
6487 self.__markerMap.update()
6488
6489 def getWarnings(self):
6490 """
6491 Public method to retrieve the warning markers.
6492
6493 @return sorted list of all lines containing a warning
6494 (list of integer)
6495 """
6496 fwlist = []
6497 for handle in list(self.warnings.keys()):
6498 fwlist.append(self.markerLine(handle) + 1)
6499
6500 fwlist.sort()
6501 return fwlist
6502
6503 def getWarningLines(self):
6504 """
6505 Public method to get the lines containing a warning.
6506
6507 @return list of lines containing a warning (list of integer)
6508 """
6509 lines = []
6510 line = -1
6511 while True:
6512 line = self.markerFindNext(line + 1, 1 << self.warning)
6513 if line < 0:
6514 break
6515 else:
6516 lines.append(line)
6517 return lines
6518
6519 def hasWarnings(self):
6520 """
6521 Public method to check for the presence of warnings.
6522
6523 @return flag indicating the presence of warnings (boolean)
6524 """
6525 return len(self.warnings) > 0
6526
6527 def nextWarning(self):
6528 """
6529 Public slot to handle the 'Next warning' context menu action.
6530 """
6531 line, index = self.getCursorPosition()
6532 if line == self.lines() - 1:
6533 line = 0
6534 else:
6535 line += 1
6536 fwline = self.markerFindNext(line, 1 << self.warning)
6537 if fwline < 0:
6538 # wrap around
6539 fwline = self.markerFindNext(0, 1 << self.warning)
6540 if fwline >= 0:
6541 self.setCursorPosition(fwline, 0)
6542 self.ensureLineVisible(fwline)
6543
6544 def previousWarning(self):
6545 """
6546 Public slot to handle the 'Previous warning' context menu action.
6547 """
6548 line, index = self.getCursorPosition()
6549 if line == 0:
6550 line = self.lines() - 1
6551 else:
6552 line -= 1
6553 fwline = self.markerFindPrevious(line, 1 << self.warning)
6554 if fwline < 0:
6555 # wrap around
6556 fwline = self.markerFindPrevious(
6557 self.lines() - 1, 1 << self.warning)
6558 if fwline >= 0:
6559 self.setCursorPosition(fwline, 0)
6560 self.ensureLineVisible(fwline)
6561
6562 def clearFlakesWarnings(self):
6563 """
6564 Public slot to clear all pyflakes warnings.
6565 """
6566 self.__clearTypedWarning(Editor.WarningCode)
6567
6568 def clearStyleWarnings(self):
6569 """
6570 Public slot to clear all style warnings.
6571 """
6572 self.__clearTypedWarning(Editor.WarningStyle)
6573
6574 def __clearTypedWarning(self, warningKind):
6575 """
6576 Private method to clear warnings of a specific kind.
6577
6578 @param warningKind kind of warning to clear (Editor.WarningCode,
6579 Editor.WarningStyle)
6580 """
6581 for handle in list(self.warnings.keys()):
6582 warnings = []
6583 for msg, warningType in self.warnings[handle]:
6584 if warningType == warningKind:
6585 continue
6586
6587 warnings.append((msg, warningType))
6588
6589 if warnings:
6590 self.warnings[handle] = warnings
6591 self.__setAnnotation(self.markerLine(handle))
6592 else:
6593 del self.warnings[handle]
6594 self.__setAnnotation(self.markerLine(handle))
6595 self.markerDeleteHandle(handle)
6596 self.syntaxerrorToggled.emit(self)
6597 self.__markerMap.update()
6598
6599 def clearWarnings(self):
6600 """
6601 Public slot to clear all warnings.
6602 """
6603 for handle in self.warnings:
6604 self.warnings[handle] = []
6605 self.__setAnnotation(self.markerLine(handle))
6606 self.markerDeleteHandle(handle)
6607 self.warnings.clear()
6608 self.syntaxerrorToggled.emit(self)
6609 self.__markerMap.update()
6610
6611 def __showWarning(self, line=-1):
6612 """
6613 Private slot to handle the 'Show warning' context menu action.
6614
6615 @param line line number to show the warning for (integer)
6616 """
6617 if line == -1:
6618 line = self.line
6619
6620 for handle in list(self.warnings.keys()):
6621 if self.markerLine(handle) == line:
6622 EricMessageBox.warning(
6623 self,
6624 self.tr("Warning"),
6625 '\n'.join([w[0] for w in self.warnings[handle]]))
6626 break
6627 else:
6628 EricMessageBox.warning(
6629 self,
6630 self.tr("Warning"),
6631 self.tr("No warning messages available."))
6632
6633 ###########################################################################
6634 ## Annotation handling methods below
6635 ###########################################################################
6636
6637 def __setAnnotationStyles(self):
6638 """
6639 Private slot to define the style used by inline annotations.
6640 """
6641 if hasattr(QsciScintilla, "annotate"):
6642 self.annotationWarningStyle = (
6643 QsciScintilla.STYLE_LASTPREDEFINED + 1
6644 )
6645 self.SendScintilla(
6646 QsciScintilla.SCI_STYLESETFORE,
6647 self.annotationWarningStyle,
6648 Preferences.getEditorColour("AnnotationsWarningForeground"))
6649 self.SendScintilla(
6650 QsciScintilla.SCI_STYLESETBACK,
6651 self.annotationWarningStyle,
6652 Preferences.getEditorColour("AnnotationsWarningBackground"))
6653
6654 self.annotationErrorStyle = self.annotationWarningStyle + 1
6655 self.SendScintilla(
6656 QsciScintilla.SCI_STYLESETFORE,
6657 self.annotationErrorStyle,
6658 Preferences.getEditorColour("AnnotationsErrorForeground"))
6659 self.SendScintilla(
6660 QsciScintilla.SCI_STYLESETBACK,
6661 self.annotationErrorStyle,
6662 Preferences.getEditorColour("AnnotationsErrorBackground"))
6663
6664 self.annotationStyleStyle = self.annotationErrorStyle + 1
6665 self.SendScintilla(
6666 QsciScintilla.SCI_STYLESETFORE,
6667 self.annotationStyleStyle,
6668 Preferences.getEditorColour("AnnotationsStyleForeground"))
6669 self.SendScintilla(
6670 QsciScintilla.SCI_STYLESETBACK,
6671 self.annotationStyleStyle,
6672 Preferences.getEditorColour("AnnotationsStyleBackground"))
6673
6674 def __setAnnotation(self, line):
6675 """
6676 Private method to set the annotations for the given line.
6677
6678 @param line number of the line that needs annotation (integer)
6679 """
6680 if hasattr(QsciScintilla, "annotate"):
6681 warningAnnotations = []
6682 errorAnnotations = []
6683 styleAnnotations = []
6684
6685 # step 1: do warnings
6686 for handle in self.warnings:
6687 if self.markerLine(handle) == line:
6688 for msg, warningType in self.warnings[handle]:
6689 if warningType == self.WarningStyle:
6690 styleAnnotations.append(
6691 self.tr("Style: {0}").format(msg))
6692 else:
6693 warningAnnotations.append(
6694 self.tr("Warning: {0}").format(msg))
6695
6696 # step 2: do syntax errors
6697 for handle in self.syntaxerrors:
6698 if self.markerLine(handle) == line:
6699 for msg, _ in self.syntaxerrors[handle]:
6700 errorAnnotations.append(
6701 self.tr("Error: {0}").format(msg))
6702
6703 annotations = []
6704 if styleAnnotations:
6705 annotationStyleTxt = "\n".join(styleAnnotations)
6706 if warningAnnotations or errorAnnotations:
6707 annotationStyleTxt += "\n"
6708 annotations.append(QsciStyledText(
6709 annotationStyleTxt, self.annotationStyleStyle))
6710
6711 if warningAnnotations:
6712 annotationWarningTxt = "\n".join(warningAnnotations)
6713 if errorAnnotations:
6714 annotationWarningTxt += "\n"
6715 annotations.append(QsciStyledText(
6716 annotationWarningTxt, self.annotationWarningStyle))
6717
6718 if errorAnnotations:
6719 annotationErrorTxt = "\n".join(errorAnnotations)
6720 annotations.append(QsciStyledText(
6721 annotationErrorTxt, self.annotationErrorStyle))
6722
6723 if annotations:
6724 self.annotate(line, annotations)
6725 else:
6726 self.clearAnnotations(line)
6727
6728 def __refreshAnnotations(self):
6729 """
6730 Private method to refresh the annotations.
6731 """
6732 if hasattr(QsciScintilla, "annotate"):
6733 self.clearAnnotations()
6734 for handle in (
6735 list(self.warnings.keys()) +
6736 list(self.syntaxerrors.keys())
6737 ):
6738 line = self.markerLine(handle)
6739 self.__setAnnotation(line)
6740
6741 #################################################################
6742 ## Fold handling methods
6743 #################################################################
6744
6745 def toggleCurrentFold(self):
6746 """
6747 Public slot to toggle the fold containing the current line.
6748 """
6749 line, index = self.getCursorPosition()
6750 self.foldLine(line)
6751
6752 def expandFoldWithChildren(self, line=-1):
6753 """
6754 Public slot to expand the current fold including its children.
6755
6756 @param line number of line to be expanded
6757 @type int
6758 """
6759 if line == -1:
6760 line, index = self.getCursorPosition()
6761
6762 self.SendScintilla(QsciScintilla.SCI_FOLDCHILDREN, line,
6763 QsciScintilla.SC_FOLDACTION_EXPAND)
6764
6765 def collapseFoldWithChildren(self, line=-1):
6766 """
6767 Public slot to collapse the current fold including its children.
6768
6769 @param line number of line to be expanded
6770 @type int
6771 """
6772 if line == -1:
6773 line, index = self.getCursorPosition()
6774
6775 self.SendScintilla(QsciScintilla.SCI_FOLDCHILDREN, line,
6776 QsciScintilla.SC_FOLDACTION_CONTRACT)
6777
6778 def __contextMenuExpandFoldWithChildren(self):
6779 """
6780 Private slot to handle the context menu expand with children action.
6781 """
6782 self.expandFoldWithChildren(self.line)
6783
6784 def __contextMenuCollapseFoldWithChildren(self):
6785 """
6786 Private slot to handle the context menu collapse with children action.
6787 """
6788 self.collapseFoldWithChildren(self.line)
6789
6790 #################################################################
6791 ## Macro handling methods
6792 #################################################################
6793
6794 def __getMacroName(self):
6795 """
6796 Private method to select a macro name from the list of macros.
6797
6798 @return Tuple of macro name and a flag, indicating, if the user
6799 pressed ok or canceled the operation. (string, boolean)
6800 """
6801 qs = []
6802 for s in list(self.macros.keys()):
6803 qs.append(s)
6804 qs.sort()
6805 return QInputDialog.getItem(
6806 self,
6807 self.tr("Macro Name"),
6808 self.tr("Select a macro name:"),
6809 qs,
6810 0, False)
6811
6812 def macroRun(self):
6813 """
6814 Public method to execute a macro.
6815 """
6816 name, ok = self.__getMacroName()
6817 if ok and name:
6818 self.macros[name].play()
6819
6820 def macroDelete(self):
6821 """
6822 Public method to delete a macro.
6823 """
6824 name, ok = self.__getMacroName()
6825 if ok and name:
6826 del self.macros[name]
6827
6828 def macroLoad(self):
6829 """
6830 Public method to load a macro from a file.
6831 """
6832 configDir = Utilities.getConfigDir()
6833 fname = EricFileDialog.getOpenFileName(
6834 self,
6835 self.tr("Load macro file"),
6836 configDir,
6837 self.tr("Macro files (*.macro)"))
6838
6839 if not fname:
6840 return # user aborted
6841
6842 try:
6843 with open(fname, "r", encoding="utf-8") as f:
6844 lines = f.readlines()
6845 except OSError:
6846 EricMessageBox.critical(
6847 self,
6848 self.tr("Error loading macro"),
6849 self.tr(
6850 "<p>The macro file <b>{0}</b> could not be read.</p>")
6851 .format(fname))
6852 return
6853
6854 if len(lines) != 2:
6855 EricMessageBox.critical(
6856 self,
6857 self.tr("Error loading macro"),
6858 self.tr("<p>The macro file <b>{0}</b> is corrupt.</p>")
6859 .format(fname))
6860 return
6861
6862 macro = QsciMacro(lines[1], self)
6863 self.macros[lines[0].strip()] = macro
6864
6865 def macroSave(self):
6866 """
6867 Public method to save a macro to a file.
6868 """
6869 configDir = Utilities.getConfigDir()
6870
6871 name, ok = self.__getMacroName()
6872 if not ok or not name:
6873 return # user abort
6874
6875 fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
6876 self,
6877 self.tr("Save macro file"),
6878 configDir,
6879 self.tr("Macro files (*.macro)"),
6880 "",
6881 EricFileDialog.DontConfirmOverwrite)
6882
6883 if not fname:
6884 return # user aborted
6885
6886 fpath = pathlib.Path(fname)
6887 if not fpath.suffix:
6888 ex = selectedFilter.split("(*")[1].split(")")[0]
6889 if ex:
6890 fpath = fpath.with_suffix(ex)
6891 if fpath.exists():
6892 res = EricMessageBox.yesNo(
6893 self,
6894 self.tr("Save macro"),
6895 self.tr("<p>The macro file <b>{0}</b> already exists."
6896 " Overwrite it?</p>").format(fpath),
6897 icon=EricMessageBox.Warning)
6898 if not res:
6899 return
6900
6901 try:
6902 with fpath.open("w", encoding="utf-8") as f:
6903 f.write("{0}{1}".format(name, "\n"))
6904 f.write(self.macros[name].save())
6905 except OSError:
6906 EricMessageBox.critical(
6907 self,
6908 self.tr("Error saving macro"),
6909 self.tr(
6910 "<p>The macro file <b>{0}</b> could not be written.</p>")
6911 .format(fpath))
6912 return
6913
6914 def macroRecordingStart(self):
6915 """
6916 Public method to start macro recording.
6917 """
6918 if self.recording:
6919 res = EricMessageBox.yesNo(
6920 self,
6921 self.tr("Start Macro Recording"),
6922 self.tr("Macro recording is already active. Start new?"),
6923 icon=EricMessageBox.Warning,
6924 yesDefault=True)
6925 if res:
6926 self.macroRecordingStop()
6927 else:
6928 return
6929 else:
6930 self.recording = True
6931
6932 self.curMacro = QsciMacro(self)
6933 self.curMacro.startRecording()
6934
6935 def macroRecordingStop(self):
6936 """
6937 Public method to stop macro recording.
6938 """
6939 if not self.recording:
6940 return # we are not recording
6941
6942 self.curMacro.endRecording()
6943 self.recording = False
6944
6945 name, ok = QInputDialog.getText(
6946 self,
6947 self.tr("Macro Recording"),
6948 self.tr("Enter name of the macro:"),
6949 QLineEdit.EchoMode.Normal)
6950
6951 if ok and name:
6952 self.macros[name] = self.curMacro
6953
6954 self.curMacro = None
6955
6956 #################################################################
6957 ## Overwritten methods
6958 #################################################################
6959
6960 def undo(self):
6961 """
6962 Public method to undo the last recorded change.
6963 """
6964 super().undo()
6965 self.undoAvailable.emit(self.isUndoAvailable())
6966 self.redoAvailable.emit(self.isRedoAvailable())
6967
6968 def redo(self):
6969 """
6970 Public method to redo the last recorded change.
6971 """
6972 super().redo()
6973 self.undoAvailable.emit(self.isUndoAvailable())
6974 self.redoAvailable.emit(self.isRedoAvailable())
6975
6976 def close(self, alsoDelete=False):
6977 """
6978 Public method called when the window gets closed.
6979
6980 This overwritten method redirects the action to our
6981 ViewManager.closeEditor, which in turn calls our closeIt
6982 method.
6983
6984 @param alsoDelete ignored
6985 @return flag indicating a successful close of the editor (boolean)
6986 """
6987 return self.vm.closeEditor(self)
6988
6989 def closeIt(self):
6990 """
6991 Public method called by the viewmanager to finally get rid of us.
6992 """
6993 if Preferences.getEditor("ClearBreaksOnClose") and not self.__clones:
6994 self.__menuClearBreakpoints()
6995
6996 for clone in self.__clones[:]:
6997 self.removeClone(clone)
6998 clone.removeClone(self)
6999
7000 self.breakpointModel.rowsAboutToBeRemoved.disconnect(
7001 self.__deleteBreakPoints)
7002 self.breakpointModel.dataAboutToBeChanged.disconnect(
7003 self.__breakPointDataAboutToBeChanged)
7004 self.breakpointModel.dataChanged.disconnect(
7005 self.__changeBreakPoints)
7006 self.breakpointModel.rowsInserted.disconnect(
7007 self.__addBreakPoints)
7008
7009 if self.syntaxCheckService is not None:
7010 self.syntaxCheckService.syntaxChecked.disconnect(
7011 self.__processSyntaxCheckResult)
7012 self.syntaxCheckService.error.disconnect(
7013 self.__processSyntaxCheckError)
7014
7015 if self.spell:
7016 self.spell.stopIncrementalCheck()
7017
7018 with contextlib.suppress(TypeError):
7019 self.project.projectPropertiesChanged.disconnect(
7020 self.__projectPropertiesChanged)
7021
7022 if self.fileName:
7023 self.taskViewer.clearFileTasks(self.fileName, True)
7024
7025 super().close()
7026
7027 def keyPressEvent(self, ev):
7028 """
7029 Protected method to handle the user input a key at a time.
7030
7031 @param ev key event
7032 @type QKeyEvent
7033 """
7034 def encloseSelectedText(encString):
7035 """
7036 Local function to enclose the current selection with some
7037 characters.
7038
7039 @param encString string to use to enclose the selection
7040 (one or two characters)
7041 @type str
7042 """
7043 startChar = encString[0]
7044 endChar = encString[1] if len(encString) == 2 else startChar
7045
7046 sline, sindex, eline, eindex = self.getSelection()
7047 replaceText = startChar + self.selectedText() + endChar
7048 self.beginUndoAction()
7049 self.replaceSelectedText(replaceText)
7050 self.endUndoAction()
7051 self.setSelection(sline, sindex + 1, eline, eindex + 1)
7052
7053 txt = ev.text()
7054
7055 # See it is text to insert.
7056 if len(txt) and txt >= " ":
7057 if (
7058 self.hasSelectedText() and
7059 txt in Editor.EncloseChars
7060 ):
7061 encloseSelectedText(Editor.EncloseChars[txt])
7062 ev.accept()
7063 return
7064
7065 super().keyPressEvent(ev)
7066 else:
7067 ev.ignore()
7068
7069 def focusInEvent(self, event):
7070 """
7071 Protected method called when the editor receives focus.
7072
7073 This method checks for modifications of the current file and
7074 rereads it upon request. The cursor is placed at the current position
7075 assuming, that it is in the vicinity of the old position after the
7076 reread.
7077
7078 @param event the event object
7079 @type QFocusEvent
7080 """
7081 self.recolor()
7082 self.vm.editActGrp.setEnabled(True)
7083 self.vm.editorActGrp.setEnabled(True)
7084 self.vm.copyActGrp.setEnabled(True)
7085 self.vm.viewActGrp.setEnabled(True)
7086 self.vm.searchActGrp.setEnabled(True)
7087 with contextlib.suppress(AttributeError):
7088 self.setCaretWidth(self.caretWidth)
7089 self.__updateReadOnly(False)
7090 if (
7091 self.vm.editorsCheckFocusInEnabled() and
7092 not self.inReopenPrompt and self.fileName and
7093 pathlib.Path(self.fileName).exists() and
7094 pathlib.Path(self.fileName).stat().st_mtime != self.lastModified
7095 ):
7096 self.inReopenPrompt = True
7097 if Preferences.getEditor("AutoReopen") and not self.isModified():
7098 self.refresh()
7099 else:
7100 msg = self.tr(
7101 """<p>The file <b>{0}</b> has been changed while it"""
7102 """ was opened in eric. Reread it?</p>"""
7103 ).format(self.fileName)
7104 yesDefault = True
7105 if self.isModified():
7106 msg += self.tr(
7107 """<br><b>Warning:</b> You will lose"""
7108 """ your changes upon reopening it.""")
7109 yesDefault = False
7110 res = EricMessageBox.yesNo(
7111 self,
7112 self.tr("File changed"), msg,
7113 icon=EricMessageBox.Warning,
7114 yesDefault=yesDefault)
7115 if res:
7116 self.refresh()
7117 else:
7118 # do not prompt for this change again...
7119 self.lastModified = (
7120 pathlib.Path(self.fileName).stat().st_mtime
7121 )
7122 self.inReopenPrompt = False
7123
7124 self.setCursorFlashTime(QApplication.cursorFlashTime())
7125
7126 super().focusInEvent(event)
7127
7128 def focusOutEvent(self, event):
7129 """
7130 Protected method called when the editor loses focus.
7131
7132 @param event the event object
7133 @type QFocusEvent
7134 """
7135 self.vm.editorActGrp.setEnabled(False)
7136 self.setCaretWidth(0)
7137
7138 self.cancelCallTips()
7139
7140 super().focusOutEvent(event)
7141
7142 def changeEvent(self, evt):
7143 """
7144 Protected method called to process an event.
7145
7146 This implements special handling for the events showMaximized,
7147 showMinimized and showNormal. The windows caption is shortened
7148 for the minimized mode and reset to the full filename for the
7149 other modes. This is to make the editor windows work nicer
7150 with the QWorkspace.
7151
7152 @param evt the event, that was generated
7153 @type QEvent
7154 """
7155 if (
7156 evt.type() == QEvent.Type.WindowStateChange and
7157 bool(self.fileName)
7158 ):
7159 cap = (
7160 os.path.basename(self.fileName)
7161 if self.windowState() == Qt.WindowState.WindowMinimized else
7162 self.fileName
7163 )
7164 if self.isReadOnly():
7165 cap = self.tr("{0} (ro)").format(cap)
7166 self.setWindowTitle(cap)
7167
7168 super().changeEvent(evt)
7169
7170 def mousePressEvent(self, event):
7171 """
7172 Protected method to handle the mouse press event.
7173
7174 @param event the mouse press event
7175 @type QMouseEvent
7176 """
7177 if event.button() == Qt.MouseButton.XButton1:
7178 self.undo()
7179 event.accept()
7180 elif event.button() == Qt.MouseButton.XButton2:
7181 self.redo()
7182 event.accept()
7183 elif (
7184 event.button() == Qt.MouseButton.LeftButton and
7185 bool(event.modifiers() & (
7186 Qt.KeyboardModifier.MetaModifier |
7187 Qt.KeyboardModifier.AltModifier
7188 ))
7189 ):
7190 line, index = self.lineIndexFromPoint(event.position().toPoint())
7191 self.addCursor(line, index)
7192 event.accept()
7193 else:
7194 self.vm.eventFilter(self, event)
7195 super().mousePressEvent(event)
7196
7197 def mouseDoubleClickEvent(self, evt):
7198 """
7199 Protected method to handle mouse double click events.
7200
7201 @param evt reference to the mouse event
7202 @type QMouseEvent
7203 """
7204 super().mouseDoubleClickEvent(evt)
7205
7206 self.mouseDoubleClick.emit(evt.position().toPoint(), evt.buttons())
7207
7208 def wheelEvent(self, evt):
7209 """
7210 Protected method to handle wheel events.
7211
7212 @param evt reference to the wheel event
7213 @type QWheelEvent
7214 """
7215 delta = evt.angleDelta().y()
7216 if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
7217 if delta < 0:
7218 self.zoomOut()
7219 elif delta > 0:
7220 self.zoomIn()
7221 evt.accept()
7222 return
7223
7224 if evt.modifiers() & Qt.KeyboardModifier.ShiftModifier:
7225 if delta < 0:
7226 self.gotoMethodClass(False)
7227 elif delta > 0:
7228 self.gotoMethodClass(True)
7229 evt.accept()
7230 return
7231
7232 super().wheelEvent(evt)
7233
7234 def event(self, evt):
7235 """
7236 Public method handling events.
7237
7238 @param evt reference to the event
7239 @type QEvent
7240 @return flag indicating, if the event was handled
7241 @rtype bool
7242 """
7243 if evt.type() == QEvent.Type.Gesture:
7244 self.gestureEvent(evt)
7245 return True
7246
7247 return super().event(evt)
7248
7249 def gestureEvent(self, evt):
7250 """
7251 Protected method handling gesture events.
7252
7253 @param evt reference to the gesture event
7254 @type QGestureEvent
7255 """
7256 pinch = evt.gesture(Qt.GestureType.PinchGesture)
7257 if pinch:
7258 if pinch.state() == Qt.GestureState.GestureStarted:
7259 zoom = (self.getZoom() + 10) / 10.0
7260 pinch.setTotalScaleFactor(zoom)
7261 elif pinch.state() == Qt.GestureState.GestureUpdated:
7262 zoom = int(pinch.totalScaleFactor() * 10) - 10
7263 if zoom <= -9:
7264 zoom = -9
7265 pinch.setTotalScaleFactor(0.1)
7266 elif zoom >= 20:
7267 zoom = 20
7268 pinch.setTotalScaleFactor(3.0)
7269 self.zoomTo(zoom)
7270 evt.accept()
7271
7272 def resizeEvent(self, evt):
7273 """
7274 Protected method handling resize events.
7275
7276 @param evt reference to the resize event
7277 @type QResizeEvent
7278 """
7279 super().resizeEvent(evt)
7280 self.__markerMap.calculateGeometry()
7281
7282 def viewportEvent(self, evt):
7283 """
7284 Protected method handling event of the viewport.
7285
7286 @param evt reference to the event
7287 @type QEvent
7288 @return flag indiating that the event was handled
7289 @rtype bool
7290 """
7291 with contextlib.suppress(AttributeError):
7292 self.__markerMap.calculateGeometry()
7293 return super().viewportEvent(evt)
7294
7295 def __updateReadOnly(self, bForce=True):
7296 """
7297 Private method to update the readOnly information for this editor.
7298
7299 If bForce is True, then updates everything regardless if
7300 the attributes have actually changed, such as during
7301 initialization time. A signal is emitted after the
7302 caption change.
7303
7304 @param bForce True to force change, False to only update and emit
7305 signal if there was an attribute change.
7306 """
7307 if self.fileName == "":
7308 return
7309
7310 readOnly = (
7311 not os.access(self.fileName, os.W_OK) or
7312 self.isReadOnly()
7313 )
7314 if not bForce and (readOnly == self.isReadOnly()):
7315 return
7316
7317 cap = self.fileName
7318 if readOnly:
7319 cap = self.tr("{0} (ro)".format(cap))
7320 self.setReadOnly(readOnly)
7321 self.setWindowTitle(cap)
7322 self.captionChanged.emit(cap, self)
7323
7324 def refresh(self):
7325 """
7326 Public slot to refresh the editor contents.
7327 """
7328 # save cursor position
7329 cline, cindex = self.getCursorPosition()
7330
7331 # save bookmarks and breakpoints and clear them
7332 bmlist = self.getBookmarks()
7333 self.clearBookmarks()
7334
7335 # clear syntax error markers
7336 self.clearSyntaxError()
7337
7338 # clear flakes warning markers
7339 self.clearWarnings()
7340
7341 # clear breakpoint markers
7342 for handle in list(self.breaks.keys()):
7343 self.markerDeleteHandle(handle)
7344 self.breaks.clear()
7345
7346 if not os.path.exists(self.fileName):
7347 # close the file, if it was deleted in the background
7348 self.close()
7349 return
7350
7351 # reread the file
7352 try:
7353 self.readFile(self.fileName)
7354 except OSError:
7355 # do not prompt for this change again...
7356 self.lastModified = QDateTime.currentDateTime()
7357 self.setModified(False)
7358 self.__convertTabs()
7359
7360 # re-initialize the online change tracer
7361 self.__reinitOnlineChangeTrace()
7362
7363 # reset cursor position
7364 self.setCursorPosition(cline, cindex)
7365 self.ensureCursorVisible()
7366
7367 # reset bookmarks and breakpoints to their old position
7368 if bmlist:
7369 for bm in bmlist:
7370 self.toggleBookmark(bm)
7371 self.__restoreBreakpoints()
7372
7373 self.editorSaved.emit(self.fileName)
7374 self.checkSyntax()
7375
7376 self.__markerMap.update()
7377
7378 self.refreshed.emit()
7379
7380 def setMonospaced(self, on):
7381 """
7382 Public method to set/reset a monospaced font.
7383
7384 @param on flag to indicate usage of a monospace font (boolean)
7385 """
7386 if on:
7387 if not self.lexer_:
7388 f = Preferences.getEditorOtherFonts("MonospacedFont")
7389 self.monospacedStyles(f)
7390 else:
7391 if not self.lexer_:
7392 self.clearStyles()
7393 self.__setMarginsDisplay()
7394 self.setFont(Preferences.getEditorOtherFonts("DefaultFont"))
7395
7396 self.useMonospaced = on
7397
7398 def clearStyles(self):
7399 """
7400 Public method to set the styles according the selected Qt style
7401 or the selected editor colours.
7402 """
7403 super().clearStyles()
7404 if Preferences.getEditor("OverrideEditAreaColours"):
7405 self.setColor(Preferences.getEditorColour("EditAreaForeground"))
7406 self.setPaper(Preferences.getEditorColour("EditAreaBackground"))
7407
7408 #################################################################
7409 ## Drag and Drop Support
7410 #################################################################
7411
7412 def dragEnterEvent(self, event):
7413 """
7414 Protected method to handle the drag enter event.
7415
7416 @param event the drag enter event (QDragEnterEvent)
7417 """
7418 self.inDragDrop = event.mimeData().hasUrls()
7419 if self.inDragDrop:
7420 event.acceptProposedAction()
7421 else:
7422 super().dragEnterEvent(event)
7423
7424 def dragMoveEvent(self, event):
7425 """
7426 Protected method to handle the drag move event.
7427
7428 @param event the drag move event (QDragMoveEvent)
7429 """
7430 if self.inDragDrop:
7431 event.accept()
7432 else:
7433 super().dragMoveEvent(event)
7434
7435 def dragLeaveEvent(self, event):
7436 """
7437 Protected method to handle the drag leave event.
7438
7439 @param event the drag leave event (QDragLeaveEvent)
7440 """
7441 if self.inDragDrop:
7442 self.inDragDrop = False
7443 event.accept()
7444 else:
7445 super().dragLeaveEvent(event)
7446
7447 def dropEvent(self, event):
7448 """
7449 Protected method to handle the drop event.
7450
7451 @param event the drop event (QDropEvent)
7452 """
7453 if event.mimeData().hasUrls():
7454 for url in event.mimeData().urls():
7455 fname = url.toLocalFile()
7456 if fname:
7457 if not pathlib.Path(fname).is_dir():
7458 self.vm.openSourceFile(fname)
7459 else:
7460 EricMessageBox.information(
7461 self,
7462 self.tr("Drop Error"),
7463 self.tr("""<p><b>{0}</b> is not a file.</p>""")
7464 .format(fname))
7465 event.acceptProposedAction()
7466 else:
7467 super().dropEvent(event)
7468
7469 self.inDragDrop = False
7470
7471 #################################################################
7472 ## Support for Qt resources files
7473 #################################################################
7474
7475 def __initContextMenuResources(self):
7476 """
7477 Private method used to setup the Resources context sub menu.
7478
7479 @return reference to the generated menu (QMenu)
7480 """
7481 menu = QMenu(self.tr('Resources'))
7482
7483 menu.addAction(
7484 self.tr('Add file...'), self.__addFileResource)
7485 menu.addAction(
7486 self.tr('Add files...'), self.__addFileResources)
7487 menu.addAction(
7488 self.tr('Add aliased file...'),
7489 self.__addFileAliasResource)
7490 menu.addAction(
7491 self.tr('Add localized resource...'),
7492 self.__addLocalizedResource)
7493 menu.addSeparator()
7494 menu.addAction(
7495 self.tr('Add resource frame'), self.__addResourceFrame)
7496
7497 menu.aboutToShow.connect(self.__showContextMenuResources)
7498
7499 return menu
7500
7501 def __showContextMenuResources(self):
7502 """
7503 Private slot handling the aboutToShow signal of the resources context
7504 menu.
7505 """
7506 self.showMenu.emit("Resources", self.resourcesMenu, self)
7507
7508 def __addFileResource(self):
7509 """
7510 Private method to handle the Add file context menu action.
7511 """
7512 dirStr = os.path.dirname(self.fileName)
7513 file = EricFileDialog.getOpenFileName(
7514 self,
7515 self.tr("Add file resource"),
7516 dirStr,
7517 "")
7518 if file:
7519 relFile = QDir(dirStr).relativeFilePath(file)
7520 line, index = self.getCursorPosition()
7521 self.insert(" <file>{0}</file>\n".format(relFile))
7522 self.setCursorPosition(line + 1, index)
7523
7524 def __addFileResources(self):
7525 """
7526 Private method to handle the Add files context menu action.
7527 """
7528 dirStr = os.path.dirname(self.fileName)
7529 files = EricFileDialog.getOpenFileNames(
7530 self,
7531 self.tr("Add file resources"),
7532 dirStr,
7533 "")
7534 if files:
7535 myDir = QDir(dirStr)
7536 filesText = ""
7537 for file in files:
7538 relFile = myDir.relativeFilePath(file)
7539 filesText += " <file>{0}</file>\n".format(relFile)
7540 line, index = self.getCursorPosition()
7541 self.insert(filesText)
7542 self.setCursorPosition(line + len(files), index)
7543
7544 def __addFileAliasResource(self):
7545 """
7546 Private method to handle the Add aliased file context menu action.
7547 """
7548 dirStr = os.path.dirname(self.fileName)
7549 file = EricFileDialog.getOpenFileName(
7550 self,
7551 self.tr("Add aliased file resource"),
7552 dirStr,
7553 "")
7554 if file:
7555 relFile = QDir(dirStr).relativeFilePath(file)
7556 alias, ok = QInputDialog.getText(
7557 self,
7558 self.tr("Add aliased file resource"),
7559 self.tr("Alias for file <b>{0}</b>:").format(relFile),
7560 QLineEdit.EchoMode.Normal,
7561 relFile)
7562 if ok and alias:
7563 line, index = self.getCursorPosition()
7564 self.insert(' <file alias="{1}">{0}</file>\n'
7565 .format(relFile, alias))
7566 self.setCursorPosition(line + 1, index)
7567
7568 def __addLocalizedResource(self):
7569 """
7570 Private method to handle the Add localized resource context menu
7571 action.
7572 """
7573 from Project.AddLanguageDialog import AddLanguageDialog
7574 dlg = AddLanguageDialog(self)
7575 if dlg.exec() == QDialog.DialogCode.Accepted:
7576 lang = dlg.getSelectedLanguage()
7577 line, index = self.getCursorPosition()
7578 self.insert('<qresource lang="{0}">\n</qresource>\n'.format(lang))
7579 self.setCursorPosition(line + 2, index)
7580
7581 def __addResourceFrame(self):
7582 """
7583 Private method to handle the Add resource frame context menu action.
7584 """
7585 line, index = self.getCursorPosition()
7586 self.insert('<!DOCTYPE RCC>\n'
7587 '<RCC version="1.0">\n'
7588 '<qresource>\n'
7589 '</qresource>\n'
7590 '</RCC>\n')
7591 self.setCursorPosition(line + 5, index)
7592
7593 #################################################################
7594 ## Support for diagrams below
7595 #################################################################
7596
7597 def __showClassDiagram(self):
7598 """
7599 Private method to handle the Class Diagram context menu action.
7600 """
7601 from Graphics.UMLDialog import UMLDialog, UMLDialogType
7602 if not self.checkDirty():
7603 return
7604
7605 self.classDiagram = UMLDialog(
7606 UMLDialogType.CLASS_DIAGRAM, self.project, self.fileName,
7607 self, noAttrs=False)
7608 self.classDiagram.show()
7609
7610 def __showPackageDiagram(self):
7611 """
7612 Private method to handle the Package Diagram context menu action.
7613 """
7614 from Graphics.UMLDialog import UMLDialog, UMLDialogType
7615 if not self.checkDirty():
7616 return
7617
7618 package = (
7619 os.path.isdir(self.fileName) and
7620 self.fileName or os.path.dirname(self.fileName)
7621 )
7622 res = EricMessageBox.yesNo(
7623 self,
7624 self.tr("Package Diagram"),
7625 self.tr("""Include class attributes?"""),
7626 yesDefault=True)
7627 self.packageDiagram = UMLDialog(
7628 UMLDialogType.PACKAGE_DIAGRAM, self.project, package,
7629 self, noAttrs=not res)
7630 self.packageDiagram.show()
7631
7632 def __showImportsDiagram(self):
7633 """
7634 Private method to handle the Imports Diagram context menu action.
7635 """
7636 from Graphics.UMLDialog import UMLDialog, UMLDialogType
7637 if not self.checkDirty():
7638 return
7639
7640 package = os.path.dirname(self.fileName)
7641 res = EricMessageBox.yesNo(
7642 self,
7643 self.tr("Imports Diagram"),
7644 self.tr("""Include imports from external modules?"""))
7645 self.importsDiagram = UMLDialog(
7646 UMLDialogType.IMPORTS_DIAGRAM, self.project, package,
7647 self, showExternalImports=res)
7648 self.importsDiagram.show()
7649
7650 def __showApplicationDiagram(self):
7651 """
7652 Private method to handle the Imports Diagram context menu action.
7653 """
7654 from Graphics.UMLDialog import UMLDialog, UMLDialogType
7655 res = EricMessageBox.yesNo(
7656 self,
7657 self.tr("Application Diagram"),
7658 self.tr("""Include module names?"""),
7659 yesDefault=True)
7660 self.applicationDiagram = UMLDialog(
7661 UMLDialogType.APPLICATION_DIAGRAM, self.project,
7662 self, noModules=not res)
7663 self.applicationDiagram.show()
7664
7665 def __loadDiagram(self):
7666 """
7667 Private slot to load a diagram from file.
7668 """
7669 from Graphics.UMLDialog import UMLDialog, UMLDialogType
7670 self.loadedDiagram = UMLDialog(
7671 UMLDialogType.NO_DIAGRAM, self.project, parent=self)
7672 if self.loadedDiagram.load():
7673 self.loadedDiagram.show(fromFile=True)
7674 else:
7675 self.loadedDiagram = None
7676
7677 #######################################################################
7678 ## Typing aids related methods below
7679 #######################################################################
7680
7681 def __toggleTypingAids(self):
7682 """
7683 Private slot to toggle the typing aids.
7684 """
7685 if self.menuActs["TypingAidsEnabled"].isChecked():
7686 self.completer.setEnabled(True)
7687 else:
7688 self.completer.setEnabled(False)
7689
7690 #######################################################################
7691 ## Auto-completing templates
7692 #######################################################################
7693
7694 def editorCommand(self, cmd):
7695 """
7696 Public method to perform a simple editor command.
7697
7698 @param cmd the scintilla command to be performed
7699 """
7700 if cmd == QsciScintilla.SCI_TAB:
7701 try:
7702 templateViewer = ericApp().getObject("TemplateViewer")
7703 except KeyError:
7704 # template viewer is not active
7705 templateViewer = None
7706
7707 if templateViewer is not None:
7708 line, index = self.getCursorPosition()
7709 tmplName = self.getWordLeft(line, index)
7710 if tmplName:
7711 if templateViewer.hasTemplate(tmplName,
7712 self.getLanguage()):
7713 self.__applyTemplate(tmplName, self.getLanguage())
7714 return
7715 else:
7716 templateNames = templateViewer.getTemplateNames(
7717 tmplName, self.getLanguage())
7718 if len(templateNames) == 1:
7719 self.__applyTemplate(templateNames[0],
7720 self.getLanguage())
7721 return
7722 elif len(templateNames) > 1:
7723 self.showUserList(
7724 TemplateCompletionListID,
7725 ["{0}?{1:d}".format(t, self.TemplateImageID)
7726 for t in templateNames])
7727 return
7728
7729 elif cmd == QsciScintilla.SCI_DELETEBACK:
7730 line, index = self.getCursorPosition()
7731 text = self.text(line)[index - 1:index + 1]
7732 matchingPairs = ['()', '[]', '{}', '<>', "''", '""']
7733 # __IGNORE_WARNING_M613__
7734 if text in matchingPairs:
7735 self.delete()
7736
7737 super().editorCommand(cmd)
7738
7739 def __applyTemplate(self, templateName, language):
7740 """
7741 Private method to apply a template by name.
7742
7743 @param templateName name of the template to apply (string)
7744 @param language name of the language (group) to get the template
7745 from (string)
7746 """
7747 try:
7748 templateViewer = ericApp().getObject("TemplateViewer")
7749 except KeyError:
7750 # template viewer is not active
7751 return
7752
7753 if templateViewer.hasTemplate(templateName, self.getLanguage()):
7754 self.extendSelectionWordLeft()
7755 templateViewer.applyNamedTemplate(templateName,
7756 self.getLanguage())
7757
7758 #######################################################################
7759 ## Project related methods
7760 #######################################################################
7761
7762 def __projectPropertiesChanged(self):
7763 """
7764 Private slot to handle changes of the project properties.
7765 """
7766 if self.spell:
7767 pwl, pel = self.project.getProjectDictionaries()
7768 self.__setSpellingLanguage(self.project.getProjectSpellLanguage(),
7769 pwl=pwl, pel=pel)
7770
7771 editorConfigEol = self.__getEditorConfig("EOLMode", nodefault=True)
7772 if editorConfigEol is not None:
7773 self.setEolMode(editorConfigEol)
7774 else:
7775 self.setEolModeByEolString(self.project.getEolString())
7776 self.convertEols(self.eolMode())
7777
7778 def addedToProject(self):
7779 """
7780 Public method to signal, that this editor has been added to a project.
7781 """
7782 if self.spell:
7783 pwl, pel = self.project.getProjectDictionaries()
7784 self.__setSpellingLanguage(self.project.getProjectSpellLanguage(),
7785 pwl=pwl, pel=pel)
7786
7787 self.project.projectPropertiesChanged.connect(
7788 self.__projectPropertiesChanged)
7789
7790 def projectOpened(self):
7791 """
7792 Public slot to handle the opening of a project.
7793 """
7794 if (
7795 self.fileName and
7796 self.project.isProjectSource(self.fileName)
7797 ):
7798 self.project.projectPropertiesChanged.connect(
7799 self.__projectPropertiesChanged)
7800 self.setSpellingForProject()
7801
7802 def projectClosed(self):
7803 """
7804 Public slot to handle the closing of a project.
7805 """
7806 with contextlib.suppress(TypeError):
7807 self.project.projectPropertiesChanged.disconnect(
7808 self.__projectPropertiesChanged)
7809
7810 #######################################################################
7811 ## Spell checking related methods
7812 #######################################################################
7813
7814 def getSpellingLanguage(self):
7815 """
7816 Public method to get the current spelling language.
7817
7818 @return current spelling language
7819 @rtype str
7820 """
7821 if self.spell:
7822 return self.spell.getLanguage()
7823
7824 return ""
7825
7826 def __setSpellingLanguage(self, language, pwl="", pel=""):
7827 """
7828 Private slot to set the spell checking language.
7829
7830 @param language spell checking language to be set (string)
7831 @param pwl name of the personal/project word list (string)
7832 @param pel name of the personal/project exclude list (string)
7833 """
7834 if self.spell and self.spell.getLanguage() != language:
7835 self.spell.setLanguage(language, pwl=pwl, pel=pel)
7836 self.spell.checkDocumentIncrementally()
7837
7838 def __setSpelling(self):
7839 """
7840 Private method to initialize the spell checking functionality.
7841 """
7842 if Preferences.getEditor("SpellCheckingEnabled"):
7843 self.__spellCheckStringsOnly = Preferences.getEditor(
7844 "SpellCheckStringsOnly")
7845 if self.spell is None:
7846 self.spell = SpellChecker(self, self.spellingIndicator,
7847 checkRegion=self.isSpellCheckRegion)
7848 self.setSpellingForProject()
7849 self.spell.setMinimumWordSize(
7850 Preferences.getEditor("SpellCheckingMinWordSize"))
7851
7852 self.setAutoSpellChecking()
7853 else:
7854 self.spell = None
7855 self.clearAllIndicators(self.spellingIndicator)
7856
7857 def setSpellingForProject(self):
7858 """
7859 Public method to set the spell checking options for files belonging
7860 to the current project.
7861 """
7862 if (
7863 self.fileName and
7864 self.project.isOpen() and
7865 self.project.isProjectSource(self.fileName)
7866 ):
7867 pwl, pel = self.project.getProjectDictionaries()
7868 self.__setSpellingLanguage(self.project.getProjectSpellLanguage(),
7869 pwl=pwl, pel=pel)
7870
7871 def setAutoSpellChecking(self):
7872 """
7873 Public method to set the automatic spell checking.
7874 """
7875 if Preferences.getEditor("AutoSpellCheckingEnabled"):
7876 with contextlib.suppress(TypeError):
7877 self.SCN_CHARADDED.connect(
7878 self.__spellCharAdded, Qt.ConnectionType.UniqueConnection)
7879 self.spell.checkDocumentIncrementally()
7880 else:
7881 with contextlib.suppress(TypeError):
7882 self.SCN_CHARADDED.disconnect(self.__spellCharAdded)
7883 self.clearAllIndicators(self.spellingIndicator)
7884
7885 def isSpellCheckRegion(self, pos):
7886 """
7887 Public method to check, if the given position is within a region, that
7888 should be spell checked.
7889
7890 For files with a configured full text file extension all regions will
7891 be regarded as to be checked. Depending on configuration, all unknown
7892 files (i.e. those without a file extension) will be checked fully as
7893 well.
7894
7895 @param pos position to be checked
7896 @type int
7897 @return flag indicating pos is in a spell check region
7898 @rtype bool
7899 """
7900 if self.__spellCheckStringsOnly:
7901 if (
7902 (self.__fileNameExtension in
7903 Preferences.getEditor("FullSpellCheckExtensions")) or
7904 (not self.__fileNameExtension and
7905 Preferences.getEditor("FullSpellCheckUnknown"))
7906 ):
7907 return True
7908 else:
7909 style = self.styleAt(pos)
7910 if self.lexer_ is not None:
7911 return (
7912 self.lexer_.isCommentStyle(style) or
7913 self.lexer_.isStringStyle(style)
7914 )
7915
7916 return True
7917
7918 @pyqtSlot(int)
7919 def __spellCharAdded(self, charNumber):
7920 """
7921 Private slot called to handle the user entering a character.
7922
7923 @param charNumber value of the character entered (integer)
7924 """
7925 if self.spell:
7926 if not chr(charNumber).isalnum():
7927 self.spell.checkWord(
7928 self.positionBefore(self.currentPosition()), True)
7929 elif self.hasIndicator(
7930 self.spellingIndicator, self.currentPosition()):
7931 self.spell.checkWord(self.currentPosition())
7932
7933 def checkSpelling(self):
7934 """
7935 Public slot to perform an interactive spell check of the document.
7936 """
7937 if self.spell:
7938 cline, cindex = self.getCursorPosition()
7939 from .SpellCheckingDialog import SpellCheckingDialog
7940 dlg = SpellCheckingDialog(self.spell, 0, self.length(), self)
7941 dlg.exec()
7942 self.setCursorPosition(cline, cindex)
7943 if Preferences.getEditor("AutoSpellCheckingEnabled"):
7944 self.spell.checkDocumentIncrementally()
7945
7946 def __checkSpellingSelection(self):
7947 """
7948 Private slot to spell check the current selection.
7949 """
7950 from .SpellCheckingDialog import SpellCheckingDialog
7951 sline, sindex, eline, eindex = self.getSelection()
7952 startPos = self.positionFromLineIndex(sline, sindex)
7953 endPos = self.positionFromLineIndex(eline, eindex)
7954 dlg = SpellCheckingDialog(self.spell, startPos, endPos, self)
7955 dlg.exec()
7956
7957 def __checkSpellingWord(self):
7958 """
7959 Private slot to check the word below the spelling context menu.
7960 """
7961 from .SpellCheckingDialog import SpellCheckingDialog
7962 line, index = self.lineIndexFromPosition(self.spellingMenuPos)
7963 wordStart, wordEnd = self.getWordBoundaries(line, index)
7964 wordStartPos = self.positionFromLineIndex(line, wordStart)
7965 wordEndPos = self.positionFromLineIndex(line, wordEnd)
7966 dlg = SpellCheckingDialog(self.spell, wordStartPos, wordEndPos, self)
7967 dlg.exec()
7968
7969 def __showContextMenuSpelling(self):
7970 """
7971 Private slot to set up the spelling menu before it is shown.
7972 """
7973 self.spellingMenu.clear()
7974 self.spellingSuggActs = []
7975 line, index = self.lineIndexFromPosition(self.spellingMenuPos)
7976 word = self.getWord(line, index)
7977 suggestions = self.spell.getSuggestions(word)
7978 for suggestion in suggestions[:5]:
7979 self.spellingSuggActs.append(
7980 self.spellingMenu.addAction(suggestion))
7981 if suggestions:
7982 self.spellingMenu.addSeparator()
7983 self.spellingMenu.addAction(
7984 UI.PixmapCache.getIcon("spellchecking"),
7985 self.tr("Check spelling..."), self.__checkSpellingWord)
7986 self.spellingMenu.addAction(
7987 self.tr("Add to dictionary"), self.__addToSpellingDictionary)
7988 self.spellingMenu.addAction(
7989 self.tr("Ignore All"), self.__ignoreSpellingAlways)
7990
7991 self.showMenu.emit("Spelling", self.spellingMenu, self)
7992
7993 def __contextMenuSpellingTriggered(self, action):
7994 """
7995 Private slot to handle the selection of a suggestion of the spelling
7996 context menu.
7997
7998 @param action reference to the action that was selected (QAction)
7999 """
8000 if action in self.spellingSuggActs:
8001 replacement = action.text()
8002 line, index = self.lineIndexFromPosition(self.spellingMenuPos)
8003 wordStart, wordEnd = self.getWordBoundaries(line, index)
8004 self.setSelection(line, wordStart, line, wordEnd)
8005 self.beginUndoAction()
8006 self.removeSelectedText()
8007 self.insert(replacement)
8008 self.endUndoAction()
8009
8010 def __addToSpellingDictionary(self):
8011 """
8012 Private slot to add the word below the spelling context menu to the
8013 dictionary.
8014 """
8015 line, index = self.lineIndexFromPosition(self.spellingMenuPos)
8016 word = self.getWord(line, index)
8017 self.spell.add(word)
8018
8019 wordStart, wordEnd = self.getWordBoundaries(line, index)
8020 self.clearIndicator(self.spellingIndicator, line, wordStart,
8021 line, wordEnd)
8022 if Preferences.getEditor("AutoSpellCheckingEnabled"):
8023 self.spell.checkDocumentIncrementally()
8024
8025 def __removeFromSpellingDictionary(self):
8026 """
8027 Private slot to remove the word below the context menu to the
8028 dictionary.
8029 """
8030 line, index = self.lineIndexFromPosition(self.spellingMenuPos)
8031 word = self.getWord(line, index)
8032 self.spell.remove(word)
8033
8034 if Preferences.getEditor("AutoSpellCheckingEnabled"):
8035 self.spell.checkDocumentIncrementally()
8036
8037 def __ignoreSpellingAlways(self):
8038 """
8039 Private to always ignore the word below the spelling context menu.
8040 """
8041 line, index = self.lineIndexFromPosition(self.spellingMenuPos)
8042 word = self.getWord(line, index)
8043 self.spell.ignoreAlways(word)
8044 if Preferences.getEditor("AutoSpellCheckingEnabled"):
8045 self.spell.checkDocumentIncrementally()
8046
8047 #######################################################################
8048 ## Cooperation related methods
8049 #######################################################################
8050
8051 def getSharingStatus(self):
8052 """
8053 Public method to get some share status info.
8054
8055 @return tuple indicating, if the editor is sharable, the sharing
8056 status, if it is inside a locally initiated shared edit session
8057 and if it is inside a remotely initiated shared edit session
8058 (boolean, boolean, boolean, boolean)
8059 """
8060 return (
8061 (bool(self.fileName) and
8062 self.project.isOpen() and
8063 self.project.isProjectFile(self.fileName)),
8064 self.__isShared,
8065 self.__inSharedEdit,
8066 self.__inRemoteSharedEdit
8067 )
8068
8069 def shareConnected(self, connected):
8070 """
8071 Public slot to handle a change of the connected state.
8072
8073 @param connected flag indicating the connected state (boolean)
8074 """
8075 if not connected:
8076 self.__inRemoteSharedEdit = False
8077 self.setReadOnly(False)
8078 self.__updateReadOnly()
8079 self.cancelSharedEdit(send=False)
8080 self.__isSyncing = False
8081 self.__receivedWhileSyncing = []
8082
8083 def shareEditor(self, share):
8084 """
8085 Public slot to set the shared status of the editor.
8086
8087 @param share flag indicating the share status (boolean)
8088 """
8089 self.__isShared = share
8090 if not share:
8091 self.shareConnected(False)
8092
8093 def startSharedEdit(self):
8094 """
8095 Public slot to start a shared edit session for the editor.
8096 """
8097 self.__inSharedEdit = True
8098 self.__savedText = self.text()
8099 hashStr = str(
8100 QCryptographicHash.hash(
8101 Utilities.encode(self.__savedText, self.encoding)[0],
8102 QCryptographicHash.Algorithm.Sha1).toHex(),
8103 encoding="utf-8")
8104 self.__send(Editor.StartEditToken, hashStr)
8105
8106 def sendSharedEdit(self):
8107 """
8108 Public slot to end a shared edit session for the editor and
8109 send the changes.
8110 """
8111 commands = self.__calculateChanges(self.__savedText, self.text())
8112 self.__send(Editor.EndEditToken, commands)
8113 self.__inSharedEdit = False
8114 self.__savedText = ""
8115
8116 def cancelSharedEdit(self, send=True):
8117 """
8118 Public slot to cancel a shared edit session for the editor.
8119
8120 @param send flag indicating to send the CancelEdit command (boolean)
8121 """
8122 self.__inSharedEdit = False
8123 self.__savedText = ""
8124 if send:
8125 self.__send(Editor.CancelEditToken)
8126
8127 def __send(self, token, args=None):
8128 """
8129 Private method to send an editor command to remote editors.
8130
8131 @param token command token (string)
8132 @param args arguments for the command (string)
8133 """
8134 if self.vm.isConnected():
8135 msg = ""
8136 if token in (Editor.StartEditToken,
8137 Editor.EndEditToken,
8138 Editor.RequestSyncToken,
8139 Editor.SyncToken):
8140 msg = "{0}{1}{2}".format(
8141 token,
8142 Editor.Separator,
8143 args
8144 )
8145 elif token == Editor.CancelEditToken:
8146 msg = "{0}{1}c".format(
8147 token,
8148 Editor.Separator
8149 )
8150
8151 self.vm.send(self.fileName, msg)
8152
8153 def receive(self, command):
8154 """
8155 Public slot to handle received editor commands.
8156
8157 @param command command string (string)
8158 """
8159 if self.__isShared:
8160 if (
8161 self.__isSyncing and
8162 not command.startswith(Editor.SyncToken + Editor.Separator)
8163 ):
8164 self.__receivedWhileSyncing.append(command)
8165 else:
8166 self.__dispatchCommand(command)
8167
8168 def __dispatchCommand(self, command):
8169 """
8170 Private method to dispatch received commands.
8171
8172 @param command command to be processed (string)
8173 """
8174 token, argsString = command.split(Editor.Separator, 1)
8175 if token == Editor.StartEditToken:
8176 self.__processStartEditCommand(argsString)
8177 elif token == Editor.CancelEditToken:
8178 self.shareConnected(False)
8179 elif token == Editor.EndEditToken:
8180 self.__processEndEditCommand(argsString)
8181 elif token == Editor.RequestSyncToken:
8182 self.__processRequestSyncCommand(argsString)
8183 elif token == Editor.SyncToken:
8184 self.__processSyncCommand(argsString)
8185
8186 def __processStartEditCommand(self, argsString):
8187 """
8188 Private slot to process a remote StartEdit command.
8189
8190 @param argsString string containing the command parameters (string)
8191 """
8192 if not self.__inSharedEdit and not self.__inRemoteSharedEdit:
8193 self.__inRemoteSharedEdit = True
8194 self.setReadOnly(True)
8195 self.__updateReadOnly()
8196 hashStr = str(
8197 QCryptographicHash.hash(
8198 Utilities.encode(self.text(), self.encoding)[0],
8199 QCryptographicHash.Algorithm.Sha1).toHex(),
8200 encoding="utf-8")
8201 if hashStr != argsString:
8202 # text is different to the remote site, request to sync it
8203 self.__isSyncing = True
8204 self.__send(Editor.RequestSyncToken, argsString)
8205
8206 def __calculateChanges(self, old, new):
8207 """
8208 Private method to determine change commands to convert old text into
8209 new text.
8210
8211 @param old old text (string)
8212 @param new new text (string)
8213 @return commands to change old into new (string)
8214 """
8215 oldL = old.splitlines()
8216 newL = new.splitlines()
8217 matcher = difflib.SequenceMatcher(None, oldL, newL)
8218
8219 formatStr = "@@{0} {1} {2} {3}"
8220 commands = []
8221 for token, i1, i2, j1, j2 in matcher.get_opcodes():
8222 if token == "insert": # secok
8223 commands.append(formatStr.format("i", j1, j2 - j1, -1))
8224 commands.extend(newL[j1:j2])
8225 elif token == "delete": # secok
8226 commands.append(formatStr.format("d", j1, i2 - i1, -1))
8227 elif token == "replace": # secok
8228 commands.append(formatStr.format("r", j1, i2 - i1, j2 - j1))
8229 commands.extend(newL[j1:j2])
8230
8231 return "\n".join(commands) + "\n"
8232
8233 def __processEndEditCommand(self, argsString):
8234 """
8235 Private slot to process a remote EndEdit command.
8236
8237 @param argsString string containing the command parameters (string)
8238 """
8239 commands = argsString.splitlines()
8240 sep = self.getLineSeparator()
8241 cur = self.getCursorPosition()
8242
8243 self.setReadOnly(False)
8244 self.beginUndoAction()
8245 while commands:
8246 commandLine = commands.pop(0)
8247 if not commandLine.startswith("@@"):
8248 continue
8249
8250 args = commandLine.split()
8251 command = args.pop(0)
8252 pos, l1, l2 = [int(arg) for arg in args]
8253 if command == "@@i":
8254 txt = sep.join(commands[0:l1]) + sep
8255 self.insertAt(txt, pos, 0)
8256 del commands[0:l1]
8257 elif command == "@@d":
8258 self.setSelection(pos, 0, pos + l1, 0)
8259 self.removeSelectedText()
8260 elif command == "@@r":
8261 self.setSelection(pos, 0, pos + l1, 0)
8262 self.removeSelectedText()
8263 txt = sep.join(commands[0:l2]) + sep
8264 self.insertAt(txt, pos, 0)
8265 del commands[0:l2]
8266 self.endUndoAction()
8267 self.__updateReadOnly()
8268 self.__inRemoteSharedEdit = False
8269
8270 self.setCursorPosition(*cur)
8271
8272 def __processRequestSyncCommand(self, argsString):
8273 """
8274 Private slot to process a remote RequestSync command.
8275
8276 @param argsString string containing the command parameters (string)
8277 """
8278 if self.__inSharedEdit:
8279 hashStr = str(
8280 QCryptographicHash.hash(
8281 Utilities.encode(self.__savedText, self.encoding)[0],
8282 QCryptographicHash.Algorithm.Sha1).toHex(),
8283 encoding="utf-8")
8284
8285 if hashStr == argsString:
8286 self.__send(Editor.SyncToken, self.__savedText)
8287
8288 def __processSyncCommand(self, argsString):
8289 """
8290 Private slot to process a remote Sync command.
8291
8292 @param argsString string containing the command parameters (string)
8293 """
8294 if self.__isSyncing:
8295 cur = self.getCursorPosition()
8296
8297 self.setReadOnly(False)
8298 self.beginUndoAction()
8299 self.selectAll()
8300 self.removeSelectedText()
8301 self.insertAt(argsString, 0, 0)
8302 self.endUndoAction()
8303 self.setReadOnly(True)
8304
8305 self.setCursorPosition(*cur)
8306
8307 while self.__receivedWhileSyncing:
8308 command = self.__receivedWhileSyncing.pop(0)
8309 self.__dispatchCommand(command)
8310
8311 self.__isSyncing = False
8312
8313 #######################################################################
8314 ## Special search related methods
8315 #######################################################################
8316
8317 def searchCurrentWordForward(self):
8318 """
8319 Public slot to search the current word forward.
8320 """
8321 self.__searchCurrentWord(forward=True)
8322
8323 def searchCurrentWordBackward(self):
8324 """
8325 Public slot to search the current word backward.
8326 """
8327 self.__searchCurrentWord(forward=False)
8328
8329 def __searchCurrentWord(self, forward=True):
8330 """
8331 Private slot to search the next occurrence of the current word.
8332
8333 @param forward flag indicating the search direction (boolean)
8334 """
8335 self.hideFindIndicator()
8336 line, index = self.getCursorPosition()
8337 word = self.getCurrentWord()
8338 wordStart, wordEnd = self.getCurrentWordBoundaries()
8339 wordStartPos = self.positionFromLineIndex(line, wordStart)
8340 wordEndPos = self.positionFromLineIndex(line, wordEnd)
8341
8342 regExp = re.compile(r"\b{0}\b".format(word))
8343 startPos = wordEndPos if forward else wordStartPos
8344
8345 matches = [m for m in regExp.finditer(self.text())]
8346 if matches:
8347 if forward:
8348 matchesAfter = [m for m in matches if m.start() >= startPos]
8349 if matchesAfter:
8350 match = matchesAfter[0]
8351 else:
8352 # wrap around
8353 match = matches[0]
8354 else:
8355 matchesBefore = [m for m in matches if m.start() < startPos]
8356 if matchesBefore:
8357 match = matchesBefore[-1]
8358 else:
8359 # wrap around
8360 match = matches[-1]
8361 line, index = self.lineIndexFromPosition(match.start())
8362 self.setSelection(line, index + len(match.group(0)), line, index)
8363 self.showFindIndicator(line, index,
8364 line, index + len(match.group(0)))
8365
8366 #######################################################################
8367 ## Sort related methods
8368 #######################################################################
8369
8370 def sortLines(self):
8371 """
8372 Public slot to sort the lines spanned by a rectangular selection.
8373 """
8374 if not self.selectionIsRectangle():
8375 return
8376
8377 from .SortOptionsDialog import SortOptionsDialog
8378 dlg = SortOptionsDialog()
8379 if dlg.exec() == QDialog.DialogCode.Accepted:
8380 ascending, alnum, caseSensitive = dlg.getData()
8381 origStartLine, origStartIndex, origEndLine, origEndIndex = (
8382 self.getRectangularSelection()
8383 )
8384 # convert to upper-left to lower-right
8385 startLine = min(origStartLine, origEndLine)
8386 startIndex = min(origStartIndex, origEndIndex)
8387 endLine = max(origStartLine, origEndLine)
8388 endIndex = max(origStartIndex, origEndIndex)
8389
8390 # step 1: extract the text of the rectangular selection and
8391 # the lines
8392 selText = {}
8393 txtLines = {}
8394 for line in range(startLine, endLine + 1):
8395 txtLines[line] = self.text(line)
8396 txt = txtLines[line][startIndex:endIndex].strip()
8397 if not alnum:
8398 try:
8399 txt = float(txt)
8400 except ValueError:
8401 EricMessageBox.critical(
8402 self,
8403 self.tr("Sort Lines"),
8404 self.tr(
8405 """The selection contains illegal data for a"""
8406 """ numerical sort."""))
8407 return
8408
8409 if txt in selText:
8410 selText[txt].append(line)
8411 else:
8412 selText[txt] = [line]
8413
8414 # step 2: calculate the sort parameters
8415 reverse = not ascending
8416 if alnum and not caseSensitive:
8417 keyFun = str.lower
8418 else:
8419 keyFun = None
8420
8421 # step 3: sort the lines
8422 eol = self.getLineSeparator()
8423 lastWithEol = True
8424 newLines = []
8425 for txt in sorted(selText.keys(), key=keyFun, reverse=reverse):
8426 for line in selText[txt]:
8427 txt = txtLines[line]
8428 if not txt.endswith(eol):
8429 lastWithEol = False
8430 txt += eol
8431 newLines.append(txt)
8432 if not lastWithEol:
8433 newLines[-1] = newLines[-1][:-len(eol)]
8434
8435 # step 4: replace the lines by the sorted ones
8436 self.setSelection(startLine, 0, endLine + 1, 0)
8437 self.beginUndoAction()
8438 self.replaceSelectedText("".join(newLines))
8439 self.endUndoAction()
8440
8441 # step 5: reset the rectangular selection
8442 self.setRectangularSelection(origStartLine, origStartIndex,
8443 origEndLine, origEndIndex)
8444 self.selectionChanged.emit()
8445
8446 #######################################################################
8447 ## Mouse click handler related methods
8448 #######################################################################
8449
8450 def mouseReleaseEvent(self, evt):
8451 """
8452 Protected method calling a registered mouse click handler function.
8453
8454 @param evt event object
8455 @type QMouseEvent
8456 """
8457 modifiers = evt.modifiers()
8458 button = evt.button()
8459 key = (modifiers, button)
8460
8461 self.vm.eventFilter(self, evt)
8462 super().mouseReleaseEvent(evt)
8463
8464 if (
8465 button != Qt.MouseButton.NoButton and
8466 Preferences.getEditor("MouseClickHandlersEnabled") and
8467 key in self.__mouseClickHandlers
8468 ):
8469 evt.accept()
8470 self.__mouseClickHandlers[key][1](self)
8471 else:
8472 super().mouseReleaseEvent(evt)
8473
8474 def setMouseClickHandler(self, name, modifiers, button, function):
8475 """
8476 Public method to set a mouse click handler.
8477
8478 @param name name of the plug-in (or 'internal') setting this handler
8479 @type str
8480 @param modifiers keyboard modifiers of the handler
8481 @type Qt.KeyboardModifiers or int
8482 @param button mouse button of the handler
8483 @type Qt.MouseButton or int
8484 @param function handler function
8485 @type func
8486 @return flag indicating success
8487 @rtype bool
8488 """
8489 if button and button != Qt.MouseButton.NoButton:
8490 key = (modifiers, button)
8491 if key in self.__mouseClickHandlers:
8492 EricMessageBox.warning(
8493 self,
8494 self.tr("Register Mouse Click Handler"),
8495 self.tr("""A mouse click handler for "{0}" was already"""
8496 """ registered by "{1}". Aborting request by"""
8497 """ "{2}"...""").format(
8498 MouseUtilities.MouseButtonModifier2String(
8499 modifiers, button),
8500 self.__mouseClickHandlers[key][0],
8501 name))
8502 return False
8503
8504 self.__mouseClickHandlers[key] = (name, function)
8505 return True
8506
8507 return False
8508
8509 def getMouseClickHandler(self, modifiers, button):
8510 """
8511 Public method to get a registered mouse click handler.
8512
8513 @param modifiers keyboard modifiers of the handler
8514 @type Qt.KeyboardModifiers
8515 @param button mouse button of the handler
8516 @type Qt.MouseButton
8517 @return plug-in name and registered function
8518 @rtype tuple of str and func
8519 """
8520 key = (modifiers, button)
8521 if key in self.__mouseClickHandlers:
8522 return self.__mouseClickHandlers[key]
8523 else:
8524 return ("", None)
8525
8526 def getMouseClickHandlers(self, name):
8527 """
8528 Public method to get all registered mouse click handlers of
8529 a plug-in.
8530
8531 @param name name of the plug-in
8532 @type str
8533 @return registered mouse click handlers as list of modifiers,
8534 mouse button and function
8535 @rtype list of tuple of (Qt.KeyboardModifiers, Qt.MouseButton, func)
8536 """
8537 lst = []
8538 for key, value in self.__mouseClickHandlers.items():
8539 if value[0] == name:
8540 lst.append((key[0], key[1], value[1]))
8541 return lst
8542
8543 def removeMouseClickHandler(self, modifiers, button):
8544 """
8545 Public method to un-registered a mouse click handler.
8546
8547 @param modifiers keyboard modifiers of the handler
8548 @type Qt.KeyboardModifiers
8549 @param button mouse button of the handler
8550 @type Qt.MouseButton
8551 """
8552 key = (modifiers, button)
8553 if key in self.__mouseClickHandlers:
8554 del self.__mouseClickHandlers[key]
8555
8556 def removeMouseClickHandlers(self, name):
8557 """
8558 Public method to un-registered all mouse click handlers of
8559 a plug-in.
8560
8561 @param name name of the plug-in
8562 @type str
8563 """
8564 keys = []
8565 for key in self.__mouseClickHandlers:
8566 if self.__mouseClickHandlers[key][0] == name:
8567 keys.append(key)
8568 for key in keys:
8569 del self.__mouseClickHandlers[key]
8570
8571 def gotoReferenceHandler(self, referencesList):
8572 """
8573 Public method to handle a list of references to perform a goto.
8574
8575 @param referencesList list of references for a 'goto' action
8576 @type ReferenceItem
8577 """
8578 references = []
8579 referencePositions = []
8580
8581 for reference in referencesList:
8582 if (
8583 reference.modulePath != self.getFileName() or
8584 self.getCursorPosition()[0] + 1 != reference.line
8585 ):
8586 if reference.modulePath == self.getFileName():
8587 references.append(
8588 self.tr("{0:4d} {1}", "line number, source code")
8589 .format(reference.line, reference.codeLine.strip())
8590 )
8591 else:
8592 references.append(
8593 self.tr("{0:4d} {1}\n => {2}",
8594 "line number, source code, file name")
8595 .format(
8596 reference.line, reference.codeLine.strip(),
8597 self.project.getRelativePath(reference.modulePath)
8598 )
8599 )
8600 referencePositions.append(
8601 (reference.modulePath, reference.line, reference.column))
8602
8603 if references:
8604 if self.isCallTipActive():
8605 self.cancelCallTips()
8606 self.__referencesList = references
8607 self.__referencesPositionsList = referencePositions
8608 self.showUserList(ReferencesListID, references)
8609
8610 #######################################################################
8611 ## Methods implementing a Shell interface
8612 #######################################################################
8613
8614 def __executeSelection(self):
8615 """
8616 Private slot to execute the selected text in the shell window.
8617 """
8618 txt = self.selectedText()
8619 ericApp().getObject("Shell").executeLines(txt)
8620
8621 #######################################################################
8622 ## Methods implementing the interface to EditorConfig
8623 #######################################################################
8624
8625 def __loadEditorConfig(self, fileName=""):
8626 """
8627 Private method to load the EditorConfig properties.
8628
8629 @param fileName name of the file
8630 @type str
8631 """
8632 if not fileName:
8633 fileName = self.fileName
8634
8635 self.__editorConfig = self.__loadEditorConfigObject(fileName)
8636
8637 if fileName:
8638 self.__setTabAndIndent()
8639
8640 def __loadEditorConfigObject(self, fileName):
8641 """
8642 Private method to load the EditorConfig properties for the given
8643 file name.
8644
8645 @param fileName name of the file
8646 @type str
8647 @return EditorConfig dictionary
8648 @rtype dict
8649 """
8650 editorConfig = {}
8651
8652 if fileName:
8653 try:
8654 editorConfig = editorconfig.get_properties(fileName)
8655 except editorconfig.EditorConfigError:
8656 EricMessageBox.warning(
8657 self,
8658 self.tr("EditorConfig Properties"),
8659 self.tr("""<p>The EditorConfig properties for file"""
8660 """ <b>{0}</b> could not be loaded.</p>""")
8661 .format(fileName))
8662
8663 return editorConfig
8664
8665 def __getEditorConfig(self, option, nodefault=False, config=None):
8666 """
8667 Private method to get the requested option via EditorConfig.
8668
8669 If there is no EditorConfig defined, the equivalent built-in option
8670 will be used (Preferences.getEditor() ). The option must be given as
8671 the Preferences option key. The mapping to the EditorConfig option name
8672 will be done within this method.
8673
8674 @param option Preferences option key
8675 @type str
8676 @param nodefault flag indicating to not get the default value from
8677 Preferences but return None instead
8678 @type bool
8679 @param config reference to an EditorConfig object or None
8680 @type dict
8681 @return value of requested setting or None if nothing was found and
8682 nodefault parameter was True
8683 @rtype any
8684 """
8685 if config is None:
8686 config = self.__editorConfig
8687
8688 if not config:
8689 if nodefault:
8690 return None
8691 else:
8692 value = self.__getOverrideValue(option)
8693 if value is None:
8694 # no override
8695 value = Preferences.getEditor(option)
8696 return value
8697
8698 try:
8699 if option == "EOLMode":
8700 value = config["end_of_line"]
8701 if value == "lf":
8702 value = QsciScintilla.EolMode.EolUnix
8703 elif value == "crlf":
8704 value = QsciScintilla.EolMode.EolWindows
8705 elif value == "cr":
8706 value = QsciScintilla.EolMode.EolMac
8707 else:
8708 value = None
8709 elif option == "DefaultEncoding":
8710 value = config["charset"]
8711 elif option == "InsertFinalNewline":
8712 value = Utilities.toBool(config["insert_final_newline"])
8713 elif option == "StripTrailingWhitespace":
8714 value = Utilities.toBool(config["trim_trailing_whitespace"])
8715 elif option == "TabWidth":
8716 value = int(config["tab_width"])
8717 elif option == "IndentWidth":
8718 value = config["indent_size"]
8719 if value == "tab":
8720 value = self.__getEditorConfig("TabWidth", config=config)
8721 else:
8722 value = int(value)
8723 elif option == "TabForIndentation":
8724 value = config["indent_style"] == "tab"
8725 except KeyError:
8726 value = None
8727
8728 if value is None and not nodefault:
8729 # use Preferences in case of error
8730 value = self.__getOverrideValue(option)
8731 if value is None:
8732 # no override
8733 value = Preferences.getEditor(option)
8734
8735 return value
8736
8737 def getEditorConfig(self, option):
8738 """
8739 Public method to get the requested option via EditorConfig.
8740
8741 @param option Preferences option key
8742 @type str
8743 @return value of requested setting
8744 @rtype any
8745 """
8746 return self.__getEditorConfig(option)
8747
8748 def __getOverrideValue(self, option):
8749 """
8750 Private method to get an override value for the current file type.
8751
8752 @param option Preferences option key
8753 @type str
8754 @return override value; None in case nothing is defined
8755 @rtype any
8756 """
8757 if option in ("TabWidth", "IndentWidth"):
8758 overrides = Preferences.getEditor("TabIndentOverride")
8759 language = self.filetype or self.apiLanguage
8760 if language in overrides:
8761 if option == "TabWidth":
8762 return overrides[language][0]
8763 elif option == "IndentWidth":
8764 return overrides[language][1]
8765
8766 return None
8767
8768 #######################################################################
8769 ## Methods implementing the docstring generator interface
8770 #######################################################################
8771
8772 def getDocstringGenerator(self):
8773 """
8774 Public method to get a reference to the docstring generator.
8775
8776 @return reference to the docstring generator
8777 @rtype BaseDocstringGenerator
8778 """
8779 if self.__docstringGenerator is None:
8780 from . import DocstringGenerator
8781 self.__docstringGenerator = (
8782 DocstringGenerator.getDocstringGenerator(self)
8783 )
8784
8785 return self.__docstringGenerator
8786
8787 def insertDocstring(self):
8788 """
8789 Public method to generate and insert a docstring for the function under
8790 the cursor.
8791
8792 Note: This method is called via a keyboard shortcut or through the
8793 global 'Edit' menu.
8794 """
8795 generator = self.getDocstringGenerator()
8796 generator.insertDocstringFromShortcut(self.getCursorPosition())
8797
8798 @pyqtSlot()
8799 def __insertDocstring(self):
8800 """
8801 Private slot to generate and insert a docstring for the function under
8802 the cursor.
8803 """
8804 generator = self.getDocstringGenerator()
8805 generator.insertDocstring(self.getCursorPosition(), fromStart=True)
8806
8807 def __delayedDocstringMenuPopup(self, cursorPosition):
8808 """
8809 Private method to test, if the user might want to insert a docstring.
8810
8811 @param cursorPosition current cursor position (line and column)
8812 @type tuple of (int, int)
8813 """
8814 if (
8815 Preferences.getEditor("DocstringAutoGenerate") and
8816 self.getDocstringGenerator().isDocstringIntro(cursorPosition)
8817 ):
8818 lineText2Cursor = self.text(cursorPosition[0])[:cursorPosition[1]]
8819
8820 QTimer.singleShot(
8821 300,
8822 lambda: self.__popupDocstringMenu(lineText2Cursor,
8823 cursorPosition)
8824 )
8825
8826 def __popupDocstringMenu(self, lastLineText, lastCursorPosition):
8827 """
8828 Private slot to pop up a menu asking the user, if a docstring should be
8829 inserted.
8830
8831 @param lastLineText line contents when the delay timer was started
8832 @type str
8833 @param lastCursorPosition position of the cursor when the delay timer
8834 was started (line and index)
8835 @type tuple of (int, int)
8836 """
8837 cursorPosition = self.getCursorPosition()
8838 if lastCursorPosition != cursorPosition:
8839 return
8840
8841 if self.text(cursorPosition[0])[:cursorPosition[1]] != lastLineText:
8842 return
8843
8844 generator = self.getDocstringGenerator()
8845 if generator.hasFunctionDefinition(cursorPosition):
8846 from .DocstringGenerator.BaseDocstringGenerator import (
8847 DocstringMenuForEnterOnly
8848 )
8849 docstringMenu = DocstringMenuForEnterOnly(self)
8850 act = docstringMenu.addAction(
8851 UI.PixmapCache.getIcon("fileText"),
8852 self.tr("Generate Docstring"),
8853 lambda: generator.insertDocstring(cursorPosition,
8854 fromStart=False)
8855 )
8856 docstringMenu.setActiveAction(act)
8857 docstringMenu.popup(
8858 self.mapToGlobal(self.getGlobalCursorPosition()))
8859
8860 #######################################################################
8861 ## Methods implementing the mouse hover help interface
8862 #######################################################################
8863
8864 @pyqtSlot(int, int, int)
8865 def __showMouseHoverHelp(self, pos, x, y):
8866 """
8867 Private slot showing code information about the symbol under the
8868 cursor.
8869
8870 @param pos mouse position into the document
8871 @type int
8872 @param x x-value of mouse screen position
8873 @type int
8874 @param y y-value of mouse screen position
8875 @type int
8876 """
8877 if not self.isCallTipActive() and not self.isListActive():
8878 if self.__mouseHoverHelp is not None and pos > 0 and y > 0:
8879 line, index = self.lineIndexFromPosition(pos)
8880 if index > 0:
8881 self.__mouseHoverHelp(self, line, index)
8882 else:
8883 self.__cancelMouseHoverHelp()
8884 else:
8885 self.__cancelMouseHoverHelp()
8886
8887 def __cancelMouseHoverHelp(self):
8888 """
8889 Private slot cancelling the display of mouse hover help.
8890 """
8891 if self.__showingMouseHoverHelp:
8892 self.cancelCallTips()
8893 self.__showingMouseHoverHelp = False
8894
8895 def registerMouseHoverHelpFunction(self, func):
8896 """
8897 Public method to register a mouse hover help function.
8898
8899 Note: Only one plugin should provide this function. Otherwise
8900 the last one wins.
8901
8902 @param func function accepting a reference to the calling editor and
8903 the line and column position (zero based each)
8904 @type func
8905 """
8906 self.__mouseHoverHelp = func
8907
8908 def unregisterMouseHoverHelpFunction(self, func):
8909 """
8910 Public method to unregister a mouse hover help function.
8911
8912 @param func function accepting a reference to the calling editor and
8913 the line and column position (zero based each)
8914 @type func
8915 """
8916 if self.__mouseHoverHelp is func:
8917 self.__mouseHoverHelp = None
8918
8919 def showMouseHoverHelpData(self, line, index, data):
8920 """
8921 Public method to show the mouse hover help data.
8922
8923 @param line line of mouse cursor position
8924 @type int
8925 @param index column of mouse cursor position
8926 @type int
8927 @param data information text to be shown
8928 @type str
8929 """
8930 if data and self.hasFocus() and not self.isListActive():
8931 pos = self.positionFromLineIndex(line, index)
8932 self.SendScintilla(QsciScintilla.SCI_CALLTIPSHOW,
8933 pos, self._encodeString(data))
8934 self.__showingMouseHoverHelp = True
8935 else:
8936 self.__cancelMouseHoverHelp()

eric ide

mercurial