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