--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/QScintilla/Editor.py Sun Jul 24 11:29:56 2022 +0200 @@ -0,0 +1,9042 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the editor component of the eric IDE. +""" + +import bisect +import collections +import contextlib +import difflib +import os +import pathlib +import re + +import editorconfig + +from PyQt6.QtCore import ( + pyqtSignal, + pyqtSlot, + Qt, + QDir, + QTimer, + QModelIndex, + QCryptographicHash, + QEvent, + QDateTime, + QPoint, + QSize, +) +from PyQt6.QtGui import QPalette, QFont, QPixmap, QPainter, QActionGroup +from PyQt6.QtWidgets import QLineEdit, QDialog, QInputDialog, QApplication, QMenu +from PyQt6.QtPrintSupport import QPrinter, QPrintDialog, QAbstractPrintDialog +from PyQt6.Qsci import QsciScintilla, QsciMacro, QsciStyledText + +from EricWidgets.EricApplication import ericApp +from EricWidgets import EricFileDialog, EricMessageBox +from EricGui.EricOverrideCursor import EricOverrideCursor + +from EricUtilities.EricCache import EricCache + +from .QsciScintillaCompat import QsciScintillaCompat +from .EditorMarkerMap import EditorMarkerMap +from .SpellChecker import SpellChecker + +from Globals import recentNameBreakpointConditions + +import Preferences +import Utilities +from Utilities import MouseUtilities + +import UI.PixmapCache + +from UI import PythonDisViewer + +from CodeFormatting.BlackFormattingAction import BlackFormattingAction + +EditorAutoCompletionListID = 1 +TemplateCompletionListID = 2 +ReferencesListID = 3 + +ReferenceItem = collections.namedtuple( + "ReferenceItem", ["modulePath", "codeLine", "line", "column"] +) + + +class Editor(QsciScintillaCompat): + """ + Class implementing the editor component of the eric IDE. + + @signal modificationStatusChanged(bool, QsciScintillaCompat) emitted when + the modification status has changed + @signal undoAvailable(bool) emitted to signal the undo availability + @signal redoAvailable(bool) emitted to signal the redo availability + @signal cursorChanged(str, int, int) emitted when the cursor position + was changed + @signal cursorLineChanged(int) emitted when the cursor line was changed + @signal editorAboutToBeSaved(str) emitted before the editor is saved + @signal editorSaved(str) emitted after the editor has been saved + @signal editorRenamed(str) emitted after the editor got a new name + (i.e. after a 'Save As') + @signal captionChanged(str, QsciScintillaCompat) emitted when the caption + is updated. Typically due to a readOnly attribute change. + @signal breakpointToggled(QsciScintillaCompat) emitted when a breakpoint + is toggled + @signal bookmarkToggled(QsciScintillaCompat) emitted when a bookmark is + toggled + @signal syntaxerrorToggled(QsciScintillaCompat) emitted when a syntax error + was discovered + @signal autoCompletionAPIsAvailable(bool) emitted after the autocompletion + function has been configured + @signal coverageMarkersShown(bool) emitted after the coverage markers have + been shown or cleared + @signal taskMarkersUpdated(QsciScintillaCompat) emitted when the task + markers were updated + @signal changeMarkersUpdated(QsciScintillaCompat) emitted when the change + markers were updated + @signal showMenu(str, QMenu, QsciScintillaCompat) emitted when a menu is + about to be shown. The name of the menu, a reference to the menu and + a reference to the editor are given. + @signal languageChanged(str) emitted when the editors language was set. The + language is passed as a parameter. + @signal eolChanged(str) emitted when the editors eol type was set. The eol + string is passed as a parameter. + @signal encodingChanged(str) emitted when the editors encoding was set. The + encoding name is passed as a parameter. + @signal spellLanguageChanged(str) emitted when the editor spell check + language was set. The language is passed as a parameter. + @signal lastEditPositionAvailable() emitted when a last edit position is + available + @signal refreshed() emitted to signal a refresh of the editor contents + @signal settingsRead() emitted to signal, that the settings have been read + and set + @signal mouseDoubleClick(position, buttons) emitted to signal a mouse + double click somewhere in the editor area + """ + + modificationStatusChanged = pyqtSignal(bool, QsciScintillaCompat) + undoAvailable = pyqtSignal(bool) + redoAvailable = pyqtSignal(bool) + cursorChanged = pyqtSignal(str, int, int) + cursorLineChanged = pyqtSignal(int) + editorAboutToBeSaved = pyqtSignal(str) + editorSaved = pyqtSignal(str) + editorRenamed = pyqtSignal(str) + captionChanged = pyqtSignal(str, QsciScintillaCompat) + breakpointToggled = pyqtSignal(QsciScintillaCompat) + bookmarkToggled = pyqtSignal(QsciScintillaCompat) + syntaxerrorToggled = pyqtSignal(QsciScintillaCompat) + autoCompletionAPIsAvailable = pyqtSignal(bool) + coverageMarkersShown = pyqtSignal(bool) + taskMarkersUpdated = pyqtSignal(QsciScintillaCompat) + changeMarkersUpdated = pyqtSignal(QsciScintillaCompat) + showMenu = pyqtSignal(str, QMenu, QsciScintillaCompat) + languageChanged = pyqtSignal(str) + eolChanged = pyqtSignal(str) + encodingChanged = pyqtSignal(str) + spellLanguageChanged = pyqtSignal(str) + lastEditPositionAvailable = pyqtSignal() + refreshed = pyqtSignal() + settingsRead = pyqtSignal() + mouseDoubleClick = pyqtSignal(QPoint, int) + + WarningCode = 1 + WarningStyle = 2 + + # Autocompletion icon definitions + ClassID = 1 + ClassProtectedID = 2 + ClassPrivateID = 3 + MethodID = 4 + MethodProtectedID = 5 + MethodPrivateID = 6 + AttributeID = 7 + AttributeProtectedID = 8 + AttributePrivateID = 9 + EnumID = 10 + KeywordsID = 11 + ModuleID = 12 + + FromDocumentID = 99 + + TemplateImageID = 100 + + # Cooperation related definitions + Separator = "@@@" + + StartEditToken = "START_EDIT" + EndEditToken = "END_EDIT" + CancelEditToken = "CANCEL_EDIT" + RequestSyncToken = "REQUEST_SYNC" + SyncToken = "SYNC" + + VcsConflictMarkerLineRegExpList = ( + r"""^<<<<<<< .*?$""", + r"""^\|\|\|\|\|\|\| .*?$""", + r"""^=======.*?$""", + r"""^>>>>>>> .*?$""", + ) + + EncloseChars = { + '"': '"', + "'": "'", + "(": "()", + ")": "()", + "{": "{}", # __IGNORE_WARNING_M613__ + "}": "{}", # __IGNORE_WARNING_M613__ + "[": "[]", + "]": "[]", + "<": "<>", + ">": "<>", + } + + def __init__( + self, dbs, fn="", vm=None, filetype="", editor=None, tv=None, parent=None + ): + """ + Constructor + + @param dbs reference to the debug server object + @type DebugServer + @param fn name of the file to be opened. If it is None, a new (empty) + editor is opened. + @type str + @param vm reference to the view manager object + @type ViewManager + @param filetype type of the source file + @type str + @param editor reference to an Editor object, if this is a cloned view + @type Editor + @param tv reference to the task viewer object + @type TaskViewer + @param parent reference to the parent widget + @type QWidget + @exception OSError raised to indicate an issue accessing the file + """ + super().__init__(parent) + self.setAttribute(Qt.WidgetAttribute.WA_KeyCompression) + self.setUtf8(True) + + self.enableMultiCursorSupport() + + self.dbs = dbs + self.taskViewer = tv + self.__setFileName(fn) + self.vm = vm + self.filetype = filetype + self.filetypeByFlag = False + self.noName = "" + self.project = ericApp().getObject("Project") + + # clear some variables + self.lastHighlight = None # remember the last highlighted line + self.lastErrorMarker = None # remember the last error line + self.lastCurrMarker = None # remember the last current line + + self.breaks = {} + # key: marker handle, + # value: (lineno, condition, temporary, + # enabled, ignorecount) + self.bookmarks = [] + # bookmarks are just a list of handles to the + # bookmark markers + self.syntaxerrors = {} + # key: marker handle + # value: list of (error message, error index) + self.warnings = {} + # key: marker handle + # value: list of (warning message, warning type) + self.notcoveredMarkers = [] # just a list of marker handles + self.showingNotcoveredMarkers = False + + self.lexer_ = None + self.apiLanguage = "" + + self.__loadEditorConfig() + + self.__lexerReset = False + self.completer = None + self.encoding = self.__getEditorConfig("DefaultEncoding") + self.lastModified = 0 + self.line = -1 + self.inReopenPrompt = False + # true if the prompt to reload a changed source is present + self.inFileRenamed = False + # true if we are propagating a rename action + self.inLanguageChanged = False + # true if we are propagating a language change + self.inEolChanged = False + # true if we are propagating an eol change + self.inEncodingChanged = False + # true if we are propagating an encoding change + self.inDragDrop = False + # true if we are in drop mode + self.inLinesChanged = False + # true if we are propagating a lines changed event + self.__hasTaskMarkers = False + # no task markers present + + self.macros = {} # list of defined macros + self.curMacro = None + self.recording = False + + self.acAPI = False + + self.__lastEditPosition = None + self.__annotationLines = 0 + + self.__docstringGenerator = None + + # list of clones + self.__clones = [] + + # clear QScintilla defined keyboard commands + # we do our own handling through the view manager + self.clearAlternateKeys() + self.clearKeys() + + self.__markerMap = EditorMarkerMap(self) + + # initialize the mark occurrences timer + self.__markOccurrencesTimer = QTimer(self) + self.__markOccurrencesTimer.setSingleShot(True) + self.__markOccurrencesTimer.setInterval( + Preferences.getEditor("MarkOccurrencesTimeout") + ) + self.__markOccurrencesTimer.timeout.connect(self.__markOccurrences) + self.__markedText = "" + self.__searchIndicatorLines = [] + + # initialize some spellchecking stuff + self.spell = None + self.lastLine = 0 + self.lastIndex = 0 + self.__inSpellLanguageChanged = False + + # initialize some cooperation stuff + self.__isSyncing = False + self.__receivedWhileSyncing = [] + self.__savedText = "" + self.__inSharedEdit = False + self.__isShared = False + self.__inRemoteSharedEdit = False + + # connect signals before loading the text + self.modificationChanged.connect(self.__modificationChanged) + self.cursorPositionChanged.connect(self.__cursorPositionChanged) + self.modificationAttempted.connect(self.__modificationReadOnly) + + # define the margins markers + self.__changeMarkerSaved = self.markerDefine( + self.__createChangeMarkerPixmap("OnlineChangeTraceMarkerSaved") + ) + self.__changeMarkerUnsaved = self.markerDefine( + self.__createChangeMarkerPixmap("OnlineChangeTraceMarkerUnsaved") + ) + self.breakpoint = self.markerDefine(UI.PixmapCache.getPixmap("break")) + self.cbreakpoint = self.markerDefine(UI.PixmapCache.getPixmap("cBreak")) + self.tbreakpoint = self.markerDefine(UI.PixmapCache.getPixmap("tBreak")) + self.tcbreakpoint = self.markerDefine(UI.PixmapCache.getPixmap("tCBreak")) + self.dbreakpoint = self.markerDefine(UI.PixmapCache.getPixmap("breakDisabled")) + self.bookmark = self.markerDefine(UI.PixmapCache.getPixmap("bookmark16")) + self.syntaxerror = self.markerDefine(UI.PixmapCache.getPixmap("syntaxError")) + self.notcovered = self.markerDefine(UI.PixmapCache.getPixmap("notcovered")) + self.taskmarker = self.markerDefine(UI.PixmapCache.getPixmap("task")) + self.warning = self.markerDefine(UI.PixmapCache.getPixmap("warning")) + + # define the line markers + if Preferences.getEditor("LineMarkersBackground"): + self.currentline = self.markerDefine(QsciScintilla.MarkerSymbol.Background) + self.errorline = self.markerDefine(QsciScintilla.MarkerSymbol.Background) + self.__setLineMarkerColours() + else: + self.currentline = self.markerDefine( + UI.PixmapCache.getPixmap("currentLineMarker") + ) + self.errorline = self.markerDefine( + UI.PixmapCache.getPixmap("errorLineMarker") + ) + + self.breakpointMask = ( + (1 << self.breakpoint) + | (1 << self.cbreakpoint) + | (1 << self.tbreakpoint) + | (1 << self.tcbreakpoint) + | (1 << self.dbreakpoint) + ) + + self.changeMarkersMask = (1 << self.__changeMarkerSaved) | ( + 1 << self.__changeMarkerUnsaved + ) + + # configure the margins + self.__setMarginsDisplay() + self.linesChanged.connect(self.__resizeLinenoMargin) + + self.marginClicked.connect(self.__marginClicked) + + # set the eol mode + self.__setEolMode() + + # set the text display + self.__setTextDisplay() + + # initialize the online syntax check timer + try: + self.syntaxCheckService = ericApp().getObject("SyntaxCheckService") + self.syntaxCheckService.syntaxChecked.connect( + self.__processSyntaxCheckResult + ) + self.syntaxCheckService.error.connect(self.__processSyntaxCheckError) + self.__initOnlineSyntaxCheck() + except KeyError: + self.syntaxCheckService = None + + self.isResourcesFile = False + if editor is None: + if self.fileName: + if ( + pathlib.Path(self.fileName).stat().st_size // 1024 + ) > Preferences.getEditor("WarnFilesize"): + res = EricMessageBox.yesNo( + self, + self.tr("Open File"), + self.tr( + """<p>The size of the file <b>{0}</b>""" + """ is <b>{1} KB</b>.""" + """ Do you really want to load it?</p>""" + ).format( + self.fileName, + pathlib.Path(self.fileName).stat().st_size // 1024, + ), + icon=EricMessageBox.Warning, + ) + if not res: + raise OSError() + self.readFile(self.fileName, True) + self.__bindLexer(self.fileName) + self.__bindCompleter(self.fileName) + self.checkSyntax() + self.isResourcesFile = self.fileName.endswith(".qrc") + + self.__convertTabs() + + self.recolor() + else: + # clone the given editor + self.setDocument(editor.document()) + self.breaks = editor.breaks + self.bookmarks = editor.bookmarks + self.syntaxerrors = editor.syntaxerrors + self.notcoveredMarkers = editor.notcoveredMarkers + self.showingNotcoveredMarkers = editor.showingNotcoveredMarkers + self.isResourcesFile = editor.isResourcesFile + self.lastModified = editor.lastModified + + self.addClone(editor) + editor.addClone(self) + + self.gotoLine(1) + + # connect the mouse hover signals + self.SCN_DWELLSTART.connect(self.__showMouseHoverHelp) + self.SCN_DWELLEND.connect(self.__cancelMouseHoverHelp) + self.__mouseHoverHelp = None + self.__showingMouseHoverHelp = False + + # set the text display again + self.__setTextDisplay() + + # set the auto-completion function + self.__acContext = True + self.__acText = "" + self.__acCompletions = set() + self.__acCompletionsFinished = 0 + self.__acCache = EricCache( + size=Preferences.getEditor("AutoCompletionCacheSize") + ) + self.__acCache.setMaximumCacheTime( + Preferences.getEditor("AutoCompletionCacheTime") + ) + self.__acCacheEnabled = Preferences.getEditor("AutoCompletionCacheEnabled") + self.__acTimer = QTimer(self) + self.__acTimer.setSingleShot(True) + self.__acTimer.setInterval(Preferences.getEditor("AutoCompletionTimeout")) + self.__acTimer.timeout.connect(self.__autoComplete) + + self.__acWatchdog = QTimer(self) + self.__acWatchdog.setSingleShot(True) + self.__acWatchdog.setInterval( + Preferences.getEditor("AutoCompletionWatchdogTime") + ) + self.__acWatchdog.timeout.connect(self.autoCompleteQScintilla) + + self.userListActivated.connect(self.__completionListSelected) + self.SCN_CHARADDED.connect(self.__charAdded) + self.SCN_AUTOCCANCELLED.connect(self.__autocompletionCancelled) + + self.__completionListHookFunctions = {} + self.__completionListAsyncHookFunctions = {} + self.__setAutoCompletion() + + # set the call-tips function + self.__ctHookFunctions = {} + self.__setCallTips() + + # set the mouse click handlers (fired on mouse release) + self.__mouseClickHandlers = {} + # dictionary with tuple of keyboard modifier and mouse button as key + # and tuple of plug-in name and function as value + + sh = self.sizeHint() + if sh.height() < 300: + sh.setHeight(300) + self.resize(sh) + + # Make sure tabbing through a QWorkspace works. + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + + self.__updateReadOnly(True) + + self.setWhatsThis( + self.tr( + """<b>A Source Editor Window</b>""" + """<p>This window is used to display and edit a source file.""" + """ You can open as many of these as you like. The name of the""" + """ file is displayed in the window's titlebar.</p>""" + """<p>In order to set breakpoints just click in the space""" + """ between the line numbers and the fold markers. Via the""" + """ context menu of the margins they may be edited.</p>""" + """<p>In order to set bookmarks just Shift click in the space""" + """ between the line numbers and the fold markers.</p>""" + """<p>These actions can be reversed via the context menu.</p>""" + """<p>Ctrl clicking on a syntax error marker shows some info""" + """ about this error.</p>""" + ) + ) + + # Set the editors size, if it is too big for the view manager. + if self.vm is not None: + req = self.size() + bnd = req.boundedTo(self.vm.size()) + + if bnd.width() < req.width() or bnd.height() < req.height(): + self.resize(bnd) + + # set the autosave flag + self.autosaveEnabled = Preferences.getEditor("AutosaveInterval") > 0 + self.autosaveManuallyDisabled = False + + # code coverage related attributes + self.__coverageFile = "" + + self.__initContextMenu() + self.__initContextMenuMargins() + + self.__checkEol() + if editor is None: + self.__checkLanguage() + self.__checkEncoding() + self.__checkSpellLanguage() + else: + # it's a clone + self.__languageChanged(editor.apiLanguage, propagate=False) + self.__encodingChanged(editor.encoding, propagate=False) + self.__spellLanguageChanged(editor.getSpellingLanguage(), propagate=False) + # link the warnings to the original editor + self.warnings = editor.warnings + + self.setAcceptDrops(True) + + # breakpoint handling + self.breakpointModel = self.dbs.getBreakPointModel() + self.__restoreBreakpoints() + self.breakpointModel.rowsAboutToBeRemoved.connect(self.__deleteBreakPoints) + self.breakpointModel.dataAboutToBeChanged.connect( + self.__breakPointDataAboutToBeChanged + ) + self.breakpointModel.dataChanged.connect(self.__changeBreakPoints) + self.breakpointModel.rowsInserted.connect(self.__addBreakPoints) + self.SCN_MODIFIED.connect(self.__modified) + + # establish connection to some ViewManager action groups + self.addActions(self.vm.editorActGrp.actions()) + self.addActions(self.vm.editActGrp.actions()) + self.addActions(self.vm.copyActGrp.actions()) + self.addActions(self.vm.viewActGrp.actions()) + + # register images to be shown in autocompletion lists + self.__registerImages() + + # connect signals after loading the text + self.textChanged.connect(self.__textChanged) + + # initialize the online change trace timer + self.__initOnlineChangeTrace() + + if ( + self.fileName + and self.project.isOpen() + and self.project.isProjectSource(self.fileName) + ): + self.project.projectPropertiesChanged.connect( + self.__projectPropertiesChanged + ) + + self.grabGesture(Qt.GestureType.PinchGesture) + + self.SCN_ZOOM.connect(self.__markerMap.update) + self.__markerMap.update() + + def __setFileName(self, name): + """ + Private method to set the file name of the current file. + + @param name name of the current file + @type str + """ + self.fileName = name + + if self.fileName: + self.__fileNameExtension = os.path.splitext(self.fileName)[1][1:].lower() + else: + self.__fileNameExtension = "" + + def __registerImages(self): + """ + Private method to register images for autocompletion lists. + """ + # finale size of the completion images + imageSize = QSize(22, 22) + + self.registerImage(self.ClassID, UI.PixmapCache.getPixmap("class", imageSize)) + self.registerImage( + self.ClassProtectedID, + UI.PixmapCache.getPixmap("class_protected", imageSize), + ) + self.registerImage( + self.ClassPrivateID, UI.PixmapCache.getPixmap("class_private", imageSize) + ) + self.registerImage(self.MethodID, UI.PixmapCache.getPixmap("method", imageSize)) + self.registerImage( + self.MethodProtectedID, + UI.PixmapCache.getPixmap("method_protected", imageSize), + ) + self.registerImage( + self.MethodPrivateID, UI.PixmapCache.getPixmap("method_private", imageSize) + ) + self.registerImage( + self.AttributeID, UI.PixmapCache.getPixmap("attribute", imageSize) + ) + self.registerImage( + self.AttributeProtectedID, + UI.PixmapCache.getPixmap("attribute_protected", imageSize), + ) + self.registerImage( + self.AttributePrivateID, + UI.PixmapCache.getPixmap("attribute_private", imageSize), + ) + self.registerImage(self.EnumID, UI.PixmapCache.getPixmap("enum", imageSize)) + self.registerImage( + self.KeywordsID, UI.PixmapCache.getPixmap("keywords", imageSize) + ) + self.registerImage(self.ModuleID, UI.PixmapCache.getPixmap("module", imageSize)) + + self.registerImage( + self.FromDocumentID, UI.PixmapCache.getPixmap("editor", imageSize) + ) + + self.registerImage( + self.TemplateImageID, UI.PixmapCache.getPixmap("templateViewer", imageSize) + ) + + def addClone(self, editor): + """ + Public method to add a clone to our list. + + @param editor reference to the cloned editor + @type Editor + """ + self.__clones.append(editor) + + editor.editorRenamed.connect(self.fileRenamed) + editor.languageChanged.connect(self.languageChanged) + editor.eolChanged.connect(self.__eolChanged) + editor.encodingChanged.connect(self.__encodingChanged) + editor.spellLanguageChanged.connect(self.__spellLanguageChanged) + + def removeClone(self, editor): + """ + Public method to remove a clone from our list. + + @param editor reference to the cloned editor + @type Editor + """ + if editor in self.__clones: + editor.editorRenamed.disconnect(self.fileRenamed) + editor.languageChanged.disconnect(self.languageChanged) + editor.eolChanged.disconnect(self.__eolChanged) + editor.encodingChanged.disconnect(self.__encodingChanged) + editor.spellLanguageChanged.disconnect(self.__spellLanguageChanged) + self.__clones.remove(editor) + + def isClone(self, editor): + """ + Public method to test, if the given editor is a clone. + + @param editor reference to the cloned editor + @type Editor + @return flag indicating a clone + @rtype bool + """ + return editor in self.__clones + + def __bindName(self, line0): + """ + Private method to generate a dummy filename for binding a lexer. + + @param line0 first line of text to use in the generation process + (string) + @return dummy file name to be used for binding a lexer (string) + """ + bindName = "" + line0 = line0.lower() + + # check first line if it does not start with #! + if line0.startswith(("<html", "<!doctype html", "<?php")): + bindName = "dummy.html" + elif line0.startswith(("<?xml", "<!doctype")): + bindName = "dummy.xml" + elif line0.startswith("index: "): + bindName = "dummy.diff" + elif line0.startswith("\\documentclass"): + bindName = "dummy.tex" + + if not bindName and self.filetype: + # check filetype + from . import Lexers + + supportedLanguages = Lexers.getSupportedLanguages() + if self.filetype in supportedLanguages: + bindName = supportedLanguages[self.filetype][1] + elif self.filetype in ["Python", "Python3", "MicroPython"]: + bindName = "dummy.py" + + if not bindName and line0.startswith("#!"): + # #! marker detection + if ( + "python3" in line0 + or "python" in line0 + or "pypy3" in line0 + or "pypy" in line0 + ): + bindName = "dummy.py" + self.filetype = "Python3" + elif "/bash" in line0 or "/sh" in line0: + bindName = "dummy.sh" + elif "ruby" in line0: + bindName = "dummy.rb" + self.filetype = "Ruby" + elif "perl" in line0: + bindName = "dummy.pl" + elif "lua" in line0: + bindName = "dummy.lua" + elif "dmd" in line0: + bindName = "dummy.d" + self.filetype = "D" + + if not bindName: + # mode line detection: -*- mode: python -*- + match = re.search(r"mode[:=]\s*([-\w_.]+)", line0) + if match: + mode = match.group(1).lower() + if mode in ["python3", "pypy3"]: + bindName = "dummy.py" + self.filetype = "Python3" + elif mode == "ruby": + bindName = "dummy.rb" + self.filetype = "Ruby" + elif mode == "perl": + bindName = "dummy.pl" + elif mode == "lua": + bindName = "dummy.lua" + elif mode in ["dmd", "d"]: + bindName = "dummy.d" + self.filetype = "D" + + if not bindName: + bindName = self.fileName + + return bindName + + def getMenu(self, menuName): + """ + Public method to get a reference to the main context menu or a submenu. + + @param menuName name of the menu (string) + @return reference to the requested menu (QMenu) or None + """ + try: + return self.__menus[menuName] + except KeyError: + return None + + def hasMiniMenu(self): + """ + Public method to check the miniMenu flag. + + @return flag indicating a minimized context menu (boolean) + """ + return self.miniMenu + + def __initContextMenu(self): + """ + Private method used to setup the context menu. + """ + self.miniMenu = Preferences.getEditor("MiniContextMenu") + + self.menuActs = {} + self.menu = QMenu() + self.__menus = { + "Main": self.menu, + } + + self.languagesMenu = self.__initContextMenuLanguages() + self.__menus["Languages"] = self.languagesMenu + if self.isResourcesFile: + self.resourcesMenu = self.__initContextMenuResources() + self.__menus["Resources"] = self.resourcesMenu + else: + self.checksMenu = self.__initContextMenuChecks() + self.menuShow = self.__initContextMenuShow() + self.graphicsMenu = self.__initContextMenuGraphics() + self.autocompletionMenu = self.__initContextMenuAutocompletion() + self.codeFormattingMenu = self.__initContextMenuFormatting() + self.__menus["Checks"] = self.checksMenu + self.__menus["Show"] = self.menuShow + self.__menus["Graphics"] = self.graphicsMenu + self.__menus["Autocompletion"] = self.autocompletionMenu + self.__menus["Formatting"] = self.codeFormattingMenu + self.toolsMenu = self.__initContextMenuTools() + self.__menus["Tools"] = self.toolsMenu + self.eolMenu = self.__initContextMenuEol() + self.__menus["Eol"] = self.eolMenu + self.encodingsMenu = self.__initContextMenuEncodings() + self.__menus["Encodings"] = self.encodingsMenu + self.spellCheckMenu = self.__initContextMenuSpellCheck() + self.__menus["SpellCheck"] = self.spellCheckMenu + + self.menuActs["Undo"] = self.menu.addAction( + UI.PixmapCache.getIcon("editUndo"), self.tr("Undo"), self.undo + ) + self.menuActs["Redo"] = self.menu.addAction( + UI.PixmapCache.getIcon("editRedo"), self.tr("Redo"), self.redo + ) + self.menuActs["Revert"] = self.menu.addAction( + self.tr("Revert to last saved state"), self.revertToUnmodified + ) + self.menu.addSeparator() + self.menuActs["Cut"] = self.menu.addAction( + UI.PixmapCache.getIcon("editCut"), self.tr("Cut"), self.cut + ) + self.menuActs["Copy"] = self.menu.addAction( + UI.PixmapCache.getIcon("editCopy"), self.tr("Copy"), self.copy + ) + self.menuActs["Paste"] = self.menu.addAction( + UI.PixmapCache.getIcon("editPaste"), self.tr("Paste"), self.paste + ) + if not self.miniMenu: + self.menu.addSeparator() + self.menu.addAction( + UI.PixmapCache.getIcon("editIndent"), + self.tr("Indent"), + self.indentLineOrSelection, + ) + self.menu.addAction( + UI.PixmapCache.getIcon("editUnindent"), + self.tr("Unindent"), + self.unindentLineOrSelection, + ) + self.menuActs["Comment"] = self.menu.addAction( + UI.PixmapCache.getIcon("editComment"), + self.tr("Comment"), + self.commentLineOrSelection, + ) + self.menuActs["Uncomment"] = self.menu.addAction( + UI.PixmapCache.getIcon("editUncomment"), + self.tr("Uncomment"), + self.uncommentLineOrSelection, + ) + self.menu.addSeparator() + self.menuActs["Docstring"] = self.menu.addAction( + self.tr("Generate Docstring"), self.__insertDocstring + ) + self.menu.addSeparator() + self.menu.addAction(self.tr("Select to brace"), self.selectToMatchingBrace) + self.menu.addAction(self.tr("Select all"), self.__selectAll) + self.menu.addAction(self.tr("Deselect all"), self.__deselectAll) + self.menuActs["ExecuteSelection"] = self.menu.addAction( + self.tr("Execute Selection In Console"), self.__executeSelection + ) + else: + self.menuActs["ExecuteSelection"] = None + self.menu.addSeparator() + self.menu.addMenu(self.spellCheckMenu) + self.menu.addSeparator() + self.menuActs["Languages"] = self.menu.addMenu(self.languagesMenu) + self.menuActs["Encodings"] = self.menu.addMenu(self.encodingsMenu) + self.menuActs["Eol"] = self.menu.addMenu(self.eolMenu) + self.menu.addSeparator() + self.menuActs["MonospacedFont"] = self.menu.addAction( + self.tr("Use Monospaced Font"), self.handleMonospacedEnable + ) + self.menuActs["MonospacedFont"].setCheckable(True) + self.menuActs["MonospacedFont"].setChecked(self.useMonospaced) + self.menuActs["AutosaveEnable"] = self.menu.addAction( + self.tr("Autosave enabled"), self.__autosaveEnable + ) + self.menuActs["AutosaveEnable"].setCheckable(True) + self.menuActs["AutosaveEnable"].setChecked(self.autosaveEnabled) + self.menuActs["TypingAidsEnabled"] = self.menu.addAction( + self.tr("Typing aids enabled"), self.__toggleTypingAids + ) + self.menuActs["TypingAidsEnabled"].setCheckable(True) + self.menuActs["TypingAidsEnabled"].setEnabled(self.completer is not None) + self.menuActs["TypingAidsEnabled"].setChecked( + self.completer is not None and self.completer.isEnabled() + ) + self.menuActs["AutoCompletionEnable"] = self.menu.addAction( + self.tr("Automatic Completion enabled"), self.__toggleAutoCompletionEnable + ) + self.menuActs["AutoCompletionEnable"].setCheckable(True) + self.menuActs["AutoCompletionEnable"].setChecked( + self.autoCompletionThreshold() != -1 + ) + if not self.isResourcesFile: + self.menu.addMenu(self.autocompletionMenu) + self.menuActs["calltip"] = self.menu.addAction( + self.tr("Calltip"), self.callTip + ) + self.menuActs["codeInfo"] = self.menu.addAction( + self.tr("Code Info"), self.__showCodeInfo + ) + self.menu.addSeparator() + if self.isResourcesFile: + self.menu.addMenu(self.resourcesMenu) + else: + self.menuActs["Check"] = self.menu.addMenu(self.checksMenu) + self.menuActs["Formatting"] = self.menu.addMenu(self.codeFormattingMenu) + self.menuActs["Show"] = self.menu.addMenu(self.menuShow) + self.menuActs["Diagrams"] = self.menu.addMenu(self.graphicsMenu) + self.menu.addSeparator() + self.menuActs["Tools"] = self.menu.addMenu(self.toolsMenu) + self.menu.addSeparator() + self.menu.addAction( + UI.PixmapCache.getIcon("documentNewView"), + self.tr("New Document View"), + self.__newView, + ) + self.menuActs["NewSplit"] = self.menu.addAction( + UI.PixmapCache.getIcon("splitVertical"), + self.tr("New Document View (with new split)"), + self.__newViewNewSplit, + ) + self.menuActs["NewSplit"].setEnabled(self.vm.canSplit()) + self.menu.addSeparator() + self.reopenEncodingMenu = self.__initContextMenuReopenWithEncoding() + self.menuActs["Reopen"] = self.menu.addMenu(self.reopenEncodingMenu) + self.menuActs["Save"] = self.menu.addAction( + UI.PixmapCache.getIcon("fileSave"), self.tr("Save"), self.__contextSave + ) + self.menu.addAction( + UI.PixmapCache.getIcon("fileSaveAs"), + self.tr("Save As..."), + self.__contextSaveAs, + ) + self.menu.addAction( + UI.PixmapCache.getIcon("fileSaveCopy"), + self.tr("Save Copy..."), + self.__contextSaveCopy, + ) + + self.menu.aboutToShow.connect(self.__showContextMenu) + + self.spellingMenu = QMenu() + self.__menus["Spelling"] = self.spellingMenu + + self.spellingMenu.aboutToShow.connect(self.__showContextMenuSpelling) + self.spellingMenu.triggered.connect(self.__contextMenuSpellingTriggered) + + def __initContextMenuAutocompletion(self): + """ + Private method used to setup the Checks context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + menu = QMenu(self.tr("Complete")) + + self.menuActs["acDynamic"] = menu.addAction( + self.tr("Complete"), self.autoComplete + ) + menu.addSeparator() + self.menuActs["acClearCache"] = menu.addAction( + self.tr("Clear Completions Cache"), self.__clearCompletionsCache + ) + menu.addSeparator() + menu.addAction(self.tr("Complete from Document"), self.autoCompleteFromDocument) + self.menuActs["acAPI"] = menu.addAction( + self.tr("Complete from APIs"), self.autoCompleteFromAPIs + ) + self.menuActs["acAPIDocument"] = menu.addAction( + self.tr("Complete from Document and APIs"), self.autoCompleteFromAll + ) + + menu.aboutToShow.connect(self.__showContextMenuAutocompletion) + + return menu + + def __initContextMenuChecks(self): + """ + Private method used to setup the Checks context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + menu = QMenu(self.tr("Check")) + menu.aboutToShow.connect(self.__showContextMenuChecks) + return menu + + def __initContextMenuFormatting(self): + """ + Private method used to setup the Code Formatting context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + menu = QMenu(self.tr("Code Formatting")) + + menu.addAction( + self.tr("Format Code"), + lambda: self.__performFormatWithBlack(BlackFormattingAction.Format), + ) + menu.addAction( + self.tr("Check Formatting"), + lambda: self.__performFormatWithBlack(BlackFormattingAction.Check), + ) + menu.addAction( + self.tr("Formatting Diff"), + lambda: self.__performFormatWithBlack(BlackFormattingAction.Diff), + ) + + menu.aboutToShow.connect(self.__showContextMenuFormatting) + + return menu + + def __initContextMenuTools(self): + """ + Private method used to setup the Tools context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + menu = QMenu(self.tr("Tools")) + menu.aboutToShow.connect(self.__showContextMenuTools) + return menu + + def __initContextMenuShow(self): + """ + Private method used to setup the Show context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + menu = QMenu(self.tr("Show")) + + menu.addAction(self.tr("Code metrics..."), self.__showCodeMetrics) + self.coverageMenuAct = menu.addAction( + self.tr("Code coverage..."), self.__showCodeCoverage + ) + self.coverageShowAnnotationMenuAct = menu.addAction( + self.tr("Show code coverage annotations"), self.codeCoverageShowAnnotations + ) + self.coverageHideAnnotationMenuAct = menu.addAction( + self.tr("Hide code coverage annotations"), + self.__codeCoverageHideAnnotations, + ) + self.profileMenuAct = menu.addAction( + self.tr("Profile data..."), self.__showProfileData + ) + + menu.aboutToShow.connect(self.__showContextMenuShow) + + return menu + + def __initContextMenuGraphics(self): + """ + Private method used to setup the diagrams context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + menu = QMenu(self.tr("Diagrams")) + + menu.addAction(self.tr("Class Diagram..."), self.__showClassDiagram) + menu.addAction(self.tr("Package Diagram..."), self.__showPackageDiagram) + menu.addAction(self.tr("Imports Diagram..."), self.__showImportsDiagram) + self.applicationDiagramMenuAct = menu.addAction( + self.tr("Application Diagram..."), self.__showApplicationDiagram + ) + menu.addSeparator() + menu.addAction( + UI.PixmapCache.getIcon("open"), + self.tr("Load Diagram..."), + self.__loadDiagram, + ) + + menu.aboutToShow.connect(self.__showContextMenuGraphics) + + return menu + + def __initContextMenuLanguages(self): + """ + Private method used to setup the Languages context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + menu = QMenu(self.tr("Languages")) + + self.languagesActGrp = QActionGroup(self) + self.noLanguageAct = menu.addAction( + UI.PixmapCache.getIcon("fileText"), self.tr("Text") + ) + self.noLanguageAct.setCheckable(True) + self.noLanguageAct.setData("None") + self.languagesActGrp.addAction(self.noLanguageAct) + menu.addSeparator() + + from . import Lexers + + self.supportedLanguages = {} + supportedLanguages = Lexers.getSupportedLanguages() + languages = sorted(supportedLanguages.keys()) + for language in languages: + if language != "Guessed": + self.supportedLanguages[language] = supportedLanguages[language][:2] + act = menu.addAction( + UI.PixmapCache.getIcon(supportedLanguages[language][2]), + self.supportedLanguages[language][0], + ) + act.setCheckable(True) + act.setData(language) + self.supportedLanguages[language].append(act) + self.languagesActGrp.addAction(act) + + menu.addSeparator() + self.pygmentsAct = menu.addAction(self.tr("Guessed")) + self.pygmentsAct.setCheckable(True) + self.pygmentsAct.setData("Guessed") + self.languagesActGrp.addAction(self.pygmentsAct) + self.pygmentsSelAct = menu.addAction(self.tr("Alternatives")) + self.pygmentsSelAct.setData("Alternatives") + + menu.triggered.connect(self.__languageMenuTriggered) + menu.aboutToShow.connect(self.__showContextMenuLanguages) + + return menu + + def __initContextMenuEncodings(self): + """ + Private method used to setup the Encodings context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + self.supportedEncodings = {} + + menu = QMenu(self.tr("Encodings")) + + self.encodingsActGrp = QActionGroup(self) + + for encoding in sorted(Utilities.supportedCodecs): + act = menu.addAction(encoding) + act.setCheckable(True) + act.setData(encoding) + self.supportedEncodings[encoding] = act + self.encodingsActGrp.addAction(act) + + menu.triggered.connect(self.__encodingsMenuTriggered) + menu.aboutToShow.connect(self.__showContextMenuEncodings) + + return menu + + def __initContextMenuReopenWithEncoding(self): + """ + Private method used to setup the Reopen With Encoding context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + menu = QMenu(self.tr("Re-Open With Encoding")) + menu.setIcon(UI.PixmapCache.getIcon("open")) + + for encoding in sorted(Utilities.supportedCodecs): + act = menu.addAction(encoding) + act.setData(encoding) + + menu.triggered.connect(self.__reopenWithEncodingMenuTriggered) + + return menu + + def __initContextMenuEol(self): + """ + Private method to setup the eol context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + self.supportedEols = {} + + menu = QMenu(self.tr("End-of-Line Type")) + + self.eolActGrp = QActionGroup(self) + + act = menu.addAction(UI.PixmapCache.getIcon("eolLinux"), self.tr("Unix")) + act.setCheckable(True) + act.setData("\n") + self.supportedEols["\n"] = act + self.eolActGrp.addAction(act) + + act = menu.addAction(UI.PixmapCache.getIcon("eolWindows"), self.tr("Windows")) + act.setCheckable(True) + act.setData("\r\n") + self.supportedEols["\r\n"] = act + self.eolActGrp.addAction(act) + + act = menu.addAction(UI.PixmapCache.getIcon("eolMac"), self.tr("Macintosh")) + act.setCheckable(True) + act.setData("\r") + self.supportedEols["\r"] = act + self.eolActGrp.addAction(act) + + menu.triggered.connect(self.__eolMenuTriggered) + menu.aboutToShow.connect(self.__showContextMenuEol) + + return menu + + def __initContextMenuSpellCheck(self): + """ + Private method used to setup the spell checking context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + menu = QMenu(self.tr("Spelling")) + menu.setIcon(UI.PixmapCache.getIcon("spellchecking")) + + self.spellLanguagesMenu = self.__initContextMenuSpellLanguages() + self.__menus["SpellLanguages"] = self.spellLanguagesMenu + + self.menuActs["SpellCheck"] = menu.addAction( + UI.PixmapCache.getIcon("spellchecking"), + self.tr("Check spelling..."), + self.checkSpelling, + ) + self.menuActs["SpellCheckSelection"] = menu.addAction( + UI.PixmapCache.getIcon("spellchecking"), + self.tr("Check spelling of selection..."), + self.__checkSpellingSelection, + ) + self.menuActs["SpellCheckRemove"] = menu.addAction( + self.tr("Remove from dictionary"), self.__removeFromSpellingDictionary + ) + self.menuActs["SpellCheckLanguages"] = menu.addMenu(self.spellLanguagesMenu) + + menu.aboutToShow.connect(self.__showContextMenuSpellCheck) + + return menu + + def __initContextMenuSpellLanguages(self): + """ + Private method to setup the spell checking languages context sub menu. + + @return reference to the generated menu + @rtype QMenu + """ + self.supportedSpellLanguages = {} + + menu = QMenu(self.tr("Spell Check Languages")) + + self.spellLanguagesActGrp = QActionGroup(self) + + self.noSpellLanguageAct = menu.addAction(self.tr("No Language")) + self.noSpellLanguageAct.setCheckable(True) + self.noSpellLanguageAct.setData("") + self.spellLanguagesActGrp.addAction(self.noSpellLanguageAct) + menu.addSeparator() + + for language in sorted(SpellChecker.getAvailableLanguages()): + act = menu.addAction(language) + act.setCheckable(True) + act.setData(language) + self.supportedSpellLanguages[language] = act + self.spellLanguagesActGrp.addAction(act) + + menu.triggered.connect(self.__spellLanguagesMenuTriggered) + menu.aboutToShow.connect(self.__showContextMenuSpellLanguages) + + return menu + + def __initContextMenuMargins(self): + """ + Private method used to setup the context menu for the margins. + """ + self.marginMenuActs = {} + + # bookmark margin + self.bmMarginMenu = QMenu() + + self.bmMarginMenu.addAction(self.tr("Toggle bookmark"), self.menuToggleBookmark) + self.marginMenuActs["NextBookmark"] = self.bmMarginMenu.addAction( + self.tr("Next bookmark"), self.nextBookmark + ) + self.marginMenuActs["PreviousBookmark"] = self.bmMarginMenu.addAction( + self.tr("Previous bookmark"), self.previousBookmark + ) + self.marginMenuActs["ClearBookmark"] = self.bmMarginMenu.addAction( + self.tr("Clear all bookmarks"), self.clearBookmarks + ) + + self.bmMarginMenu.aboutToShow.connect( + lambda: self.__showContextMenuMargin(self.bmMarginMenu) + ) + + # breakpoint margin + self.bpMarginMenu = QMenu() + + self.marginMenuActs["Breakpoint"] = self.bpMarginMenu.addAction( + self.tr("Toggle breakpoint"), self.menuToggleBreakpoint + ) + self.marginMenuActs["TempBreakpoint"] = self.bpMarginMenu.addAction( + self.tr("Toggle temporary breakpoint"), self.__menuToggleTemporaryBreakpoint + ) + self.marginMenuActs["EditBreakpoint"] = self.bpMarginMenu.addAction( + self.tr("Edit breakpoint..."), self.menuEditBreakpoint + ) + self.marginMenuActs["EnableBreakpoint"] = self.bpMarginMenu.addAction( + self.tr("Enable breakpoint"), self.__menuToggleBreakpointEnabled + ) + self.marginMenuActs["NextBreakpoint"] = self.bpMarginMenu.addAction( + self.tr("Next breakpoint"), self.menuNextBreakpoint + ) + self.marginMenuActs["PreviousBreakpoint"] = self.bpMarginMenu.addAction( + self.tr("Previous breakpoint"), self.menuPreviousBreakpoint + ) + self.marginMenuActs["ClearBreakpoint"] = self.bpMarginMenu.addAction( + self.tr("Clear all breakpoints"), self.__menuClearBreakpoints + ) + + self.bpMarginMenu.aboutToShow.connect( + lambda: self.__showContextMenuMargin(self.bpMarginMenu) + ) + + # fold margin + self.foldMarginMenu = QMenu() + + self.marginMenuActs["ToggleAllFolds"] = self.foldMarginMenu.addAction( + self.tr("Toggle all folds"), self.foldAll + ) + self.marginMenuActs[ + "ToggleAllFoldsAndChildren" + ] = self.foldMarginMenu.addAction( + self.tr("Toggle all folds (including children)"), lambda: self.foldAll(True) + ) + self.marginMenuActs["ToggleCurrentFold"] = self.foldMarginMenu.addAction( + self.tr("Toggle current fold"), self.toggleCurrentFold + ) + self.foldMarginMenu.addSeparator() + self.marginMenuActs["ExpandChildren"] = self.foldMarginMenu.addAction( + self.tr("Expand (including children)"), + self.__contextMenuExpandFoldWithChildren, + ) + self.marginMenuActs["CollapseChildren"] = self.foldMarginMenu.addAction( + self.tr("Collapse (including children)"), + self.__contextMenuCollapseFoldWithChildren, + ) + self.foldMarginMenu.addSeparator() + self.marginMenuActs["ClearAllFolds"] = self.foldMarginMenu.addAction( + self.tr("Clear all folds"), self.clearFolds + ) + + self.foldMarginMenu.aboutToShow.connect( + lambda: self.__showContextMenuMargin(self.foldMarginMenu) + ) + + # indicator margin + self.indicMarginMenu = QMenu() + + self.marginMenuActs["GotoSyntaxError"] = self.indicMarginMenu.addAction( + self.tr("Goto syntax error"), self.gotoSyntaxError + ) + self.marginMenuActs["ShowSyntaxError"] = self.indicMarginMenu.addAction( + self.tr("Show syntax error message"), self.__showSyntaxError + ) + self.marginMenuActs["ClearSyntaxError"] = self.indicMarginMenu.addAction( + self.tr("Clear syntax error"), self.clearSyntaxError + ) + self.indicMarginMenu.addSeparator() + self.marginMenuActs["NextWarningMarker"] = self.indicMarginMenu.addAction( + self.tr("Next warning"), self.nextWarning + ) + self.marginMenuActs["PreviousWarningMarker"] = self.indicMarginMenu.addAction( + self.tr("Previous warning"), self.previousWarning + ) + self.marginMenuActs["ShowWarning"] = self.indicMarginMenu.addAction( + self.tr("Show warning message"), self.__showWarning + ) + self.marginMenuActs["ClearWarnings"] = self.indicMarginMenu.addAction( + self.tr("Clear warnings"), self.clearWarnings + ) + self.indicMarginMenu.addSeparator() + self.marginMenuActs["NextCoverageMarker"] = self.indicMarginMenu.addAction( + self.tr("Next uncovered line"), self.nextUncovered + ) + self.marginMenuActs["PreviousCoverageMarker"] = self.indicMarginMenu.addAction( + self.tr("Previous uncovered line"), self.previousUncovered + ) + self.indicMarginMenu.addSeparator() + self.marginMenuActs["NextTaskMarker"] = self.indicMarginMenu.addAction( + self.tr("Next task"), self.nextTask + ) + self.marginMenuActs["PreviousTaskMarker"] = self.indicMarginMenu.addAction( + self.tr("Previous task"), self.previousTask + ) + self.indicMarginMenu.addSeparator() + self.marginMenuActs["NextChangeMarker"] = self.indicMarginMenu.addAction( + self.tr("Next change"), self.nextChange + ) + self.marginMenuActs["PreviousChangeMarker"] = self.indicMarginMenu.addAction( + self.tr("Previous change"), self.previousChange + ) + self.marginMenuActs["ClearChangeMarkers"] = self.indicMarginMenu.addAction( + self.tr("Clear changes"), self.__reinitOnlineChangeTrace + ) + + self.indicMarginMenu.aboutToShow.connect( + lambda: self.__showContextMenuMargin(self.indicMarginMenu) + ) + + def exportFile(self, exporterFormat): + """ + Public method to export the file. + + @param exporterFormat format the file should be exported into (string) + """ + if exporterFormat: + from . import Exporters + + exporter = Exporters.getExporter(exporterFormat, self) + if exporter: + exporter.exportSource() + else: + EricMessageBox.critical( + self, + self.tr("Export source"), + self.tr( + """<p>No exporter available for the """ + """export format <b>{0}</b>. Aborting...</p>""" + ).format(exporterFormat), + ) + else: + EricMessageBox.critical( + self, + self.tr("Export source"), + self.tr("""No export format given. Aborting..."""), + ) + + def __showContextMenuLanguages(self): + """ + Private slot handling the aboutToShow signal of the languages context + menu. + """ + if self.apiLanguage.startswith("Pygments|"): + self.pygmentsSelAct.setText( + self.tr("Alternatives ({0})").format(self.getLanguage(normalized=False)) + ) + else: + self.pygmentsSelAct.setText(self.tr("Alternatives")) + self.showMenu.emit("Languages", self.languagesMenu, self) + + def __selectPygmentsLexer(self): + """ + Private method to select a specific pygments lexer. + + @return name of the selected pygments lexer (string) + """ + from pygments.lexers import get_all_lexers + + lexerList = sorted(lex[0] for lex in get_all_lexers()) + try: + lexerSel = lexerList.index( + self.getLanguage(normalized=False, forPygments=True) + ) + except ValueError: + lexerSel = 0 + lexerName, ok = QInputDialog.getItem( + self, + self.tr("Pygments Lexer"), + self.tr("Select the Pygments lexer to apply."), + lexerList, + lexerSel, + False, + ) + if ok and lexerName: + return lexerName + else: + return "" + + def __languageMenuTriggered(self, act): + """ + Private method to handle the selection of a lexer language. + + @param act reference to the action that was triggered (QAction) + """ + if act == self.noLanguageAct: + self.__resetLanguage() + elif act == self.pygmentsAct: + self.setLanguage("dummy.pygments") + elif act == self.pygmentsSelAct: + language = self.__selectPygmentsLexer() + if language: + self.setLanguage("dummy.pygments", pyname=language) + else: + language = act.data() + if language: + self.filetype = language + self.setLanguage(self.supportedLanguages[language][1]) + self.checkSyntax() + + self.__docstringGenerator = None + + def __languageChanged(self, language, propagate=True): + """ + Private slot handling a change of a connected editor's language. + + @param language language to be set (string) + @param propagate flag indicating to propagate the change (boolean) + """ + if language == "": + self.__resetLanguage(propagate=propagate) + elif language == "Guessed": + self.setLanguage("dummy.pygments", propagate=propagate) + elif language.startswith("Pygments|"): + pyname = language.split("|", 1)[1] + self.setLanguage("dummy.pygments", pyname=pyname, propagate=propagate) + else: + self.filetype = language + self.setLanguage(self.supportedLanguages[language][1], propagate=propagate) + self.checkSyntax() + + self.__docstringGenerator = None + + def __resetLanguage(self, propagate=True): + """ + Private method used to reset the language selection. + + @param propagate flag indicating to propagate the change (boolean) + """ + if self.lexer_ is not None and ( + self.lexer_.lexer() == "container" or self.lexer_.lexer() is None + ): + with contextlib.suppress(TypeError): + self.SCN_STYLENEEDED.disconnect(self.__styleNeeded) + + self.apiLanguage = "" + self.lexer_ = None + self.__lexerReset = True + self.setLexer() + if self.completer is not None: + self.completer.setEnabled(False) + self.completer = None + useMonospaced = self.useMonospaced + self.__setTextDisplay() + self.__setMarginsDisplay() + self.setMonospaced(useMonospaced) + with contextlib.suppress(AttributeError): + self.menuActs["MonospacedFont"].setChecked(self.useMonospaced) + + self.__docstringGenerator = None + + if not self.inLanguageChanged and propagate: + self.inLanguageChanged = True + self.languageChanged.emit(self.apiLanguage) + self.inLanguageChanged = False + + def setLanguage(self, filename, initTextDisplay=True, propagate=True, pyname=""): + """ + Public method to set a lexer language. + + @param filename filename used to determine the associated lexer + language (string) + @param initTextDisplay flag indicating an initialization of the text + display is required as well (boolean) + @param propagate flag indicating to propagate the change (boolean) + @param pyname name of the pygments lexer to use (string) + """ + # clear all warning and syntax error markers + self.clearSyntaxError() + self.clearWarnings() + + self.menuActs["MonospacedFont"].setChecked(False) + + self.__lexerReset = False + self.__bindLexer(filename, pyname=pyname) + self.__bindCompleter(filename) + self.recolor() + self.__checkLanguage() + + self.__docstringGenerator = None + + # set the text display + if initTextDisplay: + self.__setTextDisplay() + + # set the auto-completion and call-tips function + self.__setAutoCompletion() + self.__setCallTips() + + if not self.inLanguageChanged and propagate: + self.inLanguageChanged = True + self.languageChanged.emit(self.apiLanguage) + self.inLanguageChanged = False + + def __checkLanguage(self): + """ + Private method to check the selected language of the language submenu. + """ + if self.apiLanguage == "": + self.noLanguageAct.setChecked(True) + elif self.apiLanguage == "Guessed": + self.pygmentsAct.setChecked(True) + elif self.apiLanguage.startswith("Pygments|"): + act = self.languagesActGrp.checkedAction() + if act: + act.setChecked(False) + else: + self.supportedLanguages[self.apiLanguage][2].setChecked(True) + + def projectLexerAssociationsChanged(self): + """ + Public slot to handle changes of the project lexer associations. + """ + self.setLanguage(self.fileName) + + def __showContextMenuEncodings(self): + """ + Private slot handling the aboutToShow signal of the encodings context + menu. + """ + self.showMenu.emit("Encodings", self.encodingsMenu, self) + + def __encodingsMenuTriggered(self, act): + """ + Private method to handle the selection of an encoding. + + @param act reference to the action that was triggered (QAction) + """ + encoding = act.data() + self.setModified(True) + self.__encodingChanged("{0}-selected".format(encoding)) + + def __checkEncoding(self): + """ + Private method to check the selected encoding of the encodings submenu. + """ + with contextlib.suppress(AttributeError, KeyError): + (self.supportedEncodings[self.__normalizedEncoding()].setChecked(True)) + + def __encodingChanged(self, encoding, propagate=True): + """ + Private slot to handle a change of the encoding. + + @param encoding changed encoding (string) + @param propagate flag indicating to propagate the change (boolean) + """ + self.encoding = encoding + self.__checkEncoding() + + if not self.inEncodingChanged and propagate: + self.inEncodingChanged = True + self.encodingChanged.emit(self.encoding) + self.inEncodingChanged = False + + def __normalizedEncoding(self, encoding=""): + """ + Private method to calculate the normalized encoding string. + + @param encoding encoding to be normalized (string) + @return normalized encoding (string) + """ + if not encoding: + encoding = self.encoding + return ( + encoding.replace("-default", "") + .replace("-guessed", "") + .replace("-selected", "") + ) + + def __showContextMenuEol(self): + """ + Private slot handling the aboutToShow signal of the eol context menu. + """ + self.showMenu.emit("Eol", self.eolMenu, self) + + def __eolMenuTriggered(self, act): + """ + Private method to handle the selection of an eol type. + + @param act reference to the action that was triggered (QAction) + """ + eol = act.data() + self.setEolModeByEolString(eol) + self.convertEols(self.eolMode()) + + def __checkEol(self): + """ + Private method to check the selected eol type of the eol submenu. + """ + with contextlib.suppress(AttributeError, TypeError): + self.supportedEols[self.getLineSeparator()].setChecked(True) + + def __eolChanged(self): + """ + Private slot to handle a change of the eol mode. + """ + self.__checkEol() + + if not self.inEolChanged: + self.inEolChanged = True + eol = self.getLineSeparator() + self.eolChanged.emit(eol) + self.inEolChanged = False + + def __showContextMenuSpellCheck(self): + """ + Private slot handling the aboutToShow signal of the spell check + context menu. + """ + spellingAvailable = SpellChecker.isAvailable() + self.menuActs["SpellCheck"].setEnabled(spellingAvailable) + self.menuActs["SpellCheckSelection"].setEnabled( + spellingAvailable and self.hasSelectedText() + ) + self.menuActs["SpellCheckRemove"].setEnabled( + spellingAvailable and self.spellingMenuPos >= 0 + ) + self.menuActs["SpellCheckLanguages"].setEnabled(spellingAvailable) + + self.showMenu.emit("SpellCheck", self.spellCheckMenu, self) + + def __showContextMenuSpellLanguages(self): + """ + Private slot handling the aboutToShow signal of the spell check + languages context menu. + """ + self.showMenu.emit("SpellLanguage", self.spellLanguagesMenu, self) + + def __spellLanguagesMenuTriggered(self, act): + """ + Private method to handle the selection of a spell check language. + + @param act reference to the action that was triggered + @type QAction + """ + language = act.data() + self.__setSpellingLanguage(language) + self.spellLanguageChanged.emit(language) + + def __checkSpellLanguage(self): + """ + Private slot to check the selected spell check language action. + """ + language = self.getSpellingLanguage() + with contextlib.suppress(AttributeError, KeyError): + self.supportedSpellLanguages[language].setChecked(True) + + def __spellLanguageChanged(self, language, propagate=True): + """ + Private slot to handle a change of the spell check language. + + @param language new spell check language + @type str + @param propagate flag indicating to propagate the change + @type bool + """ + self.__setSpellingLanguage(language) + self.__checkSpellLanguage() + + if not self.__inSpellLanguageChanged and propagate: + self.__inSpellLanguageChanged = True + self.spellLanguageChanged.emit(language) + self.__inSpellLanguageChanged = False + + def __bindLexer(self, filename, pyname=""): + """ + Private slot to set the correct lexer depending on language. + + @param filename filename used to determine the associated lexer + language (string) + @param pyname name of the pygments lexer to use (string) + """ + if self.lexer_ is not None and ( + self.lexer_.lexer() == "container" or self.lexer_.lexer() is None + ): + self.SCN_STYLENEEDED.disconnect(self.__styleNeeded) + + language = "" + if not self.filetype: + if filename: + basename = os.path.basename(filename) + if self.project.isOpen() and self.project.isProjectFile(filename): + language = self.project.getEditorLexerAssoc(basename) + if not language: + language = Preferences.getEditorLexerAssoc(basename) + if language == "Text": + # no highlighting for plain text files + self.__resetLanguage() + return + + if not language: + bindName = self.__bindName(self.text(0)) + if bindName: + language = Preferences.getEditorLexerAssoc(bindName) + if language == "Python": + # correction for Python + pyVer = Utilities.determinePythonVersion(filename, self.text(0), self) + language = "Python{0}".format(pyVer) + if language in [ + "Python3", + "MicroPython", + "Cython", + "Ruby", + "JavaScript", + "YAML", + "JSON", + ]: + self.filetype = language + else: + self.filetype = "" + else: + language = self.filetype + + if language.startswith("Pygments|"): + pyname = language + self.filetype = language.split("|")[-1] + language = "" + + from . import Lexers + + self.lexer_ = Lexers.getLexer(language, self, pyname=pyname) + if self.lexer_ is None: + self.setLexer() + self.apiLanguage = "" + return + + if pyname: + if pyname.startswith("Pygments|"): + self.apiLanguage = pyname + else: + self.apiLanguage = "Pygments|{0}".format(pyname) + else: + if language == "Protocol": + self.apiLanguage = language + else: + # Change API language for lexer where QScintilla reports + # an abbreviated name. + self.apiLanguage = self.lexer_.language() + if self.apiLanguage == "POV": + self.apiLanguage = "Povray" + elif self.apiLanguage == "PO": + self.apiLanguage = "Gettext" + self.setLexer(self.lexer_) + self.__setMarginsDisplay() + if self.lexer_.lexer() == "container" or self.lexer_.lexer() is None: + self.SCN_STYLENEEDED.connect(self.__styleNeeded) + + # get the font for style 0 and set it as the default font + key = ( + "Scintilla/Guessed/style0/font" + if pyname and pyname.startswith("Pygments|") + else "Scintilla/{0}/style0/font".format(self.lexer_.language()) + ) + fdesc = Preferences.getSettings().value(key) + if fdesc is not None: + font = QFont([fdesc[0]], int(fdesc[1])) + self.lexer_.setDefaultFont(font) + self.lexer_.readSettings(Preferences.getSettings(), "Scintilla") + if self.lexer_.hasSubstyles(): + self.lexer_.readSubstyles(self) + + # now set the lexer properties + self.lexer_.initProperties() + + # initialize the lexer APIs settings + projectType = ( + self.project.getProjectType() + if self.project.isOpen() and self.project.isProjectFile(filename) + else "" + ) + api = self.vm.getAPIsManager().getAPIs( + self.apiLanguage, projectType=projectType + ) + if api is not None and not api.isEmpty(): + self.lexer_.setAPIs(api.getQsciAPIs()) + self.acAPI = True + else: + self.acAPI = False + self.autoCompletionAPIsAvailable.emit(self.acAPI) + + self.__setAnnotationStyles() + + self.lexer_.setDefaultColor(self.lexer_.color(0)) + self.lexer_.setDefaultPaper(self.lexer_.paper(0)) + + def __styleNeeded(self, position): + """ + Private slot to handle the need for more styling. + + @param position end position, that needs styling (integer) + """ + self.lexer_.styleText(self.getEndStyled(), position) + + def getLexer(self): + """ + Public method to retrieve a reference to the lexer object. + + @return the lexer object (Lexer) + """ + return self.lexer_ + + def getLanguage(self, normalized=True, forPygments=False): + """ + Public method to retrieve the language of the editor. + + @param normalized flag indicating to normalize some Pygments + lexer names (boolean) + @param forPygments flag indicating to normalize some lexer + names for Pygments (boolean) + @return language of the editor (string) + """ + if self.apiLanguage == "Guessed" or self.apiLanguage.startswith("Pygments|"): + lang = self.lexer_.name() + if normalized: + # adjust some Pygments lexer names + if lang in ("Python 2.x", "Python"): + lang = "Python3" + elif lang == "Protocol Buffer": + lang = "Protocol" + + else: + lang = self.apiLanguage + if forPygments: + # adjust some names to Pygments lexer names + if lang == "Python3": + lang = "Python" + elif lang == "Protocol": + lang = "Protocol Buffer" + return lang + + def getApiLanguage(self): + """ + Public method to get the API language of the editor. + + @return API language + @rtype str + """ + return self.apiLanguage + + def __bindCompleter(self, filename): + """ + Private slot to set the correct typing completer depending on language. + + @param filename filename used to determine the associated typing + completer language (string) + """ + if self.completer is not None: + self.completer.setEnabled(False) + self.completer = None + + filename = os.path.basename(filename) + apiLanguage = Preferences.getEditorLexerAssoc(filename) + if apiLanguage == "": + pyVer = self.__getPyVersion() + if pyVer: + apiLanguage = "Python{0}".format(pyVer) + elif self.isRubyFile(): + apiLanguage = "Ruby" + + from . import TypingCompleters + + self.completer = TypingCompleters.getCompleter(apiLanguage, self) + + def getCompleter(self): + """ + Public method to retrieve a reference to the completer object. + + @return the completer object (CompleterBase) + """ + return self.completer + + def __modificationChanged(self, m): + """ + Private slot to handle the modificationChanged signal. + + It emits the signal modificationStatusChanged with parameters + m and self. + + @param m modification status + """ + if not m and bool(self.fileName) and pathlib.Path(self.fileName).exists(): + self.lastModified = pathlib.Path(self.fileName).stat().st_mtime + self.modificationStatusChanged.emit(m, self) + self.undoAvailable.emit(self.isUndoAvailable()) + self.redoAvailable.emit(self.isRedoAvailable()) + + def __cursorPositionChanged(self, line, index): + """ + Private slot to handle the cursorPositionChanged signal. + + It emits the signal cursorChanged with parameters fileName, + line and pos. + + @param line line number of the cursor + @param index position in line of the cursor + """ + self.cursorChanged.emit(self.fileName, line + 1, index) + + if Preferences.getEditor("MarkOccurrencesEnabled"): + self.__markOccurrencesTimer.stop() + self.__markOccurrencesTimer.start() + + if self.lastLine != line: + self.cursorLineChanged.emit(line) + + if self.spell is not None: + # do spell checking + doSpelling = True + if self.lastLine == line: + start, end = self.getWordBoundaries(line, index, useWordChars=False) + if start <= self.lastIndex and self.lastIndex <= end: + doSpelling = False + if doSpelling: + pos = self.positionFromLineIndex(self.lastLine, self.lastIndex) + self.spell.checkWord(pos) + + if self.lastLine != line: + self.__markerMap.update() + + self.lastLine = line + self.lastIndex = index + + def __modificationReadOnly(self): + """ + Private slot to handle the modificationAttempted signal. + """ + EricMessageBox.warning( + self, + self.tr("Modification of Read Only file"), + self.tr( + """You are attempting to change a read only file. """ + """Please save to a different file first.""" + ), + ) + + def setNoName(self, noName): + """ + Public method to set the display string for an unnamed editor. + + @param noName display string for this unnamed editor (string) + """ + self.noName = noName + + def getNoName(self): + """ + Public method to get the display string for an unnamed editor. + + @return display string for this unnamed editor (string) + """ + return self.noName + + def getFileName(self): + """ + Public method to return the name of the file being displayed. + + @return filename of the displayed file (string) + """ + return self.fileName + + def getFileType(self): + """ + Public method to return the type of the file being displayed. + + @return type of the displayed file (string) + """ + return self.filetype + + def getFileTypeByFlag(self): + """ + Public method to return the type of the file, if it was set by an + eflag: marker. + + @return type of the displayed file, if set by an eflag: marker or an + empty string (string) + """ + if self.filetypeByFlag: + return self.filetype + else: + return "" + + def determineFileType(self): + """ + Public method to determine the file type using various tests. + + @return type of the displayed file or an empty string (string) + """ + ftype = self.filetype + if not ftype: + pyVer = self.__getPyVersion() + if pyVer: + ftype = "Python{0}".format(pyVer) + elif self.isRubyFile(): + ftype = "Ruby" + else: + ftype = "" + + return ftype + + def getEncoding(self): + """ + Public method to return the current encoding. + + @return current encoding (string) + """ + return self.encoding + + def __getPyVersion(self): + """ + Private method to return the Python main version or 0 if it's + not a Python file at all. + + @return Python version or 0 if it's not a Python file (int) + """ + return Utilities.determinePythonVersion(self.fileName, self.text(0), self) + + def isPyFile(self): + """ + Public method to return a flag indicating a Python (2 or 3) file. + + @return flag indicating a Python3 file (boolean) + """ + return self.__getPyVersion() == 3 + + def isPy3File(self): + """ + Public method to return a flag indicating a Python3 file. + + @return flag indicating a Python3 file (boolean) + """ + return self.__getPyVersion() == 3 + + def isMicroPythonFile(self): + """ + Public method to return a flag indicating a MicroPython file. + + @return flag indicating a MicroPython file + @rtype bool + """ + if self.filetype == "MicroPython": + return True + + return False + + def isCythonFile(self): + """ + Public method to return a flag indicating a Cython file. + + @return flag indicating a Cython file + @rtype bool + """ + if self.filetype == "Cython": + return True + + return False + + def isRubyFile(self): + """ + Public method to return a flag indicating a Ruby file. + + @return flag indicating a Ruby file (boolean) + """ + if self.filetype == "Ruby": + return True + + if self.filetype == "": + line0 = self.text(0) + if line0.startswith("#!") and "ruby" in line0: + self.filetype = "Ruby" + return True + + if bool(self.fileName) and os.path.splitext(self.fileName)[ + 1 + ] in self.dbs.getExtensions("Ruby"): + self.filetype = "Ruby" + return True + + return False + + def isJavascriptFile(self): + """ + Public method to return a flag indicating a Javascript file. + + @return flag indicating a Javascript file (boolean) + """ + if self.filetype == "JavaScript": + return True + + if ( + self.filetype == "" + and self.fileName + and os.path.splitext(self.fileName)[1] == ".js" + ): + self.filetype = "JavaScript" + return True + + return False + + def highlightVisible(self): + """ + Public method to make sure that the highlight is visible. + """ + if self.lastHighlight is not None: + lineno = self.markerLine(self.lastHighlight) + self.ensureVisible(lineno + 1) + + def highlight(self, line=None, error=False, syntaxError=False): + """ + Public method to highlight [or de-highlight] a particular line. + + @param line line number to highlight (integer) + @param error flag indicating whether the error highlight should be + used (boolean) + @param syntaxError flag indicating a syntax error (boolean) + """ + if line is None: + self.lastHighlight = None + if self.lastErrorMarker is not None: + self.markerDeleteHandle(self.lastErrorMarker) + self.lastErrorMarker = None + if self.lastCurrMarker is not None: + self.markerDeleteHandle(self.lastCurrMarker) + self.lastCurrMarker = None + else: + if error: + if self.lastErrorMarker is not None: + self.markerDeleteHandle(self.lastErrorMarker) + self.lastErrorMarker = self.markerAdd(line - 1, self.errorline) + self.lastHighlight = self.lastErrorMarker + else: + if self.lastCurrMarker is not None: + self.markerDeleteHandle(self.lastCurrMarker) + self.lastCurrMarker = self.markerAdd(line - 1, self.currentline) + self.lastHighlight = self.lastCurrMarker + self.setCursorPosition(line - 1, 0) + + def getHighlightPosition(self): + """ + Public method to return the position of the highlight bar. + + @return line number of the highlight bar (integer) + """ + if self.lastHighlight is not None: + return self.markerLine(self.lastHighlight) + else: + return 1 + + ########################################################################### + ## Breakpoint handling methods below + ########################################################################### + + def __modified( + self, + pos, + mtype, + text, + length, + linesAdded, + line, + foldNow, + foldPrev, + token, + annotationLinesAdded, + ): + """ + Private method to handle changes of the number of lines. + + @param pos start position of change (integer) + @param mtype flags identifying the change (integer) + @param text text that is given to the Undo system (string) + @param length length of the change (integer) + @param linesAdded number of added/deleted lines (integer) + @param line line number of a fold level or marker change (integer) + @param foldNow new fold level (integer) + @param foldPrev previous fold level (integer) + @param token ??? + @param annotationLinesAdded number of added/deleted annotation lines + (integer) + """ + if ( + mtype & (self.SC_MOD_INSERTTEXT | self.SC_MOD_DELETETEXT) + and linesAdded != 0 + and self.breaks + ): + bps = [] # list of breakpoints + for handle, (ln, cond, temp, enabled, ignorecount) in self.breaks.items(): + line = self.markerLine(handle) + 1 + if ln != line: + bps.append((ln, line)) + self.breaks[handle] = (line, cond, temp, enabled, ignorecount) + self.inLinesChanged = True + for ln, line in sorted(bps, reverse=linesAdded > 0): + index1 = self.breakpointModel.getBreakPointIndex(self.fileName, ln) + index2 = self.breakpointModel.index(index1.row(), 1) + self.breakpointModel.setData(index2, line) + self.inLinesChanged = False + + def __restoreBreakpoints(self): + """ + Private method to restore the breakpoints. + """ + for handle in list(self.breaks.keys()): + self.markerDeleteHandle(handle) + self.__addBreakPoints(QModelIndex(), 0, self.breakpointModel.rowCount() - 1) + self.__markerMap.update() + + def __deleteBreakPoints(self, parentIndex, start, end): + """ + Private slot to delete breakpoints. + + @param parentIndex index of parent item (QModelIndex) + @param start start row (integer) + @param end end row (integer) + """ + for row in range(start, end + 1): + index = self.breakpointModel.index(row, 0, parentIndex) + fn, lineno = self.breakpointModel.getBreakPointByIndex(index)[0:2] + if fn == self.fileName: + self.clearBreakpoint(lineno) + + def __changeBreakPoints(self, startIndex, endIndex): + """ + Private slot to set changed breakpoints. + + @param startIndex start index of the breakpoints being changed + (QModelIndex) + @param endIndex end index of the breakpoints being changed + (QModelIndex) + """ + if not self.inLinesChanged: + self.__addBreakPoints(QModelIndex(), startIndex.row(), endIndex.row()) + + def __breakPointDataAboutToBeChanged(self, startIndex, endIndex): + """ + Private slot to handle the dataAboutToBeChanged signal of the + breakpoint model. + + @param startIndex start index of the rows to be changed (QModelIndex) + @param endIndex end index of the rows to be changed (QModelIndex) + """ + self.__deleteBreakPoints(QModelIndex(), startIndex.row(), endIndex.row()) + + def __addBreakPoints(self, parentIndex, start, end): + """ + Private slot to add breakpoints. + + @param parentIndex index of parent item (QModelIndex) + @param start start row (integer) + @param end end row (integer) + """ + for row in range(start, end + 1): + index = self.breakpointModel.index(row, 0, parentIndex) + ( + fn, + line, + cond, + temp, + enabled, + ignorecount, + ) = self.breakpointModel.getBreakPointByIndex(index)[:6] + if fn == self.fileName: + self.newBreakpointWithProperties( + line, (cond, temp, enabled, ignorecount) + ) + + def clearBreakpoint(self, line): + """ + Public method to clear a breakpoint. + + Note: This doesn't clear the breakpoint in the debugger, + it just deletes it from the editor internal list of breakpoints. + + @param line line number of the breakpoint (integer) + """ + if self.inLinesChanged: + return + + for handle in self.breaks: + if self.markerLine(handle) == line - 1: + break + else: + # not found, simply ignore it + return + + del self.breaks[handle] + self.markerDeleteHandle(handle) + self.__markerMap.update() + + def newBreakpointWithProperties(self, line, properties): + """ + Public method to set a new breakpoint and its properties. + + @param line line number of the breakpoint (integer) + @param properties properties for the breakpoint (tuple) + (condition, temporary flag, enabled flag, ignore count) + """ + if not properties[2]: + marker = self.dbreakpoint + elif properties[0]: + marker = properties[1] and self.tcbreakpoint or self.cbreakpoint + else: + marker = properties[1] and self.tbreakpoint or self.breakpoint + + if self.markersAtLine(line - 1) & self.breakpointMask == 0: + handle = self.markerAdd(line - 1, marker) + self.breaks[handle] = (line,) + properties + self.breakpointToggled.emit(self) + self.__markerMap.update() + + def __toggleBreakpoint(self, line, temporary=False): + """ + Private method to toggle a breakpoint. + + @param line line number of the breakpoint (integer) + @param temporary flag indicating a temporary breakpoint (boolean) + """ + for handle in self.breaks: + if self.markerLine(handle) == line - 1: + # delete breakpoint or toggle it to the next state + index = self.breakpointModel.getBreakPointIndex(self.fileName, line) + if Preferences.getDebugger( + "ThreeStateBreakPoints" + ) and not self.breakpointModel.isBreakPointTemporaryByIndex(index): + self.breakpointModel.deleteBreakPointByIndex(index) + self.__addBreakPoint(line, True) + else: + self.breakpointModel.deleteBreakPointByIndex(index) + self.breakpointToggled.emit(self) + break + else: + self.__addBreakPoint(line, temporary) + + def __addBreakPoint(self, line, temporary): + """ + Private method to add a new breakpoint. + + @param line line number of the breakpoint (integer) + @param temporary flag indicating a temporary breakpoint (boolean) + """ + if self.fileName and self.isPyFile(): + linestarts = PythonDisViewer.linestarts(self.text()) + if line not in linestarts: + if Preferences.getDebugger("IntelligentBreakpoints"): + # change line to the next one starting an instruction block + index = bisect.bisect(linestarts, line) + with contextlib.suppress(IndexError): + line = linestarts[index] + self.__toggleBreakpoint(line, temporary=temporary) + else: + EricMessageBox.warning( + self, + self.tr("Add Breakpoint"), + self.tr( + "No Python byte code will be created for the" + " selected line. No break point will be set!" + ), + ) + return + + self.breakpointModel.addBreakPoint( + self.fileName, line, ("", temporary, True, 0) + ) + self.breakpointToggled.emit(self) + + def __toggleBreakpointEnabled(self, line): + """ + Private method to toggle a breakpoints enabled status. + + @param line line number of the breakpoint (integer) + """ + for handle in self.breaks: + if self.markerLine(handle) == line - 1: + index = self.breakpointModel.getBreakPointIndex(self.fileName, line) + self.breakpointModel.setBreakPointEnabledByIndex( + index, not self.breaks[handle][3] + ) + break + + def curLineHasBreakpoint(self): + """ + Public method to check for the presence of a breakpoint at the current + line. + + @return flag indicating the presence of a breakpoint (boolean) + """ + line, _ = self.getCursorPosition() + return self.markersAtLine(line) & self.breakpointMask != 0 + + def getBreakpointLines(self): + """ + Public method to get the lines containing a breakpoint. + + @return list of lines containing a breakpoint (list of integer) + """ + lines = [] + line = -1 + while True: + line = self.markerFindNext(line + 1, self.breakpointMask) + if line < 0: + break + else: + lines.append(line) + return lines + + def hasBreakpoints(self): + """ + Public method to check for the presence of breakpoints. + + @return flag indicating the presence of breakpoints (boolean) + """ + return len(self.breaks) > 0 + + def __menuToggleTemporaryBreakpoint(self): + """ + Private slot to handle the 'Toggle temporary breakpoint' context menu + action. + """ + if self.line < 0: + self.line, index = self.getCursorPosition() + self.line += 1 + self.__toggleBreakpoint(self.line, 1) + self.line = -1 + + def menuToggleBreakpoint(self): + """ + Public slot to handle the 'Toggle breakpoint' context menu action. + """ + if self.line < 0: + self.line, index = self.getCursorPosition() + self.line += 1 + self.__toggleBreakpoint(self.line) + self.line = -1 + + def __menuToggleBreakpointEnabled(self): + """ + Private slot to handle the 'Enable/Disable breakpoint' context menu + action. + """ + if self.line < 0: + self.line, index = self.getCursorPosition() + self.line += 1 + self.__toggleBreakpointEnabled(self.line) + self.line = -1 + + def menuEditBreakpoint(self, line=None): + """ + Public slot to handle the 'Edit breakpoint' context menu action. + + @param line linenumber of the breakpoint to edit + """ + if line is not None: + self.line = line - 1 + if self.line < 0: + self.line, index = self.getCursorPosition() + + for handle in self.breaks: + if self.markerLine(handle) == self.line: + ln, cond, temp, enabled, ignorecount = self.breaks[handle] + index = self.breakpointModel.getBreakPointIndex(self.fileName, ln) + if not index.isValid(): + return + + # get recently used breakpoint conditions + rs = Preferences.Prefs.rsettings.value(recentNameBreakpointConditions) + condHistory = ( + Preferences.toList(rs)[: Preferences.getDebugger("RecentNumber")] + if rs is not None + else [] + ) + + from Debugger.EditBreakpointDialog import EditBreakpointDialog + + dlg = EditBreakpointDialog( + (self.fileName, ln), + (cond, temp, enabled, ignorecount), + condHistory, + self, + modal=True, + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + cond, temp, enabled, ignorecount = dlg.getData() + self.breakpointModel.setBreakPointByIndex( + index, self.fileName, ln, (cond, temp, enabled, ignorecount) + ) + + if cond: + # save the recently used breakpoint condition + if cond in condHistory: + condHistory.remove(cond) + condHistory.insert(0, cond) + Preferences.Prefs.rsettings.setValue( + recentNameBreakpointConditions, condHistory + ) + Preferences.Prefs.rsettings.sync() + + break + + self.line = -1 + + def menuNextBreakpoint(self): + """ + Public slot to handle the 'Next breakpoint' context menu action. + """ + line, index = self.getCursorPosition() + if line == self.lines() - 1: + line = 0 + else: + line += 1 + bpline = self.markerFindNext(line, self.breakpointMask) + if bpline < 0: + # wrap around + bpline = self.markerFindNext(0, self.breakpointMask) + if bpline >= 0: + self.setCursorPosition(bpline, 0) + self.ensureLineVisible(bpline) + + def menuPreviousBreakpoint(self): + """ + Public slot to handle the 'Previous breakpoint' context menu action. + """ + line, index = self.getCursorPosition() + if line == 0: + line = self.lines() - 1 + else: + line -= 1 + bpline = self.markerFindPrevious(line, self.breakpointMask) + if bpline < 0: + # wrap around + bpline = self.markerFindPrevious(self.lines() - 1, self.breakpointMask) + if bpline >= 0: + self.setCursorPosition(bpline, 0) + self.ensureLineVisible(bpline) + + def __menuClearBreakpoints(self): + """ + Private slot to handle the 'Clear all breakpoints' context menu action. + """ + self.__clearBreakpoints(self.fileName) + + def __clearBreakpoints(self, fileName): + """ + Private slot to clear all breakpoints. + + @param fileName name of the file (string) + """ + idxList = [] + for (ln, _, _, _, _) in self.breaks.values(): + index = self.breakpointModel.getBreakPointIndex(fileName, ln) + if index.isValid(): + idxList.append(index) + if idxList: + self.breakpointModel.deleteBreakPoints(idxList) + + ########################################################################### + ## Bookmark handling methods below + ########################################################################### + + def toggleBookmark(self, line): + """ + Public method to toggle a bookmark. + + @param line line number of the bookmark (integer) + """ + for handle in self.bookmarks: + if self.markerLine(handle) == line - 1: + self.bookmarks.remove(handle) + self.markerDeleteHandle(handle) + break + else: + # set a new bookmark + handle = self.markerAdd(line - 1, self.bookmark) + self.bookmarks.append(handle) + self.bookmarkToggled.emit(self) + self.__markerMap.update() + + def getBookmarks(self): + """ + Public method to retrieve the bookmarks. + + @return sorted list of all lines containing a bookmark + (list of integer) + """ + bmlist = [] + for handle in self.bookmarks: + bmlist.append(self.markerLine(handle) + 1) + + bmlist.sort() + return bmlist + + def getBookmarkLines(self): + """ + Public method to get the lines containing a bookmark. + + @return list of lines containing a bookmark (list of integer) + """ + lines = [] + line = -1 + while True: + line = self.markerFindNext(line + 1, 1 << self.bookmark) + if line < 0: + break + else: + lines.append(line) + return lines + + def hasBookmarks(self): + """ + Public method to check for the presence of bookmarks. + + @return flag indicating the presence of bookmarks (boolean) + """ + return len(self.bookmarks) > 0 + + def menuToggleBookmark(self): + """ + Public slot to handle the 'Toggle bookmark' context menu action. + """ + if self.line < 0: + self.line, index = self.getCursorPosition() + self.line += 1 + self.toggleBookmark(self.line) + self.line = -1 + + def nextBookmark(self): + """ + Public slot to handle the 'Next bookmark' context menu action. + """ + line, index = self.getCursorPosition() + if line == self.lines() - 1: + line = 0 + else: + line += 1 + bmline = self.markerFindNext(line, 1 << self.bookmark) + if bmline < 0: + # wrap around + bmline = self.markerFindNext(0, 1 << self.bookmark) + if bmline >= 0: + self.setCursorPosition(bmline, 0) + self.ensureLineVisible(bmline) + + def previousBookmark(self): + """ + Public slot to handle the 'Previous bookmark' context menu action. + """ + line, index = self.getCursorPosition() + if line == 0: + line = self.lines() - 1 + else: + line -= 1 + bmline = self.markerFindPrevious(line, 1 << self.bookmark) + if bmline < 0: + # wrap around + bmline = self.markerFindPrevious(self.lines() - 1, 1 << self.bookmark) + if bmline >= 0: + self.setCursorPosition(bmline, 0) + self.ensureLineVisible(bmline) + + def clearBookmarks(self): + """ + Public slot to handle the 'Clear all bookmarks' context menu action. + """ + for handle in self.bookmarks: + self.markerDeleteHandle(handle) + self.bookmarks.clear() + self.bookmarkToggled.emit(self) + self.__markerMap.update() + + ########################################################################### + ## Printing methods below + ########################################################################### + + def printFile(self): + """ + Public slot to print the text. + """ + from .Printer import Printer + + printer = Printer(mode=QPrinter.PrinterMode.HighResolution) + sb = ericApp().getObject("UserInterface").statusBar() + printDialog = QPrintDialog(printer, self) + if self.hasSelectedText(): + printDialog.setOption( + QAbstractPrintDialog.PrintDialogOption.PrintSelection, True + ) + if printDialog.exec() == QDialog.DialogCode.Accepted: + sb.showMessage(self.tr("Printing...")) + QApplication.processEvents() + fn = self.getFileName() + if fn is not None: + printer.setDocName(os.path.basename(fn)) + else: + printer.setDocName(self.noName) + if printDialog.printRange() == QAbstractPrintDialog.PrintRange.Selection: + # get the selection + fromLine, fromIndex, toLine, toIndex = self.getSelection() + if toIndex == 0: + toLine -= 1 + # QScintilla seems to print one line more than told + res = printer.printRange(self, fromLine, toLine - 1) + else: + res = printer.printRange(self) + if res: + sb.showMessage(self.tr("Printing completed"), 2000) + else: + sb.showMessage(self.tr("Error while printing"), 2000) + QApplication.processEvents() + else: + sb.showMessage(self.tr("Printing aborted"), 2000) + QApplication.processEvents() + + def printPreviewFile(self): + """ + Public slot to show a print preview of the text. + """ + from PyQt6.QtPrintSupport import QPrintPreviewDialog + from .Printer import Printer + + printer = Printer(mode=QPrinter.PrinterMode.HighResolution) + fn = self.getFileName() + if fn is not None: + printer.setDocName(os.path.basename(fn)) + else: + printer.setDocName(self.noName) + preview = QPrintPreviewDialog(printer, self) + preview.paintRequested.connect(self.__printPreview) + preview.exec() + + def __printPreview(self, printer): + """ + Private slot to generate a print preview. + + @param printer reference to the printer object + (QScintilla.Printer.Printer) + """ + printer.printRange(self) + + ########################################################################### + ## Task handling methods below + ########################################################################### + + def getTaskLines(self): + """ + Public method to get the lines containing a task. + + @return list of lines containing a task (list of integer) + """ + lines = [] + line = -1 + while True: + line = self.markerFindNext(line + 1, 1 << self.taskmarker) + if line < 0: + break + else: + lines.append(line) + return lines + + def hasTaskMarkers(self): + """ + Public method to determine, if this editor contains any task markers. + + @return flag indicating the presence of task markers (boolean) + """ + return self.__hasTaskMarkers + + def nextTask(self): + """ + Public slot to handle the 'Next task' context menu action. + """ + line, index = self.getCursorPosition() + if line == self.lines() - 1: + line = 0 + else: + line += 1 + taskline = self.markerFindNext(line, 1 << self.taskmarker) + if taskline < 0: + # wrap around + taskline = self.markerFindNext(0, 1 << self.taskmarker) + if taskline >= 0: + self.setCursorPosition(taskline, 0) + self.ensureLineVisible(taskline) + + def previousTask(self): + """ + Public slot to handle the 'Previous task' context menu action. + """ + line, index = self.getCursorPosition() + if line == 0: + line = self.lines() - 1 + else: + line -= 1 + taskline = self.markerFindPrevious(line, 1 << self.taskmarker) + if taskline < 0: + # wrap around + taskline = self.markerFindPrevious(self.lines() - 1, 1 << self.taskmarker) + if taskline >= 0: + self.setCursorPosition(taskline, 0) + self.ensureLineVisible(taskline) + + def extractTasks(self): + """ + Public slot to extract all tasks. + """ + from Tasks.Task import Task + + markers = { + taskType: Preferences.getTasks(markersName).split() + for taskType, markersName in Task.TaskType2MarkersName.items() + } + txtList = self.text().split(self.getLineSeparator()) + + # clear all task markers and tasks + self.markerDeleteAll(self.taskmarker) + self.taskViewer.clearFileTasks(self.fileName) + self.__hasTaskMarkers = False + + # now search tasks and record them + for lineIndex, line in enumerate(txtList): + shouldBreak = False + + if line.endswith("__NO-TASK__"): + # ignore potential task marker + continue + + for taskType, taskMarkers in markers.items(): + for taskMarker in taskMarkers: + index = line.find(taskMarker) + if index > -1: + task = line[index:] + self.markerAdd(lineIndex, self.taskmarker) + self.taskViewer.addFileTask( + task, self.fileName, lineIndex + 1, taskType + ) + self.__hasTaskMarkers = True + shouldBreak = True + break + if shouldBreak: + break + self.taskMarkersUpdated.emit(self) + self.__markerMap.update() + + ########################################################################### + ## Change tracing methods below + ########################################################################### + + def __createChangeMarkerPixmap(self, key, size=16, width=4): + """ + Private method to create a pixmap for the change markers. + + @param key key of the color to use (string) + @param size size of the pixmap (integer) + @param width width of the marker line (integer) + @return create pixmap (QPixmap) + """ + pixmap = QPixmap(size, size) + pixmap.fill(Qt.GlobalColor.transparent) + painter = QPainter(pixmap) + painter.fillRect(size - 4, 0, 4, size, Preferences.getEditorColour(key)) + painter.end() + return pixmap + + def __initOnlineChangeTrace(self): + """ + Private slot to initialize the online change trace. + """ + self.__hasChangeMarkers = False + self.__oldText = self.text() + self.__lastSavedText = self.text() + self.__onlineChangeTraceTimer = QTimer(self) + self.__onlineChangeTraceTimer.setSingleShot(True) + self.__onlineChangeTraceTimer.setInterval( + Preferences.getEditor("OnlineChangeTraceInterval") + ) + self.__onlineChangeTraceTimer.timeout.connect( + self.__onlineChangeTraceTimerTimeout + ) + self.textChanged.connect(self.__resetOnlineChangeTraceTimer) + + def __reinitOnlineChangeTrace(self): + """ + Private slot to re-initialize the online change trace. + """ + self.__oldText = self.text() + self.__lastSavedText = self.text() + self.__deleteAllChangeMarkers() + + def __resetOnlineChangeTraceTimer(self): + """ + Private method to reset the online syntax check timer. + """ + if Preferences.getEditor("OnlineChangeTrace"): + self.__onlineChangeTraceTimer.stop() + self.__onlineChangeTraceTimer.start() + + def __onlineChangeTraceTimerTimeout(self): + """ + Private slot to mark added and changed lines. + """ + self.__deleteAllChangeMarkers() + + # step 1: mark saved changes + oldL = self.__oldText.splitlines() + newL = self.__lastSavedText.splitlines() + matcher = difflib.SequenceMatcher(None, oldL, newL) + + for token, _, _, j1, j2 in matcher.get_opcodes(): + if token in ["insert", "replace"]: + for lineNo in range(j1, j2): + self.markerAdd(lineNo, self.__changeMarkerSaved) + self.__hasChangeMarkers = True + + # step 2: mark unsaved changes + oldL = self.__lastSavedText.splitlines() + newL = self.text().splitlines() + matcher = difflib.SequenceMatcher(None, oldL, newL) + + for token, _, _, j1, j2 in matcher.get_opcodes(): + if token in ["insert", "replace"]: + for lineNo in range(j1, j2): + self.markerAdd(lineNo, self.__changeMarkerUnsaved) + self.__hasChangeMarkers = True + + if self.__hasChangeMarkers: + self.changeMarkersUpdated.emit(self) + self.__markerMap.update() + + def __resetOnlineChangeTraceInfo(self): + """ + Private slot to reset the online change trace info. + """ + self.__lastSavedText = self.text() + self.__deleteAllChangeMarkers() + + # mark saved changes + oldL = self.__oldText.splitlines() + newL = self.__lastSavedText.splitlines() + matcher = difflib.SequenceMatcher(None, oldL, newL) + + for token, _, _, j1, j2 in matcher.get_opcodes(): + if token in ["insert", "replace"]: + for lineNo in range(j1, j2): + self.markerAdd(lineNo, self.__changeMarkerSaved) + self.__hasChangeMarkers = True + + if self.__hasChangeMarkers: + self.changeMarkersUpdated.emit(self) + self.__markerMap.update() + + def __deleteAllChangeMarkers(self): + """ + Private slot to delete all change markers. + """ + self.markerDeleteAll(self.__changeMarkerUnsaved) + self.markerDeleteAll(self.__changeMarkerSaved) + self.__hasChangeMarkers = False + self.changeMarkersUpdated.emit(self) + self.__markerMap.update() + + def getChangeLines(self): + """ + Public method to get the lines containing a change. + + @return list of lines containing a change (list of integer) + """ + lines = [] + line = -1 + while True: + line = self.markerFindNext(line + 1, self.changeMarkersMask) + if line < 0: + break + else: + lines.append(line) + return lines + + def hasChangeMarkers(self): + """ + Public method to determine, if this editor contains any change markers. + + @return flag indicating the presence of change markers (boolean) + """ + return self.__hasChangeMarkers + + def nextChange(self): + """ + Public slot to handle the 'Next change' context menu action. + """ + line, index = self.getCursorPosition() + if line == self.lines() - 1: + line = 0 + else: + line += 1 + changeline = self.markerFindNext(line, self.changeMarkersMask) + if changeline < 0: + # wrap around + changeline = self.markerFindNext(0, self.changeMarkersMask) + if changeline >= 0: + self.setCursorPosition(changeline, 0) + self.ensureLineVisible(changeline) + + def previousChange(self): + """ + Public slot to handle the 'Previous change' context menu action. + """ + line, index = self.getCursorPosition() + if line == 0: + line = self.lines() - 1 + else: + line -= 1 + changeline = self.markerFindPrevious(line, self.changeMarkersMask) + if changeline < 0: + # wrap around + changeline = self.markerFindPrevious( + self.lines() - 1, self.changeMarkersMask + ) + if changeline >= 0: + self.setCursorPosition(changeline, 0) + self.ensureLineVisible(changeline) + + ########################################################################### + ## Flags handling methods below + ########################################################################### + + def __processFlags(self): + """ + Private method to extract flags and process them. + + @return list of change flags (list of string) + """ + txt = self.text() + flags = Utilities.extractFlags(txt) + + changedFlags = [] + + # Flag 1: FileType + if "FileType" in flags: + oldFiletype = self.filetype + if isinstance(flags["FileType"], str): + self.filetype = flags["FileType"] + self.filetypeByFlag = True + if oldFiletype != self.filetype: + changedFlags.append("FileType") + else: + if self.filetype != "" and self.filetypeByFlag: + self.filetype = "" + self.filetypeByFlag = False + self.__bindName(txt.splitlines()[0]) + changedFlags.append("FileType") + + return changedFlags + + ########################################################################### + ## File handling methods below + ########################################################################### + + def checkDirty(self): + """ + Public method to check dirty status and open a message window. + + @return flag indicating successful reset of the dirty flag (boolean) + """ + if self.isModified(): + fn = self.fileName + if fn is None: + fn = self.noName + res = EricMessageBox.okToClearData( + self, + self.tr("File Modified"), + self.tr("<p>The file <b>{0}</b> has unsaved changes.</p>").format(fn), + self.saveFile, + ) + if res: + self.vm.setEditorName(self, self.fileName) + return res + + return True + + def revertToUnmodified(self): + """ + Public method to revert back to the last saved state. + """ + undo_ = True + while self.isModified(): + if undo_: + # try undo first + if self.isUndoAvailable(): + self.undo() + else: + undo_ = False + else: + # try redo next + if self.isRedoAvailable(): + self.redo() + else: + break + # Couldn't find the unmodified state + + def readFile(self, fn, createIt=False, encoding=""): + """ + Public slot to read the text from a file. + + @param fn filename to read from (string) + @param createIt flag indicating the creation of a new file, if the + given one doesn't exist (boolean) + @param encoding encoding to be used to read the file (string) + (Note: this parameter overrides encoding detection) + """ + self.__loadEditorConfig(fileName=fn) + + try: + with EricOverrideCursor(): + if createIt and not os.path.exists(fn): + with open(fn, "w"): + pass + if encoding == "": + encoding = self.__getEditorConfig("DefaultEncoding", nodefault=True) + if encoding: + txt, self.encoding = Utilities.readEncodedFileWithEncoding( + fn, encoding + ) + else: + txt, self.encoding = Utilities.readEncodedFile(fn) + except (UnicodeDecodeError, OSError) as why: + EricMessageBox.critical( + self.vm, + self.tr("Open File"), + self.tr( + "<p>The file <b>{0}</b> could not be opened.</p>" + "<p>Reason: {1}</p>" + ).format(fn, str(why)), + ) + raise + + with EricOverrideCursor(): + modified = False + + self.setText(txt) + + # get eric specific flags + self.__processFlags() + + # perform automatic EOL conversion + if self.__getEditorConfig( + "EOLMode", nodefault=True + ) or Preferences.getEditor("AutomaticEOLConversion"): + self.convertEols(self.eolMode()) + else: + fileEol = self.detectEolString(txt) + self.setEolModeByEolString(fileEol) + + self.extractTasks() + + self.setModified(modified) + self.lastModified = pathlib.Path(fn).stat().st_mtime + + def __convertTabs(self): + """ + Private slot to convert tabulators to spaces. + """ + if ( + (not self.__getEditorConfig("TabForIndentation")) + and Preferences.getEditor("ConvertTabsOnLoad") + and not (self.lexer_ and self.lexer_.alwaysKeepTabs()) + ): + txt = self.text() + txtExpanded = txt.expandtabs(self.__getEditorConfig("TabWidth")) + if txtExpanded != txt: + self.beginUndoAction() + self.setText(txt) + self.endUndoAction() + + self.setModified(True) + + def __removeTrailingWhitespace(self): + """ + Private method to remove trailing whitespace. + """ + searchRE = r"[ \t]+$" # whitespace at the end of a line + + ok = self.findFirstTarget(searchRE, True, False, False, 0, 0) + self.beginUndoAction() + while ok: + self.replaceTarget("") + ok = self.findNextTarget() + self.endUndoAction() + + def writeFile(self, fn, backup=True): + """ + Public slot to write the text to a file. + + @param fn filename to write to (string) + @param backup flag indicating to save a backup (boolean) + @return flag indicating success (boolean) + """ + config = self.__loadEditorConfigObject(fn) + + eol = self.__getEditorConfig("EOLMode", nodefault=True, config=config) + if eol is not None: + self.convertEols(eol) + + if self.__getEditorConfig("StripTrailingWhitespace", config=config): + self.__removeTrailingWhitespace() + + txt = self.text() + + if self.__getEditorConfig("InsertFinalNewline", config=config): + eol = self.getLineSeparator() + if eol: + if len(txt) >= len(eol): + if txt[-len(eol) :] != eol: + txt += eol + else: + txt += eol + + # create a backup file, if the option is set + createBackup = backup and Preferences.getEditor("CreateBackupFile") + if createBackup: + if os.path.islink(fn): + fn = os.path.realpath(fn) + bfn = "{0}~".format(fn) + try: + permissions = os.stat(fn).st_mode + perms_valid = True + except OSError: + # if there was an error, ignore it + perms_valid = False + with contextlib.suppress(OSError): + os.remove(bfn) + with contextlib.suppress(OSError): + os.rename(fn, bfn) + + # now write text to the file fn + try: + editorConfigEncoding = self.__getEditorConfig( + "DefaultEncoding", nodefault=True, config=config + ) + self.encoding = Utilities.writeEncodedFile( + fn, txt, self.encoding, forcedEncoding=editorConfigEncoding + ) + if createBackup and perms_valid: + os.chmod(fn, permissions) + return True + except (OSError, Utilities.CodingError, UnicodeError) as why: + EricMessageBox.critical( + self, + self.tr("Save File"), + self.tr( + "<p>The file <b>{0}</b> could not be saved.<br/>" "Reason: {1}</p>" + ).format(fn, str(why)), + ) + return False + + def __getSaveFileName(self, path=None): + """ + Private method to get the name of the file to be saved. + + @param path directory to save the file in (string) + @return file name (string) + """ + # save to project, if a project is loaded + if self.project.isOpen(): + if self.fileName and self.project.startswithProjectPath(self.fileName): + path = os.path.dirname(self.fileName) + elif not self.fileName: + path = self.project.getProjectPath() + + if not path and self.fileName: + path = os.path.dirname(self.fileName) + if not path: + path = Preferences.getMultiProject("Workspace") or Utilities.getHomeDir() + + from . import Lexers + + if self.fileName: + filterPattern = "(*{0})".format(os.path.splitext(self.fileName)[1]) + for fileFilter in Lexers.getSaveFileFiltersList(True): + if filterPattern in fileFilter: + defaultFilter = fileFilter + break + else: + defaultFilter = Preferences.getEditor("DefaultSaveFilter") + else: + defaultFilter = Preferences.getEditor("DefaultSaveFilter") + fn, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( + self, + self.tr("Save File"), + path, + Lexers.getSaveFileFiltersList(True, True), + defaultFilter, + EricFileDialog.DontConfirmOverwrite, + ) + + if fn: + if fn.endswith("."): + fn = fn[:-1] + + fpath = pathlib.Path(fn) + if not fpath.suffix: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fpath = fpath.with_suffix(ex) + if fpath.exists(): + res = EricMessageBox.yesNo( + self, + self.tr("Save File"), + self.tr( + "<p>The file <b>{0}</b> already exists." " Overwrite it?</p>" + ).format(fpath), + icon=EricMessageBox.Warning, + ) + if not res: + return "" + + return str(fpath) + + return "" + + def saveFileCopy(self, path=None): + """ + Public method to save a copy of the file. + + @param path directory to save the file in (string) + @return flag indicating success (boolean) + """ + fn = self.__getSaveFileName(path) + if not fn: + return False + + res = self.writeFile(fn) + if res and self.project.isOpen() and self.project.startswithProjectPath(fn): + # save to project, if a project is loaded + self.project.appendFile(fn) + + return res + + def saveFile(self, saveas=False, path=None): + """ + Public method to save the text to a file. + + @param saveas flag indicating a 'save as' action (boolean) + @param path directory to save the file in (string) + @return flag indicating success (boolean) + """ + if not saveas and not self.isModified(): + return False # do nothing if text wasn't changed + + newName = None + if saveas or self.fileName == "": + saveas = True + + fn = self.__getSaveFileName(path) + if not fn: + return False + + newName = fn + + # save to project, if a project is loaded + if self.project.isOpen() and self.project.startswithProjectPath(fn): + editorConfigEol = self.__getEditorConfig( + "EOLMode", nodefault=True, config=self.__loadEditorConfigObject(fn) + ) + if editorConfigEol is not None: + self.setEolMode(editorConfigEol) + else: + self.setEolModeByEolString(self.project.getEolString()) + self.convertEols(self.eolMode()) + else: + fn = self.fileName + + self.__loadEditorConfig(fn) + self.editorAboutToBeSaved.emit(self.fileName) + if self.writeFile(fn): + if saveas: + self.__clearBreakpoints(self.fileName) + self.__setFileName(fn) + self.setModified(False) + self.setReadOnly(False) + self.setWindowTitle(self.fileName) + # get eric specific flags + changedFlags = self.__processFlags() + if not self.__lexerReset and "FileType" in changedFlags: + self.setLanguage(self.fileName) + + if saveas: + self.isResourcesFile = self.fileName.endswith(".qrc") + self.__initContextMenu() + self.editorRenamed.emit(self.fileName) + + # save to project, if a project is loaded + if self.project.isOpen() and self.project.startswithProjectPath(fn): + self.project.appendFile(fn) + self.addedToProject() + + self.setLanguage(self.fileName) + + self.lastModified = pathlib.Path(fn).stat().st_mtime + if newName is not None: + self.vm.addToRecentList(newName) + self.editorSaved.emit(self.fileName) + self.checkSyntax() + self.extractTasks() + self.__resetOnlineChangeTraceInfo() + self.__checkEncoding() + return True + else: + self.lastModified = ( + pathlib.Path(fn).stat().st_mtime if pathlib.Path(fn).exists() else 0 + ) + 0 + ) + return False + + def saveFileAs(self, path=None, toProject=False): + """ + Public slot to save a file with a new name. + + @param path directory to save the file in (string) + @param toProject flag indicating a save to project operation + (boolean) + @return tuple of two values (boolean, string) giving a success + indicator and the name of the saved file + """ + return self.saveFile(True, path) + + def handleRenamed(self, fn): + """ + Public slot to handle the editorRenamed signal. + + @param fn filename to be set for the editor (string). + """ + self.__clearBreakpoints(fn) + + self.__setFileName(fn) + self.setWindowTitle(self.fileName) + + self.__loadEditorConfig() + + if self.lexer_ is None: + self.setLanguage(self.fileName) + + self.lastModified = pathlib.Path(fn).stat().st_mtime + self.vm.setEditorName(self, self.fileName) + self.__updateReadOnly(True) + + def fileRenamed(self, fn): + """ + Public slot to handle the editorRenamed signal. + + @param fn filename to be set for the editor (string). + """ + self.handleRenamed(fn) + if not self.inFileRenamed: + self.inFileRenamed = True + self.editorRenamed.emit(self.fileName) + self.inFileRenamed = False + + ########################################################################### + ## Utility methods below + ########################################################################### + + def ensureVisible(self, line, expand=False): + """ + Public slot to ensure, that the specified line is visible. + + @param line line number to make visible + @type int + @param expand flag indicating to expand all folds + @type bool + """ + self.ensureLineVisible(line - 1) + if expand: + self.SendScintilla( + QsciScintilla.SCI_FOLDCHILDREN, + line - 1, + QsciScintilla.SC_FOLDACTION_EXPAND, + ) + + def ensureVisibleTop(self, line, expand=False): + """ + Public slot to ensure, that the specified line is visible at the top + of the editor. + + @param line line number to make visible + @type int + @param expand flag indicating to expand all folds + @type bool + """ + self.ensureVisible(line) + self.setFirstVisibleLine(line - 1) + self.ensureCursorVisible() + if expand: + self.SendScintilla( + QsciScintilla.SCI_FOLDCHILDREN, + line - 1, + QsciScintilla.SC_FOLDACTION_EXPAND, + ) + + def __marginClicked(self, margin, line, modifiers): + """ + Private slot to handle the marginClicked signal. + + @param margin id of the clicked margin (integer) + @param line line number of the click (integer) + @param modifiers keyboard modifiers (Qt.KeyboardModifiers) + """ + if margin == self.__bmMargin: + self.toggleBookmark(line + 1) + elif margin == self.__bpMargin: + self.__toggleBreakpoint(line + 1) + elif margin == self.__indicMargin: + if self.markersAtLine(line) & (1 << self.syntaxerror): + self.__showSyntaxError(line) + elif self.markersAtLine(line) & (1 << self.warning): + self.__showWarning(line) + + def handleMonospacedEnable(self): + """ + Public slot to handle the Use Monospaced Font context menu entry. + """ + if self.menuActs["MonospacedFont"].isChecked(): + if not self.lexer_: + self.setMonospaced(True) + else: + if self.lexer_: + self.lexer_.readSettings(Preferences.getSettings(), "Scintilla") + if self.lexer_.hasSubstyles(): + self.lexer_.readSubstyles(self) + self.lexer_.initProperties() + self.setMonospaced(False) + self.__setMarginsDisplay() + + def getWordBoundaries(self, line, index, useWordChars=True): + """ + Public method to get the word boundaries at a position. + + @param line number of line to look at (int) + @param index position to look at (int) + @param useWordChars flag indicating to use the wordCharacters + method (boolean) + @return tuple with start and end indexes of the word at the position + (integer, integer) + """ + wc = self.wordCharacters() + if wc is None or not useWordChars: + pattern = r"\b[\w_]+\b" + else: + wc = re.sub(r"\w", "", wc) + pattern = r"\b[\w{0}]+\b".format(re.escape(wc)) + rx = ( + re.compile(pattern) + if self.caseSensitive() + else re.compile(pattern, re.IGNORECASE) + ) + + text = self.text(line) + for match in rx.finditer(text): + start, end = match.span() + if start <= index <= end: + return (start, end) + + return (index, index) + + def getWord(self, line, index, direction=0, useWordChars=True): + """ + Public method to get the word at a position. + + @param line number of line to look at (int) + @param index position to look at (int) + @param direction direction to look in (0 = whole word, 1 = left, + 2 = right) + @param useWordChars flag indicating to use the wordCharacters + method (boolean) + @return the word at that position (string) + """ + start, end = self.getWordBoundaries(line, index, useWordChars) + if direction == 1: + end = index + elif direction == 2: + start = index + if end > start: + text = self.text(line) + word = text[start:end] + else: + word = "" + return word + + def getWordLeft(self, line, index): + """ + Public method to get the word to the left of a position. + + @param line number of line to look at (int) + @param index position to look at (int) + @return the word to the left of that position (string) + """ + return self.getWord(line, index, 1) + + def getWordRight(self, line, index): + """ + Public method to get the word to the right of a position. + + @param line number of line to look at (int) + @param index position to look at (int) + @return the word to the right of that position (string) + """ + return self.getWord(line, index, 2) + + def getCurrentWord(self): + """ + Public method to get the word at the current position. + + @return the word at that current position (string) + """ + line, index = self.getCursorPosition() + return self.getWord(line, index) + + def getCurrentWordBoundaries(self): + """ + Public method to get the word boundaries at the current position. + + @return tuple with start and end indexes of the current word + (integer, integer) + """ + line, index = self.getCursorPosition() + return self.getWordBoundaries(line, index) + + def selectWord(self, line, index): + """ + Public method to select the word at a position. + + @param line number of line to look at (int) + @param index position to look at (int) + """ + start, end = self.getWordBoundaries(line, index) + self.setSelection(line, start, line, end) + + def selectCurrentWord(self): + """ + Public method to select the current word. + """ + line, index = self.getCursorPosition() + self.selectWord(line, index) + + def __getCharacter(self, pos): + """ + Private method to get the character to the left of the current position + in the current line. + + @param pos position to get character at (integer) + @return requested character or "", if there are no more (string) and + the next position (i.e. pos - 1) + """ + if pos <= 0: + return "", pos + + pos = self.positionBefore(pos) + ch = self.charAt(pos) + + # Don't go past the end of the previous line + if ch in ("\n", "\r"): + return "", pos + + return ch, pos + + def getSearchText(self, selectionOnly=False): + """ + Public method to determine the selection or the current word for the + next search operation. + + @param selectionOnly flag indicating that only selected text should be + returned (boolean) + @return selection or current word (string) + """ + if self.hasSelectedText(): + text = self.selectedText() + if "\r" in text or "\n" in text: + # the selection contains at least a newline, it is + # unlikely to be the expression to search for + return "" + + return text + + if not selectionOnly: + # no selected text, determine the word at the current position + return self.getCurrentWord() + + return "" + + def setSearchIndicator(self, startPos, indicLength): + """ + Public method to set a search indicator for the given range. + + @param startPos start position of the indicator (integer) + @param indicLength length of the indicator (integer) + """ + self.setIndicatorRange(self.searchIndicator, startPos, indicLength) + line = self.lineIndexFromPosition(startPos)[0] + if line not in self.__searchIndicatorLines: + self.__searchIndicatorLines.append(line) + + def clearSearchIndicators(self): + """ + Public method to clear all search indicators. + """ + self.clearAllIndicators(self.searchIndicator) + self.__markedText = "" + self.__searchIndicatorLines = [] + self.__markerMap.update() + + def __markOccurrences(self): + """ + Private method to mark all occurrences of the current word. + """ + word = self.getCurrentWord() + if not word: + self.clearSearchIndicators() + return + + if self.__markedText == word: + return + + self.clearSearchIndicators() + ok = self.findFirstTarget(word, False, self.caseSensitive(), True, 0, 0) + while ok: + tgtPos, tgtLen = self.getFoundTarget() + self.setSearchIndicator(tgtPos, tgtLen) + ok = self.findNextTarget() + self.__markedText = word + self.__markerMap.update() + + def getSearchIndicatorLines(self): + """ + Public method to get the lines containing a search indicator. + + @return list of lines containing a search indicator (list of integer) + """ + return self.__searchIndicatorLines[:] + + def updateMarkerMap(self): + """ + Public method to initiate an update of the marker map. + """ + self.__markerMap.update() + + ########################################################################### + ## Highlighting marker handling methods below + ########################################################################### + + def setHighlight(self, startLine, startIndex, endLine, endIndex): + """ + Public method to set a text highlight. + + @param startLine line of the highlight start + @type int + @param startIndex index of the highlight start + @type int + @param endLine line of the highlight end + @type int + @param endIndex index of the highlight end + @type int + """ + self.setIndicator( + self.highlightIndicator, startLine, startIndex, endLine, endIndex + ) + + def clearAllHighlights(self): + """ + Public method to clear all highlights. + """ + self.clearAllIndicators(self.highlightIndicator) + + def clearHighlight(self, startLine, startIndex, endLine, endIndex): + """ + Public method to clear a text highlight. + + @param startLine line of the highlight start + @type int + @param startIndex index of the highlight start + @type int + @param endLine line of the highlight end + @type int + @param endIndex index of the highlight end + @type int + """ + self.clearIndicator( + self.highlightIndicator, startLine, startIndex, endLine, endIndex + ) + + ########################################################################### + ## Comment handling methods below + ########################################################################### + + def __isCommentedLine(self, line, commentStr): + """ + Private method to check, if the given line is a comment line as + produced by the configured comment rules. + + @param line text of the line to check (string) + @param commentStr comment string to check against (string) + @return flag indicating a commented line (boolean) + """ + if Preferences.getEditor("CommentColumn0"): + return line.startswith(commentStr) + else: + return line.strip().startswith(commentStr) + + def toggleCommentBlock(self): + """ + Public slot to toggle the comment of a block. + + If the line of the cursor or the selection is not commented, it will + be commented. If it is commented, the comment block will be removed. + The later works independent of the current selection. + """ + if self.lexer_ is None or not self.lexer_.canBlockComment(): + return + + commentStr = self.lexer_.commentStr() + line, index = self.getCursorPosition() + + # check if line starts with our comment string (i.e. was commented + # by our comment...() slots + if self.hasSelectedText() and self.__isCommentedLine( + self.text(self.getSelection()[0]), commentStr + ): + self.uncommentLineOrSelection() + elif not self.__isCommentedLine(self.text(line), commentStr): + # it doesn't, so comment the line or selection + self.commentLineOrSelection() + else: + # determine the start of the comment block + begline = line + while begline > 0 and self.__isCommentedLine( + self.text(begline - 1), commentStr + ): + begline -= 1 + # determine the end of the comment block + endline = line + lines = self.lines() + while endline < lines and self.__isCommentedLine( + self.text(endline + 1), commentStr + ): + endline += 1 + + self.setSelection(begline, 0, endline, self.lineLength(endline)) + self.uncommentLineOrSelection() + + # reset the cursor + self.setCursorPosition(line, index - len(commentStr)) + + def commentLine(self): + """ + Public slot to comment the current line. + """ + if self.lexer_ is None or not self.lexer_.canBlockComment(): + return + + line, index = self.getCursorPosition() + self.beginUndoAction() + if Preferences.getEditor("CommentColumn0"): + self.insertAt(self.lexer_.commentStr(), line, 0) + else: + lineText = self.text(line) + pos = len(lineText.replace(lineText.lstrip(" \t"), "")) + self.insertAt(self.lexer_.commentStr(), line, pos) + self.endUndoAction() + + def uncommentLine(self): + """ + Public slot to uncomment the current line. + """ + if self.lexer_ is None or not self.lexer_.canBlockComment(): + return + + commentStr = self.lexer_.commentStr() + line, index = self.getCursorPosition() + + # check if line starts with our comment string (i.e. was commented + # by our comment...() slots + if not self.__isCommentedLine(self.text(line), commentStr): + return + + # now remove the comment string + self.beginUndoAction() + if Preferences.getEditor("CommentColumn0"): + self.setSelection(line, 0, line, len(commentStr)) + else: + lineText = self.text(line) + pos = len(lineText.replace(lineText.lstrip(" \t"), "")) + self.setSelection(line, pos, line, pos + len(commentStr)) + self.removeSelectedText() + self.endUndoAction() + + def commentSelection(self): + """ + Public slot to comment the current selection. + """ + if self.lexer_ is None or not self.lexer_.canBlockComment(): + return + + if not self.hasSelectedText(): + return + + commentStr = self.lexer_.commentStr() + + # get the selection boundaries + lineFrom, indexFrom, lineTo, indexTo = self.getSelection() + endLine = lineTo if indexTo else lineTo - 1 + + self.beginUndoAction() + # iterate over the lines + for line in range(lineFrom, endLine + 1): + if Preferences.getEditor("CommentColumn0"): + self.insertAt(commentStr, line, 0) + else: + lineText = self.text(line) + pos = len(lineText.replace(lineText.lstrip(" \t"), "")) + self.insertAt(commentStr, line, pos) + + # change the selection accordingly + self.setSelection(lineFrom, 0, endLine + 1, 0) + self.endUndoAction() + + def uncommentSelection(self): + """ + Public slot to uncomment the current selection. + """ + if self.lexer_ is None or not self.lexer_.canBlockComment(): + return + + if not self.hasSelectedText(): + return + + commentStr = self.lexer_.commentStr() + + # get the selection boundaries + lineFrom, indexFrom, lineTo, indexTo = self.getSelection() + endLine = lineTo if indexTo else lineTo - 1 + + self.beginUndoAction() + # iterate over the lines + for line in range(lineFrom, endLine + 1): + # check if line starts with our comment string (i.e. was commented + # by our comment...() slots + if not self.__isCommentedLine(self.text(line), commentStr): + continue + + if Preferences.getEditor("CommentColumn0"): + self.setSelection(line, 0, line, len(commentStr)) + else: + lineText = self.text(line) + pos = len(lineText.replace(lineText.lstrip(" \t"), "")) + self.setSelection(line, pos, line, pos + len(commentStr)) + self.removeSelectedText() + + # adjust selection start + if line == lineFrom: + indexFrom -= len(commentStr) + if indexFrom < 0: + indexFrom = 0 + + # adjust selection end + if line == lineTo: + indexTo -= len(commentStr) + if indexTo < 0: + indexTo = 0 + + # change the selection accordingly + self.setSelection(lineFrom, indexFrom, lineTo, indexTo) + self.endUndoAction() + + def commentLineOrSelection(self): + """ + Public slot to comment the current line or current selection. + """ + if self.hasSelectedText(): + self.commentSelection() + else: + self.commentLine() + + def uncommentLineOrSelection(self): + """ + Public slot to uncomment the current line or current selection. + """ + if self.hasSelectedText(): + self.uncommentSelection() + else: + self.uncommentLine() + + def streamCommentLine(self): + """ + Public slot to stream comment the current line. + """ + if self.lexer_ is None or not self.lexer_.canStreamComment(): + return + + commentStr = self.lexer_.streamCommentStr() + line, index = self.getCursorPosition() + + self.beginUndoAction() + self.insertAt(commentStr["end"], line, self.lineLength(line)) + self.insertAt(commentStr["start"], line, 0) + self.endUndoAction() + + def streamCommentSelection(self): + """ + Public slot to comment the current selection. + """ + if self.lexer_ is None or not self.lexer_.canStreamComment(): + return + + if not self.hasSelectedText(): + return + + commentStr = self.lexer_.streamCommentStr() + + # get the selection boundaries + lineFrom, indexFrom, lineTo, indexTo = self.getSelection() + if indexTo == 0: + endLine = lineTo - 1 + endIndex = self.lineLength(endLine) + else: + endLine = lineTo + endIndex = indexTo + + self.beginUndoAction() + self.insertAt(commentStr["end"], endLine, endIndex) + self.insertAt(commentStr["start"], lineFrom, indexFrom) + + # change the selection accordingly + if indexTo > 0: + indexTo += len(commentStr["end"]) + if lineFrom == endLine: + indexTo += len(commentStr["start"]) + self.setSelection(lineFrom, indexFrom, lineTo, indexTo) + self.endUndoAction() + + def streamCommentLineOrSelection(self): + """ + Public slot to stream comment the current line or current selection. + """ + if self.hasSelectedText(): + self.streamCommentSelection() + else: + self.streamCommentLine() + + def boxCommentLine(self): + """ + Public slot to box comment the current line. + """ + if self.lexer_ is None or not self.lexer_.canBoxComment(): + return + + commentStr = self.lexer_.boxCommentStr() + line, index = self.getCursorPosition() + + eol = self.getLineSeparator() + self.beginUndoAction() + self.insertAt(eol, line, self.lineLength(line)) + self.insertAt(commentStr["end"], line + 1, 0) + self.insertAt(commentStr["middle"], line, 0) + self.insertAt(eol, line, 0) + self.insertAt(commentStr["start"], line, 0) + self.endUndoAction() + + def boxCommentSelection(self): + """ + Public slot to box comment the current selection. + """ + if self.lexer_ is None or not self.lexer_.canBoxComment(): + return + + if not self.hasSelectedText(): + return + + commentStr = self.lexer_.boxCommentStr() + + # get the selection boundaries + lineFrom, indexFrom, lineTo, indexTo = self.getSelection() + endLine = lineTo if indexTo else lineTo - 1 + + self.beginUndoAction() + # iterate over the lines + for line in range(lineFrom, endLine + 1): + self.insertAt(commentStr["middle"], line, 0) + + # now do the comments before and after the selection + eol = self.getLineSeparator() + self.insertAt(eol, endLine, self.lineLength(endLine)) + self.insertAt(commentStr["end"], endLine + 1, 0) + self.insertAt(eol, lineFrom, 0) + self.insertAt(commentStr["start"], lineFrom, 0) + + # change the selection accordingly + self.setSelection(lineFrom, 0, endLine + 3, 0) + self.endUndoAction() + + def boxCommentLineOrSelection(self): + """ + Public slot to box comment the current line or current selection. + """ + if self.hasSelectedText(): + self.boxCommentSelection() + else: + self.boxCommentLine() + + ########################################################################### + ## Indentation handling methods below + ########################################################################### + + def __indentLine(self, indent=True): + """ + Private method to indent or unindent the current line. + + @param indent flag indicating an indent operation (boolean) + <br />If the flag is true, an indent operation is performed. + Otherwise the current line is unindented. + """ + line, index = self.getCursorPosition() + self.beginUndoAction() + if indent: + self.indent(line) + else: + self.unindent(line) + self.endUndoAction() + if indent: + self.setCursorPosition(line, index + self.indentationWidth()) + else: + self.setCursorPosition(line, index - self.indentationWidth()) + + def __indentSelection(self, indent=True): + """ + Private method to indent or unindent the current selection. + + @param indent flag indicating an indent operation (boolean) + <br />If the flag is true, an indent operation is performed. + Otherwise the current line is unindented. + """ + if not self.hasSelectedText(): + return + + # get the selection + lineFrom, indexFrom, lineTo, indexTo = self.getSelection() + endLine = lineTo if indexTo else lineTo - 1 + + self.beginUndoAction() + # iterate over the lines + for line in range(lineFrom, endLine + 1): + if indent: + self.indent(line) + else: + self.unindent(line) + self.endUndoAction() + if indent: + if indexTo == 0: + self.setSelection( + lineFrom, indexFrom + self.indentationWidth(), lineTo, 0 + ) + else: + self.setSelection( + lineFrom, + indexFrom + self.indentationWidth(), + lineTo, + indexTo + self.indentationWidth(), + ) + else: + indexStart = indexFrom - self.indentationWidth() + if indexStart < 0: + indexStart = 0 + indexEnd = indexTo - self.indentationWidth() + if indexEnd < 0: + indexEnd = 0 + self.setSelection(lineFrom, indexStart, lineTo, indexEnd) + + def indentLineOrSelection(self): + """ + Public slot to indent the current line or current selection. + """ + if self.hasSelectedText(): + self.__indentSelection(True) + else: + self.__indentLine(True) + + def unindentLineOrSelection(self): + """ + Public slot to unindent the current line or current selection. + """ + if self.hasSelectedText(): + self.__indentSelection(False) + else: + self.__indentLine(False) + + def smartIndentLineOrSelection(self): + """ + Public slot to indent current line smartly. + """ + if self.hasSelectedText(): + if self.lexer_ and self.lexer_.hasSmartIndent(): + self.lexer_.smartIndentSelection(self) + else: + self.__indentSelection(True) + else: + if self.lexer_ and self.lexer_.hasSmartIndent(): + self.lexer_.smartIndentLine(self) + else: + self.__indentLine(True) + + def gotoLine(self, line, pos=1, firstVisible=False, expand=False): + """ + Public slot to jump to the beginning of a line. + + @param line line number to go to + @type int + @param pos position in line to go to + @type int + @param firstVisible flag indicating to make the line the first + visible line + @type bool + @param expand flag indicating to expand all folds + @type bool + """ + self.setCursorPosition(line - 1, pos - 1) + if firstVisible: + self.ensureVisibleTop(line, expand) + else: + self.ensureVisible(line, expand) + + def __textChanged(self): + """ + Private slot to handle a change of the editor text. + + This slot defers the handling to the next time the event loop + is run in order to ensure, that cursor position has been updated + by the underlying Scintilla editor. + """ + QTimer.singleShot(0, self.__saveLastEditPosition) + + def __saveLastEditPosition(self): + """ + Private slot to record the last edit position. + """ + self.__lastEditPosition = self.getCursorPosition() + self.lastEditPositionAvailable.emit() + + def isLastEditPositionAvailable(self): + """ + Public method to check, if a last edit position is available. + + @return flag indicating availability (boolean) + """ + return self.__lastEditPosition is not None + + def gotoLastEditPosition(self): + """ + Public method to move the cursor to the last edit position. + """ + self.setCursorPosition(*self.__lastEditPosition) + self.ensureVisible(self.__lastEditPosition[0]) + + def gotoMethodClass(self, goUp=False): + """ + Public method to go to the next Python method or class definition. + + @param goUp flag indicating the move direction (boolean) + """ + if self.isPyFile() or self.isRubyFile(): + lineNo = self.getCursorPosition()[0] + line = self.text(lineNo) + if line.strip().startswith(("class ", "def ", "module ")): + if goUp: + lineNo -= 1 + else: + lineNo += 1 + while True: + if goUp and lineNo < 0: + self.setCursorPosition(0, 0) + self.ensureVisible(0) + return + elif not goUp and lineNo == self.lines(): + lineNo = self.lines() - 1 + self.setCursorPosition(lineNo, self.lineLength(lineNo)) + self.ensureVisible(lineNo) + return + + line = self.text(lineNo) + if line.strip().startswith(("class ", "def ", "module ")): + # try 'def ' first because it occurs more often + first = line.find("def ") + if first > -1: + first += 4 + else: + first = line.find("class ") + if first > -1: + first += 6 + else: + first = line.find("module ") + 7 + match = re.search("[:(]", line) + if match: + end = match.start() + else: + end = self.lineLength(lineNo) - 1 + self.setSelection(lineNo, first, lineNo, end) + self.ensureVisible(lineNo) + return + + if goUp: + lineNo -= 1 + else: + lineNo += 1 + + ########################################################################### + ## Setup methods below + ########################################################################### + + def readSettings(self): + """ + Public slot to read the settings into our lexer. + """ + # read the lexer settings and reinit the properties + if self.lexer_ is not None: + self.lexer_.readSettings(Preferences.getSettings(), "Scintilla") + if self.lexer_.hasSubstyles(): + self.lexer_.readSubstyles(self) + self.lexer_.initProperties() + + self.lexer_.setDefaultColor(self.lexer_.color(0)) + self.lexer_.setDefaultPaper(self.lexer_.paper(0)) + + self.__bindLexer(self.fileName) + self.recolor() + + # read the typing completer settings + if self.completer is not None: + self.completer.readSettings() + + # set the line marker colours or pixmap + if Preferences.getEditor("LineMarkersBackground"): + self.markerDefine(QsciScintilla.MarkerSymbol.Background, self.currentline) + self.markerDefine(QsciScintilla.MarkerSymbol.Background, self.errorline) + self.__setLineMarkerColours() + else: + self.markerDefine( + UI.PixmapCache.getPixmap("currentLineMarker"), self.currentline + ) + self.markerDefine( + UI.PixmapCache.getPixmap("errorLineMarker"), self.errorline + ) + + # set the text display + self.__setTextDisplay() + + # set margin 0 and 2 configuration + self.__setMarginsDisplay() + + # set the auto-completion function + self.__acCache.setSize(Preferences.getEditor("AutoCompletionCacheSize")) + self.__acCache.setMaximumCacheTime( + Preferences.getEditor("AutoCompletionCacheTime") + ) + self.__acCacheEnabled = Preferences.getEditor("AutoCompletionCacheEnabled") + acTimeout = Preferences.getEditor("AutoCompletionTimeout") + if acTimeout != self.__acTimer.interval: + self.__acTimer.setInterval(acTimeout) + self.__setAutoCompletion() + + # set the calltips function + self.__setCallTips() + + # set the autosave flags + self.autosaveEnabled = Preferences.getEditor("AutosaveInterval") > 0 + + if Preferences.getEditor("MiniContextMenu") != self.miniMenu: + # regenerate context menu + self.__initContextMenu() + else: + # set checked context menu items + self.menuActs["AutoCompletionEnable"].setChecked( + self.autoCompletionThreshold() != -1 + ) + self.menuActs["MonospacedFont"].setChecked(self.useMonospaced) + self.menuActs["AutosaveEnable"].setChecked( + self.autosaveEnabled and not self.autosaveManuallyDisabled + ) + + # regenerate the margins context menu(s) + self.__initContextMenuMargins() + + if Preferences.getEditor("MarkOccurrencesEnabled"): + self.__markOccurrencesTimer.setInterval( + Preferences.getEditor("MarkOccurrencesTimeout") + ) + else: + self.__markOccurrencesTimer.stop() + self.clearSearchIndicators() + + if Preferences.getEditor("OnlineSyntaxCheck"): + self.__onlineSyntaxCheckTimer.setInterval( + Preferences.getEditor("OnlineSyntaxCheckInterval") * 1000 + ) + else: + self.__onlineSyntaxCheckTimer.stop() + + if Preferences.getEditor("OnlineChangeTrace"): + self.__onlineChangeTraceTimer.setInterval( + Preferences.getEditor("OnlineChangeTraceInterval") + ) + else: + self.__onlineChangeTraceTimer.stop() + self.__deleteAllChangeMarkers() + self.markerDefine( + self.__createChangeMarkerPixmap("OnlineChangeTraceMarkerUnsaved"), + self.__changeMarkerUnsaved, + ) + self.markerDefine( + self.__createChangeMarkerPixmap("OnlineChangeTraceMarkerSaved"), + self.__changeMarkerSaved, + ) + + # refresh the annotations display + self.__refreshAnnotations() + + self.__markerMap.setMapPosition(Preferences.getEditor("ShowMarkerMapOnRight")) + self.__markerMap.initColors() + + self.setLanguage(self.fileName, propagate=False) + + self.settingsRead.emit() + + def __setLineMarkerColours(self): + """ + Private method to set the line marker colours. + """ + self.setMarkerForegroundColor( + Preferences.getEditorColour("CurrentMarker"), self.currentline + ) + self.setMarkerBackgroundColor( + Preferences.getEditorColour("CurrentMarker"), self.currentline + ) + self.setMarkerForegroundColor( + Preferences.getEditorColour("ErrorMarker"), self.errorline + ) + self.setMarkerBackgroundColor( + Preferences.getEditorColour("ErrorMarker"), self.errorline + ) + + def __setMarginsDisplay(self): + """ + Private method to configure margins 0 and 2. + """ + # set the settings for all margins + self.setMarginsFont(Preferences.getEditorOtherFonts("MarginsFont")) + self.setMarginsForegroundColor(Preferences.getEditorColour("MarginsForeground")) + self.setMarginsBackgroundColor(Preferences.getEditorColour("MarginsBackground")) + + # reset standard margins settings + for margin in range(5): + self.setMarginLineNumbers(margin, False) + self.setMarginMarkerMask(margin, 0) + self.setMarginWidth(margin, 0) + self.setMarginSensitivity(margin, False) + + # set marker margin(s) settings + self.__bmMargin = 0 + self.__linenoMargin = 1 + self.__bpMargin = 2 + self.__foldMargin = 3 + self.__indicMargin = 4 + + marginBmMask = 1 << self.bookmark + self.setMarginWidth(self.__bmMargin, 16) + self.setMarginSensitivity(self.__bmMargin, True) + self.setMarginMarkerMask(self.__bmMargin, marginBmMask) + + marginBpMask = ( + (1 << self.breakpoint) + | (1 << self.cbreakpoint) + | (1 << self.tbreakpoint) + | (1 << self.tcbreakpoint) + | (1 << self.dbreakpoint) + ) + self.setMarginWidth(self.__bpMargin, 16) + self.setMarginSensitivity(self.__bpMargin, True) + self.setMarginMarkerMask(self.__bpMargin, marginBpMask) + + marginIndicMask = ( + (1 << self.syntaxerror) + | (1 << self.notcovered) + | (1 << self.taskmarker) + | (1 << self.warning) + | (1 << self.__changeMarkerUnsaved) + | (1 << self.__changeMarkerSaved) + | (1 << self.currentline) + | (1 << self.errorline) + ) + self.setMarginWidth(self.__indicMargin, 16) + self.setMarginSensitivity(self.__indicMargin, True) + self.setMarginMarkerMask(self.__indicMargin, marginIndicMask) + + # set linenumber margin settings + linenoMargin = Preferences.getEditor("LinenoMargin") + self.setMarginLineNumbers(self.__linenoMargin, linenoMargin) + if linenoMargin: + self.__resizeLinenoMargin() + else: + self.setMarginWidth(self.__linenoMargin, 0) + + # set folding margin settings + if Preferences.getEditor("FoldingMargin"): + self.setMarginWidth(self.__foldMargin, 16) + folding = Preferences.getEditor("FoldingStyle") + self.setFolding(folding, self.__foldMargin) + self.setFoldMarginColors( + Preferences.getEditorColour("FoldmarginBackground"), + Preferences.getEditorColour("FoldmarginBackground"), + ) + self.setFoldMarkersColors( + Preferences.getEditorColour("FoldMarkersForeground"), + Preferences.getEditorColour("FoldMarkersBackground"), + ) + else: + self.setMarginWidth(self.__foldMargin, 0) + self.setFolding( + QsciScintilla.FoldStyle.NoFoldStyle.value, self.__foldMargin + ) + + def __resizeLinenoMargin(self): + """ + Private slot to resize the line numbers margin. + """ + linenoMargin = Preferences.getEditor("LinenoMargin") + if linenoMargin: + self.setMarginWidth(self.__linenoMargin, "8" * (len(str(self.lines())) + 1)) + + def __setTabAndIndent(self): + """ + Private method to set indentation size and style and tab width. + """ + self.setTabWidth(self.__getEditorConfig("TabWidth")) + self.setIndentationWidth(self.__getEditorConfig("IndentWidth")) + if self.lexer_ and self.lexer_.alwaysKeepTabs(): + self.setIndentationsUseTabs(True) + else: + self.setIndentationsUseTabs(self.__getEditorConfig("TabForIndentation")) + + def __setTextDisplay(self): + """ + Private method to configure the text display. + """ + self.__setTabAndIndent() + + self.setTabIndents(Preferences.getEditor("TabIndents")) + self.setBackspaceUnindents(Preferences.getEditor("TabIndents")) + self.setIndentationGuides(Preferences.getEditor("IndentationGuides")) + self.setIndentationGuidesBackgroundColor( + Preferences.getEditorColour("IndentationGuidesBackground") + ) + self.setIndentationGuidesForegroundColor( + Preferences.getEditorColour("IndentationGuidesForeground") + ) + if Preferences.getEditor("ShowWhitespace"): + self.setWhitespaceVisibility(QsciScintilla.WhitespaceVisibility.WsVisible) + with contextlib.suppress(AttributeError): + self.setWhitespaceForegroundColor( + Preferences.getEditorColour("WhitespaceForeground") + ) + self.setWhitespaceBackgroundColor( + Preferences.getEditorColour("WhitespaceBackground") + ) + self.setWhitespaceSize(Preferences.getEditor("WhitespaceSize")) + else: + self.setWhitespaceVisibility(QsciScintilla.WhitespaceVisibility.WsInvisible) + self.setEolVisibility(Preferences.getEditor("ShowEOL")) + self.setAutoIndent(Preferences.getEditor("AutoIndentation")) + if Preferences.getEditor("BraceHighlighting"): + self.setBraceMatching(QsciScintilla.BraceMatch.SloppyBraceMatch) + else: + self.setBraceMatching(QsciScintilla.BraceMatch.NoBraceMatch) + self.setMatchedBraceForegroundColor( + Preferences.getEditorColour("MatchingBrace") + ) + self.setMatchedBraceBackgroundColor( + Preferences.getEditorColour("MatchingBraceBack") + ) + self.setUnmatchedBraceForegroundColor( + Preferences.getEditorColour("NonmatchingBrace") + ) + self.setUnmatchedBraceBackgroundColor( + Preferences.getEditorColour("NonmatchingBraceBack") + ) + if Preferences.getEditor("CustomSelectionColours"): + self.setSelectionBackgroundColor( + Preferences.getEditorColour("SelectionBackground") + ) + else: + self.setSelectionBackgroundColor( + QApplication.palette().color(QPalette.ColorRole.Highlight) + ) + if Preferences.getEditor("ColourizeSelText"): + self.resetSelectionForegroundColor() + elif Preferences.getEditor("CustomSelectionColours"): + self.setSelectionForegroundColor( + Preferences.getEditorColour("SelectionForeground") + ) + else: + self.setSelectionForegroundColor( + QApplication.palette().color(QPalette.ColorRole.HighlightedText) + ) + self.setSelectionToEol(Preferences.getEditor("ExtendSelectionToEol")) + self.setCaretForegroundColor(Preferences.getEditorColour("CaretForeground")) + self.setCaretLineBackgroundColor( + Preferences.getEditorColour("CaretLineBackground") + ) + self.setCaretLineVisible(Preferences.getEditor("CaretLineVisible")) + self.setCaretLineAlwaysVisible(Preferences.getEditor("CaretLineAlwaysVisible")) + self.caretWidth = Preferences.getEditor("CaretWidth") + self.setCaretWidth(self.caretWidth) + self.caretLineFrameWidth = Preferences.getEditor("CaretLineFrameWidth") + self.setCaretLineFrameWidth(self.caretLineFrameWidth) + self.useMonospaced = Preferences.getEditor("UseMonospacedFont") + self.setMonospaced(self.useMonospaced) + edgeMode = Preferences.getEditor("EdgeMode") + edge = QsciScintilla.EdgeMode(edgeMode) + self.setEdgeMode(edge) + if edgeMode: + self.setEdgeColumn(Preferences.getEditor("EdgeColumn")) + self.setEdgeColor(Preferences.getEditorColour("Edge")) + + wrapVisualFlag = Preferences.getEditor("WrapVisualFlag") + self.setWrapMode(Preferences.getEditor("WrapLongLinesMode")) + self.setWrapVisualFlags(wrapVisualFlag, wrapVisualFlag) + self.setWrapIndentMode(Preferences.getEditor("WrapIndentMode")) + self.setWrapStartIndent(Preferences.getEditor("WrapStartIndent")) + + self.zoomTo(Preferences.getEditor("ZoomFactor")) + + self.searchIndicator = QsciScintilla.INDIC_CONTAINER + self.indicatorDefine( + self.searchIndicator, + QsciScintilla.INDIC_BOX, + Preferences.getEditorColour("SearchMarkers"), + ) + if ( + not Preferences.getEditor("SearchMarkersEnabled") + and not Preferences.getEditor("QuickSearchMarkersEnabled") + and not Preferences.getEditor("MarkOccurrencesEnabled") + ): + self.clearAllIndicators(self.searchIndicator) + + self.spellingIndicator = QsciScintilla.INDIC_CONTAINER + 1 + self.indicatorDefine( + self.spellingIndicator, + QsciScintilla.INDIC_SQUIGGLE, + Preferences.getEditorColour("SpellingMarkers"), + ) + self.__setSpelling() + + self.highlightIndicator = QsciScintilla.INDIC_CONTAINER + 2 + self.indicatorDefine( + self.highlightIndicator, + QsciScintilla.INDIC_FULLBOX, + Preferences.getEditorColour("HighlightMarker"), + ) + + self.setCursorFlashTime(QApplication.cursorFlashTime()) + + with contextlib.suppress(AttributeError): + if Preferences.getEditor("AnnotationsEnabled"): + self.setAnnotationDisplay( + QsciScintilla.AnnotationDisplay.AnnotationBoxed + ) + else: + self.setAnnotationDisplay( + QsciScintilla.AnnotationDisplay.AnnotationHidden + ) + self.__setAnnotationStyles() + + if Preferences.getEditor("OverrideEditAreaColours"): + self.setColor(Preferences.getEditorColour("EditAreaForeground")) + self.setPaper(Preferences.getEditorColour("EditAreaBackground")) + + self.setVirtualSpaceOptions(Preferences.getEditor("VirtualSpaceOptions")) + + if Preferences.getEditor("MouseHoverHelp"): + self.SendScintilla( + QsciScintilla.SCI_SETMOUSEDWELLTIME, + Preferences.getEditor("MouseHoverTimeout"), + ) + else: + self.SendScintilla( + QsciScintilla.SCI_SETMOUSEDWELLTIME, QsciScintilla.SC_TIME_FOREVER + ) + + # to avoid errors due to line endings by pasting + self.SendScintilla(QsciScintilla.SCI_SETPASTECONVERTENDINGS, True) + + self.__markerMap.setEnabled(True) + + def __setEolMode(self): + """ + Private method to configure the eol mode of the editor. + """ + if ( + self.fileName + and self.project.isOpen() + and self.project.isProjectFile(self.fileName) + ): + eolMode = self.__getEditorConfig("EOLMode", nodefault=True) + if eolMode is None: + eolStr = self.project.getEolString() + self.setEolModeByEolString(eolStr) + else: + self.setEolMode(eolMode) + else: + eolMode = self.__getEditorConfig("EOLMode") + self.setEolMode(eolMode) + self.__eolChanged() + + def __setAutoCompletion(self): + """ + Private method to configure the autocompletion function. + """ + if self.lexer_: + self.setAutoCompletionFillupsEnabled( + Preferences.getEditor("AutoCompletionFillups") + ) + self.setAutoCompletionCaseSensitivity( + Preferences.getEditor("AutoCompletionCaseSensitivity") + ) + self.setAutoCompletionReplaceWord( + Preferences.getEditor("AutoCompletionReplaceWord") + ) + self.setAutoCompletionThreshold(0) + if Preferences.getEditor("AutoCompletionShowSingle"): + self.setAutoCompletionUseSingle( + QsciScintilla.AutoCompletionUseSingle.AcusAlways + ) + else: + self.setAutoCompletionUseSingle( + QsciScintilla.AutoCompletionUseSingle.AcusNever + ) + autoCompletionSource = Preferences.getEditor("AutoCompletionSource") + if autoCompletionSource == QsciScintilla.AutoCompletionSource.AcsDocument: + self.setAutoCompletionSource(QsciScintilla.AutoCompletionSource.AcsDocument) + elif autoCompletionSource == QsciScintilla.AutoCompletionSource.AcsAPIs: + self.setAutoCompletionSource(QsciScintilla.AutoCompletionSource.AcsAPIs) + else: + self.setAutoCompletionSource(QsciScintilla.AutoCompletionSource.AcsAll) + + self.setAutoCompletionWidgetSize( + Preferences.getEditor("AutoCompletionMaxChars"), + Preferences.getEditor("AutoCompletionMaxLines"), + ) + + def __setCallTips(self): + """ + Private method to configure the calltips function. + """ + self.setCallTipsBackgroundColor( + Preferences.getEditorColour("CallTipsBackground") + ) + self.setCallTipsForegroundColor( + Preferences.getEditorColour("CallTipsForeground") + ) + self.setCallTipsHighlightColor(Preferences.getEditorColour("CallTipsHighlight")) + self.setCallTipsVisible(Preferences.getEditor("CallTipsVisible")) + calltipsStyle = Preferences.getEditor("CallTipsStyle") + with contextlib.suppress(AttributeError): + self.setCallTipsPosition(Preferences.getEditor("CallTipsPosition")) + + if Preferences.getEditor("CallTipsEnabled"): + if calltipsStyle == QsciScintilla.CallTipsStyle.CallTipsNoContext: + self.setCallTipsStyle(QsciScintilla.CallTipsStyle.CallTipsNoContext) + elif ( + calltipsStyle + == QsciScintilla.CallTipsStyle.CallTipsNoAutoCompletionContext + ): + self.setCallTipsStyle( + QsciScintilla.CallTipsStyle.CallTipsNoAutoCompletionContext + ) + else: + self.setCallTipsStyle(QsciScintilla.CallTipsStyle.CallTipsContext) + else: + self.setCallTipsStyle(QsciScintilla.CallTipsStyle.CallTipsNone) + + ########################################################################### + ## Autocompletion handling methods below + ########################################################################### + + def canAutoCompleteFromAPIs(self): + """ + Public method to check for API availablity. + + @return flag indicating autocompletion from APIs is available (boolean) + """ + return self.acAPI + + def autoCompleteQScintilla(self): + """ + Public method to perform an autocompletion using QScintilla methods. + """ + self.__acText = " " # Prevent long running ACs to add results + self.__acWatchdog.stop() + if self.__acCompletions: + return + + acs = Preferences.getEditor("AutoCompletionSource") + if acs == QsciScintilla.AutoCompletionSource.AcsDocument: + self.autoCompleteFromDocument() + elif acs == QsciScintilla.AutoCompletionSource.AcsAPIs: + self.autoCompleteFromAPIs() + elif acs == QsciScintilla.AutoCompletionSource.AcsAll: + self.autoCompleteFromAll() + else: + EricMessageBox.information( + self, + self.tr("Autocompletion"), + self.tr( + """Autocompletion is not available because""" + """ there is no autocompletion source set.""" + ), + ) + + def setAutoCompletionEnabled(self, enable): + """ + Public method to enable/disable autocompletion. + + @param enable flag indicating the desired autocompletion status + (boolean) + """ + if enable: + autoCompletionSource = Preferences.getEditor("AutoCompletionSource") + if autoCompletionSource == QsciScintilla.AutoCompletionSource.AcsDocument: + self.setAutoCompletionSource( + QsciScintilla.AutoCompletionSource.AcsDocument + ) + elif autoCompletionSource == QsciScintilla.AutoCompletionSource.AcsAPIs: + self.setAutoCompletionSource(QsciScintilla.AutoCompletionSource.AcsAPIs) + else: + self.setAutoCompletionSource(QsciScintilla.AutoCompletionSource.AcsAll) + + def __toggleAutoCompletionEnable(self): + """ + Private slot to handle the Enable Autocompletion context menu entry. + """ + if self.menuActs["AutoCompletionEnable"].isChecked(): + self.setAutoCompletionEnabled(True) + else: + self.setAutoCompletionEnabled(False) + + ################################################################# + ## Support for autocompletion hook methods + ################################################################# + + def __charAdded(self, charNumber): + """ + Private slot called to handle the user entering a character. + + @param charNumber value of the character entered (integer) + """ + char = chr(charNumber) + # update code documentation viewer + if char == "(" and Preferences.getDocuViewer("ShowInfoOnOpenParenthesis"): + self.vm.showEditorInfo(self) + + self.__delayedDocstringMenuPopup(self.getCursorPosition()) + + if self.isListActive(): + if self.__isStartChar(char): + self.cancelList() + self.autoComplete(auto=True, context=True) + return + elif char == "(": + self.cancelList() + else: + self.__acTimer.stop() + + if ( + self.callTipsStyle() != QsciScintilla.CallTipsStyle.CallTipsNone + and self.lexer_ is not None + and chr(charNumber) in "()" + ): + self.callTip() + + if not self.isCallTipActive(): + char = chr(charNumber) + if self.__isStartChar(char): + self.autoComplete(auto=True, context=True) + return + + line, col = self.getCursorPosition() + txt = self.getWordLeft(line, col) + if len(txt) >= Preferences.getEditor("AutoCompletionThreshold"): + self.autoComplete(auto=True, context=False) + return + + def __isStartChar(self, ch): + """ + Private method to check, if a character is an autocompletion start + character. + + @param ch character to be checked (one character string) + @return flag indicating the result (boolean) + """ + if self.lexer_ is None: + return False + + wseps = self.lexer_.autoCompletionWordSeparators() + return any(wsep.endswith(ch) for wsep in wseps) + + def __autocompletionCancelled(self): + """ + Private slot to handle the cancellation of an auto-completion list. + """ + self.__acWatchdog.stop() + + self.__acText = "" + + ################################################################# + ## auto-completion hook interfaces + ################################################################# + + def addCompletionListHook(self, key, func, asynchroneous=False): + """ + Public method to set an auto-completion list provider. + + @param key name of the provider + @type str + @param func function providing completion list. func + should be a function taking a reference to the editor and + a boolean indicating to complete a context. It should return + the possible completions as a list of strings. + @type function(editor, bool) -> list of str in case async is False + and function(editor, bool, str) returning nothing in case async + is True + @param asynchroneous flag indicating an asynchroneous function + @type bool + """ + if ( + key in self.__completionListHookFunctions + or key in self.__completionListAsyncHookFunctions + ): + # it was already registered + EricMessageBox.warning( + self, + self.tr("Auto-Completion Provider"), + self.tr( + """The completion list provider '{0}' was already""" + """ registered. Ignoring duplicate request.""" + ).format(key), + ) + return + + if asynchroneous: + self.__completionListAsyncHookFunctions[key] = func + else: + self.__completionListHookFunctions[key] = func + + def removeCompletionListHook(self, key): + """ + Public method to remove a previously registered completion list + provider. + + @param key name of the provider + @type str + """ + if key in self.__completionListHookFunctions: + del self.__completionListHookFunctions[key] + elif key in self.__completionListAsyncHookFunctions: + del self.__completionListAsyncHookFunctions[key] + + def getCompletionListHook(self, key): + """ + Public method to get the registered completion list provider. + + @param key name of the provider + @type str + @return function providing completion list + @rtype function or None + """ + return self.__completionListHookFunctions.get( + key + ) or self.__completionListAsyncHookFunctions.get(key) + + def autoComplete(self, auto=False, context=True): + """ + Public method to start auto-completion. + + @param auto flag indicating a call from the __charAdded method + (boolean) + @param context flag indicating to complete a context (boolean) + """ + if auto and not Preferences.getEditor("AutoCompletionEnabled"): + # auto-completion is disabled + return + + if self.isListActive(): + self.cancelList() + + if ( + self.__completionListHookFunctions + or self.__completionListAsyncHookFunctions + ): + # Avoid delayed auto-completion after cursor repositioning + self.__acText = self.__getAcText() + if auto and Preferences.getEditor("AutoCompletionTimeout"): + self.__acTimer.stop() + self.__acContext = context + self.__acTimer.start() + else: + self.__autoComplete(auto, context) + elif not auto or ( + self.autoCompletionSource() != QsciScintilla.AutoCompletionSource.AcsNone + ): + self.autoCompleteQScintilla() + + def __getAcText(self): + """ + Private method to get the text from cursor position for autocompleting. + + @return text left of cursor position + @rtype str + """ + line, col = self.getCursorPosition() + text = self.text(line) + try: + acText = ( + self.getWordLeft(line, col - 1) + text[col - 1] + if self.__isStartChar(text[col - 1]) + else self.getWordLeft(line, col) + ) + except IndexError: + acText = "" + + return acText + + def __autoComplete(self, auto=True, context=None): + """ + Private method to start auto-completion via plug-ins. + + @param auto flag indicating a call from the __charAdded method + (boolean) + @param context flag indicating to complete a context + @type bool or None + """ + self.__acCompletions.clear() + self.__acCompletionsFinished = 0 + + # Suppress empty completions + if auto and self.__acText == "": + return + + completions = ( + self.__acCache.get(self.__acText) if self.__acCacheEnabled else None + ) + if completions is not None: + # show list with cached entries + if self.isListActive(): + self.cancelList() + + self.__showCompletionsList(completions) + else: + if context is None: + context = self.__acContext + + for key in self.__completionListAsyncHookFunctions: + self.__completionListAsyncHookFunctions[key]( + self, context, self.__acText + ) + + for key in self.__completionListHookFunctions: + completions = self.__completionListHookFunctions[key](self, context) + self.completionsListReady(completions, self.__acText) + + if Preferences.getEditor("AutoCompletionScintillaOnFail"): + self.__acWatchdog.start() + + def completionsListReady(self, completions, acText): + """ + Public method to show the completions determined by a completions + provider. + + @param completions list of possible completions + @type list of str or set of str + @param acText text to be completed + @type str + """ + currentWord = self.__getAcText() or " " + # process the list only, if not already obsolete ... + if acText != self.__acText or not self.__acText.endswith(currentWord): + # Suppress auto-completion done by QScintilla as fallback + self.__acWatchdog.stop() + return + + self.__acCompletions.update(set(completions)) + + self.__acCompletionsFinished += 1 + # Got all results from auto completer? + if self.__acCompletionsFinished >= ( + len(self.__completionListAsyncHookFunctions) + + len(self.__completionListHookFunctions) + ): + self.__acWatchdog.stop() + + # Autocomplete with QScintilla if no results present + if ( + Preferences.getEditor("AutoCompletionScintillaOnFail") + and not self.__acCompletions + ): + self.autoCompleteQScintilla() + return + + # ... or completions are not empty + if not bool(completions): + return + + if self.isListActive(): + self.cancelList() + + if self.__acCompletions: + if self.__acCacheEnabled: + self.__acCache.add(acText, set(self.__acCompletions)) + self.__showCompletionsList(self.__acCompletions) + + def __showCompletionsList(self, completions): + """ + Private method to show the completions list. + + @param completions completions to be shown + @type list of str or set of str + """ + acCompletions = ( + sorted(completions, key=self.__replaceLeadingUnderscores) + if Preferences.getEditor("AutoCompletionReversedList") + else sorted(completions) + ) + self.showUserList(EditorAutoCompletionListID, acCompletions) + + def __replaceLeadingUnderscores(self, txt): + """ + Private method to replace the first two underlines for invers sorting. + + @param txt completion text + @type str + @return modified completion text + @rtype str + """ + if txt.startswith("_"): + return txt[:2].replace("_", "~") + txt[2:] + else: + return txt + + def __clearCompletionsCache(self): + """ + Private method to clear the auto-completions cache. + """ + self.__acCache.clear() + + def __completionListSelected(self, listId, txt): + """ + Private slot to handle the selection from the completion list. + + @param listId the ID of the user list (should be 1 or 2) (integer) + @param txt the selected text (string) + """ + # custom completions via plug-ins + if listId == EditorAutoCompletionListID: + lst = txt.split() + if len(lst) > 1: + txt = lst[0] + + self.beginUndoAction() + if Preferences.getEditor("AutoCompletionReplaceWord"): + self.selectCurrentWord() + self.removeSelectedText() + line, col = self.getCursorPosition() + else: + line, col = self.getCursorPosition() + wLeft = self.getWordLeft(line, col) + if not txt.startswith(wLeft): + self.selectCurrentWord() + self.removeSelectedText() + line, col = self.getCursorPosition() + elif wLeft: + txt = txt[len(wLeft) :] + + if txt and txt[0] in "'\"": + # New in jedi 0.16: AC of dict keys + txt = txt[1:] + self.insert(txt) + self.endUndoAction() + self.setCursorPosition(line, col + len(txt)) + + # template completions + elif listId == TemplateCompletionListID: + self.__applyTemplate(txt, self.getLanguage()) + + # 'goto reference' completions + elif listId == ReferencesListID: + with contextlib.suppress(ValueError, IndexError): + index = self.__referencesList.index(txt) + filename, line, column = self.__referencesPositionsList[index] + self.vm.openSourceFile(filename, lineno=line, pos=column, addNext=True) + + def canProvideDynamicAutoCompletion(self): + """ + Public method to test the dynamic auto-completion availability. + + @return flag indicating the availability of dynamic auto-completion + (boolean) + """ + return ( + self.acAPI + or bool(self.__completionListHookFunctions) + or bool(self.__completionListAsyncHookFunctions) + ) + + ################################################################# + ## call-tip hook interfaces + ################################################################# + + def addCallTipHook(self, key, func): + """ + Public method to set a calltip provider. + + @param key name of the provider + @type str + @param func function providing calltips. func + should be a function taking a reference to the editor, + a position into the text and the amount of commas to the + left of the cursor. It should return the possible + calltips as a list of strings. + @type function(editor, int, int) -> list of str + """ + if key in self.__ctHookFunctions: + # it was already registered + EricMessageBox.warning( + self, + self.tr("Call-Tips Provider"), + self.tr( + """The call-tips provider '{0}' was already""" + """ registered. Ignoring duplicate request.""" + ).format(key), + ) + return + + self.__ctHookFunctions[key] = func + + def removeCallTipHook(self, key): + """ + Public method to remove a previously registered calltip provider. + + @param key name of the provider + @type str + """ + if key in self.__ctHookFunctions: + del self.__ctHookFunctions[key] + + def getCallTipHook(self, key): + """ + Public method to get the registered calltip provider. + + @param key name of the provider + @type str + @return function providing calltips + @rtype function or None + """ + if key in self.__ctHookFunctions: + return self.__ctHookFunctions[key] + else: + return None + + def canProvideCallTipps(self): + """ + Public method to test the calltips availability. + + @return flag indicating the availability of calltips (boolean) + """ + return self.acAPI or bool(self.__ctHookFunctions) + + def callTip(self): + """ + Public method to show calltips. + """ + if bool(self.__ctHookFunctions): + self.__callTip() + else: + super().callTip() + + def __callTip(self): + """ + Private method to show call tips provided by a plugin. + """ + pos = self.currentPosition() + + # move backward to the start of the current calltip working out + # which argument to highlight + commas = 0 + found = False + ch, pos = self.__getCharacter(pos) + while ch: + if ch == ",": + commas += 1 + elif ch == ")": + depth = 1 + + # ignore everything back to the start of the corresponding + # parenthesis + ch, pos = self.__getCharacter(pos) + while ch: + if ch == ")": + depth += 1 + elif ch == "(": + depth -= 1 + if depth == 0: + break + ch, pos = self.__getCharacter(pos) + elif ch == "(": + found = True + break + + ch, pos = self.__getCharacter(pos) + + self.cancelCallTips() + + if not found: + return + + callTips = [] + if self.__ctHookFunctions: + for key in self.__ctHookFunctions: + callTips.extend(self.__ctHookFunctions[key](self, pos, commas)) + callTips = list(set(callTips)) + callTips.sort() + else: + # try QScintilla calltips + super().callTip() + return + if len(callTips) == 0: + if Preferences.getEditor("CallTipsScintillaOnFail"): + # try QScintilla calltips + super().callTip() + return + + ctshift = 0 + for ct in callTips: + shift = ct.index("(") + if ctshift < shift: + ctshift = shift + + cv = self.callTipsVisible() + ct = ( + # this is just a safe guard + self._encodeString("\n".join(callTips[:cv])) + if cv > 0 + else + # until here and unindent below + self._encodeString("\n".join(callTips)) + ) + + self.SendScintilla( + QsciScintilla.SCI_CALLTIPSHOW, + self.__adjustedCallTipPosition(ctshift, pos), + ct, + ) + if b"\n" in ct: + return + + # Highlight the current argument + if commas == 0: + astart = ct.find(b"(") + else: + astart = ct.find(b",") + commas -= 1 + while astart != -1 and commas > 0: + astart = ct.find(b",", astart + 1) + commas -= 1 + + if astart == -1: + return + + depth = 0 + for aend in range(astart + 1, len(ct)): + ch = ct[aend : aend + 1] + + if ch == b"," and depth == 0: + break + elif ch == b"(": + depth += 1 + elif ch == b")": + if depth == 0: + break + + depth -= 1 + + if astart != aend: + self.SendScintilla(QsciScintilla.SCI_CALLTIPSETHLT, astart + 1, aend) + + def __adjustedCallTipPosition(self, ctshift, pos): + """ + Private method to calculate an adjusted position for showing calltips. + + @param ctshift amount the calltip shall be shifted (integer) + @param pos position into the text (integer) + @return new position for the calltip (integer) + """ + ct = pos + if ctshift: + ctmin = self.SendScintilla( + QsciScintilla.SCI_POSITIONFROMLINE, + self.SendScintilla(QsciScintilla.SCI_LINEFROMPOSITION, ct), + ) + if ct - ctshift < ctmin: + ct = ctmin + else: + ct -= ctshift + return ct + + ################################################################# + ## Methods needed by the code documentation viewer + ################################################################# + + def __showCodeInfo(self): + """ + Private slot to handle the context menu action to show code info. + """ + self.vm.showEditorInfo(self) + + ################################################################# + ## Methods needed by the context menu + ################################################################# + + def __marginNumber(self, xPos): + """ + Private method to calculate the margin number based on a x position. + + @param xPos x position (integer) + @return margin number (integer, -1 for no margin) + """ + width = 0 + for margin in range(5): + width += self.marginWidth(margin) + if xPos <= width: + return margin + return -1 + + def contextMenuEvent(self, evt): + """ + Protected method implementing the context menu event. + + @param evt the context menu event (QContextMenuEvent) + """ + evt.accept() + if self.__marginNumber(evt.x()) == -1: + self.spellingMenuPos = self.positionFromPoint(evt.pos()) + if ( + self.spellingMenuPos >= 0 + and self.spell is not None + and self.hasIndicator(self.spellingIndicator, self.spellingMenuPos) + ): + self.spellingMenu.popup(evt.globalPos()) + else: + self.menu.popup(evt.globalPos()) + else: + self.line = self.lineAt(evt.pos()) + if self.__marginNumber(evt.x()) in [self.__bmMargin, self.__linenoMargin]: + self.bmMarginMenu.popup(evt.globalPos()) + elif self.__marginNumber(evt.x()) == self.__bpMargin: + self.bpMarginMenu.popup(evt.globalPos()) + elif self.__marginNumber(evt.x()) == self.__indicMargin: + self.indicMarginMenu.popup(evt.globalPos()) + elif self.__marginNumber(evt.x()) == self.__foldMargin: + self.foldMarginMenu.popup(evt.globalPos()) + + def __showContextMenu(self): + """ + Private slot handling the aboutToShow signal of the context menu. + """ + self.menuActs["Reopen"].setEnabled( + not self.isModified() and bool(self.fileName) + ) + self.menuActs["Save"].setEnabled(self.isModified()) + self.menuActs["Undo"].setEnabled(self.isUndoAvailable()) + self.menuActs["Redo"].setEnabled(self.isRedoAvailable()) + self.menuActs["Revert"].setEnabled(self.isModified()) + self.menuActs["Cut"].setEnabled(self.hasSelectedText()) + self.menuActs["Copy"].setEnabled(self.hasSelectedText()) + if self.menuActs["ExecuteSelection"] is not None: + self.menuActs["ExecuteSelection"].setEnabled(self.hasSelectedText()) + self.menuActs["Paste"].setEnabled(self.canPaste()) + if not self.isResourcesFile: + if self.fileName and self.isPyFile(): + self.menuActs["Show"].setEnabled(True) + else: + self.menuActs["Show"].setEnabled(False) + if self.fileName and (self.isPyFile() or self.isRubyFile()): + self.menuActs["Diagrams"].setEnabled(True) + else: + self.menuActs["Diagrams"].setEnabled(False) + if not self.miniMenu: + if self.lexer_ is not None: + self.menuActs["Comment"].setEnabled(self.lexer_.canBlockComment()) + self.menuActs["Uncomment"].setEnabled(self.lexer_.canBlockComment()) + else: + self.menuActs["Comment"].setEnabled(False) + self.menuActs["Uncomment"].setEnabled(False) + + cline = self.getCursorPosition()[0] + line = self.text(cline) + self.menuActs["Docstring"].setEnabled( + self.getDocstringGenerator().isFunctionStart(line) + ) + + self.menuActs["TypingAidsEnabled"].setEnabled(self.completer is not None) + self.menuActs["TypingAidsEnabled"].setChecked( + self.completer is not None and self.completer.isEnabled() + ) + + if not self.isResourcesFile: + self.menuActs["calltip"].setEnabled(self.canProvideCallTipps()) + self.menuActs["codeInfo"].setEnabled( + self.vm.isEditorInfoSupported(self.getLanguage()) + ) + + self.menuActs["MonospacedFont"].setEnabled(self.lexer_ is None) + + splitOrientation = self.vm.getSplitOrientation() + if splitOrientation == Qt.Orientation.Horizontal: + self.menuActs["NewSplit"].setIcon(UI.PixmapCache.getIcon("splitHorizontal")) + else: + self.menuActs["NewSplit"].setIcon(UI.PixmapCache.getIcon("splitVertical")) + + self.menuActs["Tools"].setEnabled(not self.toolsMenu.isEmpty()) + + self.showMenu.emit("Main", self.menu, self) + + def __showContextMenuAutocompletion(self): + """ + Private slot called before the autocompletion menu is shown. + """ + self.menuActs["acDynamic"].setEnabled(self.canProvideDynamicAutoCompletion()) + self.menuActs["acClearCache"].setEnabled(self.canProvideDynamicAutoCompletion()) + self.menuActs["acAPI"].setEnabled(self.acAPI) + self.menuActs["acAPIDocument"].setEnabled(self.acAPI) + + self.showMenu.emit("Autocompletion", self.autocompletionMenu, self) + + def __showContextMenuShow(self): + """ + Private slot called before the show menu is shown. + """ + prEnable = False + coEnable = False + + # first check if the file belongs to a project + if self.project.isOpen() and self.project.isProjectSource(self.fileName): + fn = self.project.getMainScript(True) + if fn is not None: + prEnable = self.project.isPy3Project() and bool( + Utilities.getProfileFileNames(fn) + ) + coEnable = self.project.isPy3Project() and bool( + Utilities.getCoverageFileNames(fn) + ) + + # now check ourselves + fn = self.getFileName() + if fn is not None: + prEnable |= self.project.isPy3Project() and bool( + Utilities.getProfileFileName(fn) + ) + coEnable |= self.project.isPy3Project() and bool( + Utilities.getCoverageFileName(fn) + ) + + coEnable |= bool(self.__coverageFile) + + # now check for syntax errors + if self.hasSyntaxErrors(): + coEnable = False + + self.profileMenuAct.setEnabled(prEnable) + self.coverageMenuAct.setEnabled(coEnable) + self.coverageShowAnnotationMenuAct.setEnabled( + coEnable and len(self.notcoveredMarkers) == 0 + ) + self.coverageHideAnnotationMenuAct.setEnabled(len(self.notcoveredMarkers) > 0) + + self.showMenu.emit("Show", self.menuShow, self) + + def __showContextMenuGraphics(self): + """ + Private slot handling the aboutToShow signal of the diagrams context + menu. + """ + if self.project.isOpen() and self.project.isProjectSource(self.fileName): + self.applicationDiagramMenuAct.setEnabled(True) + else: + self.applicationDiagramMenuAct.setEnabled(False) + + self.showMenu.emit("Graphics", self.graphicsMenu, self) + + def __showContextMenuMargin(self, menu): + """ + Private slot handling the aboutToShow signal of the margins context + menu. + + @param menu reference to the menu to be shown + @type QMenu + """ + if menu is self.bpMarginMenu: + supportsDebugger = bool(self.fileName and self.isPyFile()) + hasBreakpoints = bool(self.breaks) + hasBreakpoint = bool(self.markersAtLine(self.line) & self.breakpointMask) + + self.marginMenuActs["Breakpoint"].setEnabled(supportsDebugger) + self.marginMenuActs["TempBreakpoint"].setEnabled(supportsDebugger) + self.marginMenuActs["NextBreakpoint"].setEnabled( + supportsDebugger and hasBreakpoints + ) + self.marginMenuActs["PreviousBreakpoint"].setEnabled( + supportsDebugger and hasBreakpoints + ) + self.marginMenuActs["ClearBreakpoint"].setEnabled( + supportsDebugger and hasBreakpoints + ) + self.marginMenuActs["EditBreakpoint"].setEnabled( + supportsDebugger and hasBreakpoint + ) + self.marginMenuActs["EnableBreakpoint"].setEnabled( + supportsDebugger and hasBreakpoint + ) + if supportsDebugger: + if self.markersAtLine(self.line) & (1 << self.dbreakpoint): + self.marginMenuActs["EnableBreakpoint"].setText( + self.tr("Enable breakpoint") + ) + else: + self.marginMenuActs["EnableBreakpoint"].setText( + self.tr("Disable breakpoint") + ) + + if menu is self.bmMarginMenu: + hasBookmarks = bool(self.bookmarks) + + self.marginMenuActs["NextBookmark"].setEnabled(hasBookmarks) + self.marginMenuActs["PreviousBookmark"].setEnabled(hasBookmarks) + self.marginMenuActs["ClearBookmark"].setEnabled(hasBookmarks) + + if menu is self.foldMarginMenu: + isFoldHeader = bool( + self.SendScintilla(QsciScintilla.SCI_GETFOLDLEVEL, self.line) + & QsciScintilla.SC_FOLDLEVELHEADERFLAG + ) + + self.marginMenuActs["ExpandChildren"].setEnabled(isFoldHeader) + self.marginMenuActs["CollapseChildren"].setEnabled(isFoldHeader) + + if menu is self.indicMarginMenu: + hasSyntaxErrors = bool(self.syntaxerrors) + hasWarnings = bool(self.warnings) + hasNotCoveredMarkers = bool(self.notcoveredMarkers) + + self.marginMenuActs["GotoSyntaxError"].setEnabled(hasSyntaxErrors) + self.marginMenuActs["ClearSyntaxError"].setEnabled(hasSyntaxErrors) + if hasSyntaxErrors and self.markersAtLine(self.line) & ( + 1 << self.syntaxerror + ): + self.marginMenuActs["ShowSyntaxError"].setEnabled(True) + else: + self.marginMenuActs["ShowSyntaxError"].setEnabled(False) + + self.marginMenuActs["NextWarningMarker"].setEnabled(hasWarnings) + self.marginMenuActs["PreviousWarningMarker"].setEnabled(hasWarnings) + self.marginMenuActs["ClearWarnings"].setEnabled(hasWarnings) + if hasWarnings and self.markersAtLine(self.line) & (1 << self.warning): + self.marginMenuActs["ShowWarning"].setEnabled(True) + else: + self.marginMenuActs["ShowWarning"].setEnabled(False) + + self.marginMenuActs["NextCoverageMarker"].setEnabled(hasNotCoveredMarkers) + self.marginMenuActs["PreviousCoverageMarker"].setEnabled( + hasNotCoveredMarkers + ) + + self.marginMenuActs["PreviousTaskMarker"].setEnabled(self.__hasTaskMarkers) + self.marginMenuActs["NextTaskMarker"].setEnabled(self.__hasTaskMarkers) + + self.marginMenuActs["PreviousChangeMarker"].setEnabled( + self.__hasChangeMarkers + ) + self.marginMenuActs["NextChangeMarker"].setEnabled(self.__hasChangeMarkers) + self.marginMenuActs["ClearChangeMarkers"].setEnabled( + self.__hasChangeMarkers + ) + + self.showMenu.emit("Margin", menu, self) + + def __showContextMenuChecks(self): + """ + Private slot handling the aboutToShow signal of the checks context + menu. + """ + self.showMenu.emit("Checks", self.checksMenu, self) + + def __showContextMenuTools(self): + """ + Private slot handling the aboutToShow signal of the tools context + menu. + """ + self.showMenu.emit("Tools", self.toolsMenu, self) + + def __showContextMenuFormatting(self): + """ + Private slot handling the aboutToShow signal of the code formatting context + menu. + """ + self.showMenu.emit("Formatting", self.codeFormattingMenu, self) + + def __reopenWithEncodingMenuTriggered(self, act): + """ + Private method to handle the rereading of the file with a selected + encoding. + + @param act reference to the action that was triggered (QAction) + """ + encoding = act.data() + self.readFile(self.fileName, encoding=encoding) + self.__convertTabs() + self.__checkEncoding() + + def __contextSave(self): + """ + Private slot handling the save context menu entry. + """ + ok = self.saveFile() + if ok: + self.vm.setEditorName(self, self.fileName) + + def __contextSaveAs(self): + """ + Private slot handling the save as context menu entry. + """ + ok = self.saveFileAs() + if ok: + self.vm.setEditorName(self, self.fileName) + + def __contextSaveCopy(self): + """ + Private slot handling the save copy context menu entry. + """ + self.saveFileCopy() + + def __contextClose(self): + """ + Private slot handling the close context menu entry. + """ + self.vm.closeEditor(self) + + def __newView(self): + """ + Private slot to create a new view to an open document. + """ + self.vm.newEditorView(self.fileName, self, self.filetype) + + def __newViewNewSplit(self): + """ + Private slot to create a new view to an open document. + """ + self.vm.addSplit() + self.vm.newEditorView(self.fileName, self, self.filetype) + + def __selectAll(self): + """ + Private slot handling the select all context menu action. + """ + self.selectAll(True) + + def __deselectAll(self): + """ + Private slot handling the deselect all context menu action. + """ + self.selectAll(False) + + def joinLines(self): + """ + Public slot to join the current line with the next one. + """ + curLine = self.getCursorPosition()[0] + if curLine == self.lines() - 1: + return + + line0Text = self.text(curLine) + line1Text = self.text(curLine + 1) + if line1Text in ["", "\r", "\n", "\r\n"]: + return + + if line0Text.rstrip("\r\n\\ \t").endswith( + ("'", '"') + ) and line1Text.lstrip().startswith(("'", '"')): + # merging multi line strings + startChars = "\r\n\\ \t'\"" + endChars = " \t'\"" + else: + startChars = "\r\n\\ \t" + endChars = " \t" + + # determine start index + startIndex = len(line0Text) + while startIndex > 0 and line0Text[startIndex - 1] in startChars: + startIndex -= 1 + if startIndex == 0: + return + + # determine end index + endIndex = 0 + while line1Text[endIndex] in endChars: + endIndex += 1 + + self.setSelection(curLine, startIndex, curLine + 1, endIndex) + self.beginUndoAction() + self.removeSelectedText() + self.insertAt(" ", curLine, startIndex) + self.endUndoAction() + + def shortenEmptyLines(self): + """ + Public slot to compress lines consisting solely of whitespace + characters. + """ + searchRE = r"^[ \t]+$" + + ok = self.findFirstTarget(searchRE, True, False, False, 0, 0) + self.beginUndoAction() + while ok: + self.replaceTarget("") + ok = self.findNextTarget() + self.endUndoAction() + + def __autosaveEnable(self): + """ + Private slot handling the autosave enable context menu action. + """ + if self.menuActs["AutosaveEnable"].isChecked(): + self.autosaveManuallyDisabled = False + else: + self.autosaveManuallyDisabled = True + + def shouldAutosave(self): + """ + Public slot to check the autosave flags. + + @return flag indicating this editor should be saved (boolean) + """ + return ( + bool(self.fileName) + and not self.autosaveManuallyDisabled + and not self.isReadOnly() + ) + + def checkSyntax(self): + """ + Public method to perform an automatic syntax check of the file. + """ + fileType = self.filetype + if fileType == "MicroPython": + # adjustment for MicroPython + fileType = "Python3" + + if ( + self.syntaxCheckService is None + or fileType not in self.syntaxCheckService.getLanguages() + ): + return + + if Preferences.getEditor("AutoCheckSyntax"): + if Preferences.getEditor("OnlineSyntaxCheck"): + self.__onlineSyntaxCheckTimer.stop() + + self.syntaxCheckService.syntaxCheck( + fileType, self.fileName or "(Unnamed)", self.text() + ) + + def __processSyntaxCheckError(self, fn, msg): + """ + Private slot to report an error message of a syntax check. + + @param fn filename of the file + @type str + @param msg error message + @type str + """ + if fn != self.fileName and (bool(self.fileName) or fn != "(Unnamed)"): + return + + self.clearSyntaxError() + self.clearFlakesWarnings() + + self.toggleWarning(0, 0, True, msg) + + self.updateVerticalScrollBar() + + def __processSyntaxCheckResult(self, fn, problems): + """ + Private slot to report the resulting messages of a syntax check. + + @param fn filename of the checked file (str) + @param problems dictionary with the keys 'error' and 'warnings' which + hold a list containing details about the error/ warnings + (file name, line number, column, codestring (only at syntax + errors), the message) (dict) + """ + # Check if it's the requested file, otherwise ignore signal + if fn != self.fileName and (bool(self.fileName) or fn != "(Unnamed)"): + return + + self.clearSyntaxError() + self.clearFlakesWarnings() + + error = problems.get("error") + if error: + _fn, lineno, col, code, msg = error + self.toggleSyntaxError(lineno, col, True, msg) + + warnings = problems.get("warnings", []) + for _fn, lineno, col, _code, msg in warnings: + self.toggleWarning(lineno, col, True, msg) + + self.updateVerticalScrollBar() + + def __initOnlineSyntaxCheck(self): + """ + Private slot to initialize the online syntax check. + """ + self.__onlineSyntaxCheckTimer = QTimer(self) + self.__onlineSyntaxCheckTimer.setSingleShot(True) + self.__onlineSyntaxCheckTimer.setInterval( + Preferences.getEditor("OnlineSyntaxCheckInterval") * 1000 + ) + self.__onlineSyntaxCheckTimer.timeout.connect(self.checkSyntax) + self.textChanged.connect(self.__resetOnlineSyntaxCheckTimer) + + def __resetOnlineSyntaxCheckTimer(self): + """ + Private method to reset the online syntax check timer. + """ + if Preferences.getEditor("OnlineSyntaxCheck"): + self.__onlineSyntaxCheckTimer.stop() + self.__onlineSyntaxCheckTimer.start() + + def __showCodeMetrics(self): + """ + Private method to handle the code metrics context menu action. + """ + if not self.checkDirty(): + return + + from DataViews.CodeMetricsDialog import CodeMetricsDialog + + self.codemetrics = CodeMetricsDialog() + self.codemetrics.show() + self.codemetrics.start(self.fileName) + + def __getCodeCoverageFile(self): + """ + Private method to get the file name of the file containing coverage + info. + + @return file name of the coverage file + @rtype str + """ + files = set() + + if bool(self.__coverageFile): + # return the path of a previously used coverage file + return self.__coverageFile + + # first check if the file belongs to a project and there is + # a project coverage file + if self.project.isOpen() and self.project.isProjectSource(self.fileName): + pfn = self.project.getMainScript(True) + if pfn is not None: + files |= set(Utilities.getCoverageFileNames(pfn)) + + # now check, if there are coverage files belonging to ourselves + fn = self.getFileName() + if fn is not None: + files |= set(Utilities.getCoverageFileNames(fn)) + + files = list(files) + if files: + if len(files) > 1: + cfn, ok = QInputDialog.getItem( + self, + self.tr("Code Coverage"), + self.tr("Please select a coverage file"), + files, + 0, + False, + ) + if not ok: + return "" + else: + cfn = files[0] + else: + cfn = None + + return cfn + + def __showCodeCoverage(self): + """ + Private method to handle the code coverage context menu action. + """ + fn = self.__getCodeCoverageFile() + self.__coverageFile = fn + if fn: + from DataViews.PyCoverageDialog import PyCoverageDialog + + self.codecoverage = PyCoverageDialog() + self.codecoverage.show() + self.codecoverage.start(fn, self.fileName) + + def refreshCoverageAnnotations(self): + """ + Public method to refresh the code coverage annotations. + """ + if self.showingNotcoveredMarkers: + self.codeCoverageShowAnnotations(silent=True) + + def codeCoverageShowAnnotations(self, silent=False, coverageFile=None): + """ + Public method to handle the show code coverage annotations context + menu action. + + @param silent flag indicating to not show any dialog (defaults to + False) + @type bool (optional) + @param coverageFile path of the file containing the code coverage data + (defaults to None) + @type str (optional) + """ + self.__codeCoverageHideAnnotations() + + fn = coverageFile if bool(coverageFile) else self.__getCodeCoverageFile() + self.__coverageFile = fn + + if fn: + from coverage import Coverage + + cover = Coverage(data_file=fn) + cover.load() + missing = cover.analysis2(self.fileName)[3] + if missing: + for line in missing: + handle = self.markerAdd(line - 1, self.notcovered) + self.notcoveredMarkers.append(handle) + self.coverageMarkersShown.emit(True) + self.__markerMap.update() + else: + if not silent: + EricMessageBox.information( + self, + self.tr("Show Code Coverage Annotations"), + self.tr("""All lines have been covered."""), + ) + self.showingNotcoveredMarkers = True + else: + if not silent: + EricMessageBox.warning( + self, + self.tr("Show Code Coverage Annotations"), + self.tr("""There is no coverage file available."""), + ) + + def __codeCoverageHideAnnotations(self): + """ + Private method to handle the hide code coverage annotations context + menu action. + """ + for handle in self.notcoveredMarkers: + self.markerDeleteHandle(handle) + self.notcoveredMarkers.clear() + self.coverageMarkersShown.emit(False) + self.showingNotcoveredMarkers = False + self.__markerMap.update() + + def getCoverageLines(self): + """ + Public method to get the lines containing a coverage marker. + + @return list of lines containing a coverage marker (list of integer) + """ + lines = [] + line = -1 + while True: + line = self.markerFindNext(line + 1, 1 << self.notcovered) + if line < 0: + break + else: + lines.append(line) + return lines + + def hasCoverageMarkers(self): + """ + Public method to test, if there are coverage markers. + + @return flag indicating the presence of coverage markers (boolean) + """ + return len(self.notcoveredMarkers) > 0 + + def nextUncovered(self): + """ + Public slot to handle the 'Next uncovered' context menu action. + """ + line, index = self.getCursorPosition() + if line == self.lines() - 1: + line = 0 + else: + line += 1 + ucline = self.markerFindNext(line, 1 << self.notcovered) + if ucline < 0: + # wrap around + ucline = self.markerFindNext(0, 1 << self.notcovered) + if ucline >= 0: + self.setCursorPosition(ucline, 0) + self.ensureLineVisible(ucline) + + def previousUncovered(self): + """ + Public slot to handle the 'Previous uncovered' context menu action. + """ + line, index = self.getCursorPosition() + if line == 0: + line = self.lines() - 1 + else: + line -= 1 + ucline = self.markerFindPrevious(line, 1 << self.notcovered) + if ucline < 0: + # wrap around + ucline = self.markerFindPrevious(self.lines() - 1, 1 << self.notcovered) + if ucline >= 0: + self.setCursorPosition(ucline, 0) + self.ensureLineVisible(ucline) + + def __showProfileData(self): + """ + Private method to handle the show profile data context menu action. + """ + files = set() + + # first check if the file belongs to a project and there is + # a project profile file + if self.project.isOpen() and self.project.isProjectSource(self.fileName): + fn = self.project.getMainScript(True) + if fn is not None: + files |= set(Utilities.getProfileFileNames(fn)) + + # now check, if there are profile files belonging to ourselves + fn = self.getFileName() + if fn is not None: + files |= set(Utilities.getProfileFileNames(fn)) + + files = list(files) + if files: + if len(files) > 1: + fn, ok = QInputDialog.getItem( + self, + self.tr("Profile Data"), + self.tr("Please select a profile file"), + files, + 0, + False, + ) + if not ok: + return + else: + fn = files[0] + else: + return + + from DataViews.PyProfileDialog import PyProfileDialog + + self.profiledata = PyProfileDialog() + self.profiledata.show() + self.profiledata.start(fn, self.fileName) + + def __lmBbookmarks(self): + """ + Private method to handle the 'LMB toggles bookmark' context menu + action. + """ + self.marginMenuActs["LMBbookmarks"].setChecked(True) + self.marginMenuActs["LMBbreakpoints"].setChecked(False) + + def __lmBbreakpoints(self): + """ + Private method to handle the 'LMB toggles breakpoint' context menu + action. + """ + self.marginMenuActs["LMBbookmarks"].setChecked(True) + self.marginMenuActs["LMBbreakpoints"].setChecked(False) + + ########################################################################### + ## Syntax error handling methods below + ########################################################################### + + def toggleSyntaxError(self, line, index, error, msg="", show=False): + """ + Public method to toggle a syntax error indicator. + + @param line line number of the syntax error (integer) + @param index index number of the syntax error (integer) + @param error flag indicating if the error marker should be + set or deleted (boolean) + @param msg error message (string) + @param show flag indicating to set the cursor to the error position + (boolean) + """ + if line == 0: + line = 1 + # hack to show a syntax error marker, if line is reported to be 0 + if error: + # set a new syntax error marker + markers = self.markersAtLine(line - 1) + index += self.indentation(line - 1) + if not (markers & (1 << self.syntaxerror)): + handle = self.markerAdd(line - 1, self.syntaxerror) + self.syntaxerrors[handle] = [(msg, index)] + self.syntaxerrorToggled.emit(self) + else: + for handle in list(self.syntaxerrors.keys()): + if ( + self.markerLine(handle) == line - 1 + and (msg, index) not in self.syntaxerrors[handle] + ): + self.syntaxerrors[handle].append((msg, index)) + if show: + self.setCursorPosition(line - 1, index) + self.ensureLineVisible(line - 1) + else: + for handle in list(self.syntaxerrors.keys()): + if self.markerLine(handle) == line - 1: + del self.syntaxerrors[handle] + self.markerDeleteHandle(handle) + self.syntaxerrorToggled.emit(self) + + self.__setAnnotation(line - 1) + self.__markerMap.update() + + def getSyntaxErrors(self): + """ + Public method to retrieve the syntax error markers. + + @return sorted list of all lines containing a syntax error + (list of integer) + """ + selist = [] + for handle in list(self.syntaxerrors.keys()): + selist.append(self.markerLine(handle) + 1) + + selist.sort() + return selist + + def getSyntaxErrorLines(self): + """ + Public method to get the lines containing a syntax error. + + @return list of lines containing a syntax error (list of integer) + """ + lines = [] + line = -1 + while True: + line = self.markerFindNext(line + 1, 1 << self.syntaxerror) + if line < 0: + break + else: + lines.append(line) + return lines + + def hasSyntaxErrors(self): + """ + Public method to check for the presence of syntax errors. + + @return flag indicating the presence of syntax errors (boolean) + """ + return len(self.syntaxerrors) > 0 + + def gotoSyntaxError(self): + """ + Public slot to handle the 'Goto syntax error' context menu action. + """ + seline = self.markerFindNext(0, 1 << self.syntaxerror) + if seline >= 0: + index = 0 + for handle in self.syntaxerrors: + if self.markerLine(handle) == seline: + index = self.syntaxerrors[handle][0][1] + self.setCursorPosition(seline, index) + self.ensureLineVisible(seline) + + def clearSyntaxError(self): + """ + Public slot to handle the 'Clear all syntax error' context menu action. + """ + for handle in list(self.syntaxerrors.keys()): + line = self.markerLine(handle) + 1 + self.toggleSyntaxError(line, 0, False) + + self.syntaxerrors.clear() + self.syntaxerrorToggled.emit(self) + + def __showSyntaxError(self, line=-1): + """ + Private slot to handle the 'Show syntax error message' + context menu action. + + @param line line number to show the syntax error for (integer) + """ + if line == -1: + line = self.line + + for handle in list(self.syntaxerrors.keys()): + if self.markerLine(handle) == line: + errors = [e[0] for e in self.syntaxerrors[handle]] + EricMessageBox.critical( + self, self.tr("Syntax Error"), "\n".join(errors) + ) + break + else: + EricMessageBox.critical( + self, + self.tr("Syntax Error"), + self.tr("No syntax error message available."), + ) + + ########################################################################### + ## VCS conflict marker handling methods below + ########################################################################### + + def getVcsConflictMarkerLines(self): + """ + Public method to determine the lines containing a VCS conflict marker. + + @return list of line numbers containg a VCS conflict marker + @rtype list of int + """ + conflictMarkerLines = [] + + regExp = re.compile( + "|".join(Editor.VcsConflictMarkerLineRegExpList), re.MULTILINE + ) + matches = [m for m in regExp.finditer(self.text())] + for match in matches: + line, _ = self.lineIndexFromPosition(match.start()) + conflictMarkerLines.append(line) + + return conflictMarkerLines + + ########################################################################### + ## Warning handling methods below + ########################################################################### + + def toggleWarning(self, line, col, warning, msg="", warningType=WarningCode): + """ + Public method to toggle a warning indicator. + + Note: This method is used to set pyflakes and code style warnings. + + @param line line number of the warning + @param col column of the warning + @param warning flag indicating if the warning marker should be + set or deleted (boolean) + @param msg warning message (string) + @param warningType type of warning message (integer) + """ + if line == 0: + line = 1 + # hack to show a warning marker, if line is reported to be 0 + if warning: + # set/amend a new warning marker + warn = (msg, warningType) + markers = self.markersAtLine(line - 1) + if not (markers & (1 << self.warning)): + handle = self.markerAdd(line - 1, self.warning) + self.warnings[handle] = [warn] + self.syntaxerrorToggled.emit(self) + else: + for handle in list(self.warnings.keys()): + if ( + self.markerLine(handle) == line - 1 + and warn not in self.warnings[handle] + ): + self.warnings[handle].append(warn) + else: + for handle in list(self.warnings.keys()): + if self.markerLine(handle) == line - 1: + del self.warnings[handle] + self.markerDeleteHandle(handle) + self.syntaxerrorToggled.emit(self) + + self.__setAnnotation(line - 1) + self.__markerMap.update() + + def getWarnings(self): + """ + Public method to retrieve the warning markers. + + @return sorted list of all lines containing a warning + (list of integer) + """ + fwlist = [] + for handle in list(self.warnings.keys()): + fwlist.append(self.markerLine(handle) + 1) + + fwlist.sort() + return fwlist + + def getWarningLines(self): + """ + Public method to get the lines containing a warning. + + @return list of lines containing a warning (list of integer) + """ + lines = [] + line = -1 + while True: + line = self.markerFindNext(line + 1, 1 << self.warning) + if line < 0: + break + else: + lines.append(line) + return lines + + def hasWarnings(self): + """ + Public method to check for the presence of warnings. + + @return flag indicating the presence of warnings (boolean) + """ + return len(self.warnings) > 0 + + def nextWarning(self): + """ + Public slot to handle the 'Next warning' context menu action. + """ + line, index = self.getCursorPosition() + if line == self.lines() - 1: + line = 0 + else: + line += 1 + fwline = self.markerFindNext(line, 1 << self.warning) + if fwline < 0: + # wrap around + fwline = self.markerFindNext(0, 1 << self.warning) + if fwline >= 0: + self.setCursorPosition(fwline, 0) + self.ensureLineVisible(fwline) + + def previousWarning(self): + """ + Public slot to handle the 'Previous warning' context menu action. + """ + line, index = self.getCursorPosition() + if line == 0: + line = self.lines() - 1 + else: + line -= 1 + fwline = self.markerFindPrevious(line, 1 << self.warning) + if fwline < 0: + # wrap around + fwline = self.markerFindPrevious(self.lines() - 1, 1 << self.warning) + if fwline >= 0: + self.setCursorPosition(fwline, 0) + self.ensureLineVisible(fwline) + + def clearFlakesWarnings(self): + """ + Public slot to clear all pyflakes warnings. + """ + self.__clearTypedWarning(Editor.WarningCode) + + def clearStyleWarnings(self): + """ + Public slot to clear all style warnings. + """ + self.__clearTypedWarning(Editor.WarningStyle) + + def __clearTypedWarning(self, warningKind): + """ + Private method to clear warnings of a specific kind. + + @param warningKind kind of warning to clear (Editor.WarningCode, + Editor.WarningStyle) + """ + for handle in list(self.warnings.keys()): + warnings = [] + for msg, warningType in self.warnings[handle]: + if warningType == warningKind: + continue + + warnings.append((msg, warningType)) + + if warnings: + self.warnings[handle] = warnings + self.__setAnnotation(self.markerLine(handle)) + else: + del self.warnings[handle] + self.__setAnnotation(self.markerLine(handle)) + self.markerDeleteHandle(handle) + self.syntaxerrorToggled.emit(self) + self.__markerMap.update() + + def clearWarnings(self): + """ + Public slot to clear all warnings. + """ + for handle in self.warnings: + self.warnings[handle] = [] + self.__setAnnotation(self.markerLine(handle)) + self.markerDeleteHandle(handle) + self.warnings.clear() + self.syntaxerrorToggled.emit(self) + self.__markerMap.update() + + def __showWarning(self, line=-1): + """ + Private slot to handle the 'Show warning' context menu action. + + @param line line number to show the warning for (integer) + """ + if line == -1: + line = self.line + + for handle in list(self.warnings.keys()): + if self.markerLine(handle) == line: + EricMessageBox.warning( + self, + self.tr("Warning"), + "\n".join([w[0] for w in self.warnings[handle]]), + ) + break + else: + EricMessageBox.warning( + self, self.tr("Warning"), self.tr("No warning messages available.") + ) + + ########################################################################### + ## Annotation handling methods below + ########################################################################### + + def __setAnnotationStyles(self): + """ + Private slot to define the style used by inline annotations. + """ + if hasattr(QsciScintilla, "annotate"): + self.annotationWarningStyle = QsciScintilla.STYLE_LASTPREDEFINED + 1 + self.SendScintilla( + QsciScintilla.SCI_STYLESETFORE, + self.annotationWarningStyle, + Preferences.getEditorColour("AnnotationsWarningForeground"), + ) + self.SendScintilla( + QsciScintilla.SCI_STYLESETBACK, + self.annotationWarningStyle, + Preferences.getEditorColour("AnnotationsWarningBackground"), + ) + + self.annotationErrorStyle = self.annotationWarningStyle + 1 + self.SendScintilla( + QsciScintilla.SCI_STYLESETFORE, + self.annotationErrorStyle, + Preferences.getEditorColour("AnnotationsErrorForeground"), + ) + self.SendScintilla( + QsciScintilla.SCI_STYLESETBACK, + self.annotationErrorStyle, + Preferences.getEditorColour("AnnotationsErrorBackground"), + ) + + self.annotationStyleStyle = self.annotationErrorStyle + 1 + self.SendScintilla( + QsciScintilla.SCI_STYLESETFORE, + self.annotationStyleStyle, + Preferences.getEditorColour("AnnotationsStyleForeground"), + ) + self.SendScintilla( + QsciScintilla.SCI_STYLESETBACK, + self.annotationStyleStyle, + Preferences.getEditorColour("AnnotationsStyleBackground"), + ) + + def __setAnnotation(self, line): + """ + Private method to set the annotations for the given line. + + @param line number of the line that needs annotation (integer) + """ + if hasattr(QsciScintilla, "annotate"): + warningAnnotations = [] + errorAnnotations = [] + styleAnnotations = [] + + # step 1: do warnings + for handle in self.warnings: + if self.markerLine(handle) == line: + for msg, warningType in self.warnings[handle]: + if warningType == self.WarningStyle: + styleAnnotations.append(self.tr("Style: {0}").format(msg)) + else: + warningAnnotations.append( + self.tr("Warning: {0}").format(msg) + ) + + # step 2: do syntax errors + for handle in self.syntaxerrors: + if self.markerLine(handle) == line: + for msg, _ in self.syntaxerrors[handle]: + errorAnnotations.append(self.tr("Error: {0}").format(msg)) + + annotations = [] + if styleAnnotations: + annotationStyleTxt = "\n".join(styleAnnotations) + if warningAnnotations or errorAnnotations: + annotationStyleTxt += "\n" + annotations.append( + QsciStyledText(annotationStyleTxt, self.annotationStyleStyle) + ) + + if warningAnnotations: + annotationWarningTxt = "\n".join(warningAnnotations) + if errorAnnotations: + annotationWarningTxt += "\n" + annotations.append( + QsciStyledText(annotationWarningTxt, self.annotationWarningStyle) + ) + + if errorAnnotations: + annotationErrorTxt = "\n".join(errorAnnotations) + annotations.append( + QsciStyledText(annotationErrorTxt, self.annotationErrorStyle) + ) + + if annotations: + self.annotate(line, annotations) + else: + self.clearAnnotations(line) + + def __refreshAnnotations(self): + """ + Private method to refresh the annotations. + """ + if hasattr(QsciScintilla, "annotate"): + self.clearAnnotations() + for handle in list(self.warnings.keys()) + list(self.syntaxerrors.keys()): + line = self.markerLine(handle) + self.__setAnnotation(line) + + ################################################################# + ## Fold handling methods + ################################################################# + + def toggleCurrentFold(self): + """ + Public slot to toggle the fold containing the current line. + """ + line, index = self.getCursorPosition() + self.foldLine(line) + + def expandFoldWithChildren(self, line=-1): + """ + Public slot to expand the current fold including its children. + + @param line number of line to be expanded + @type int + """ + if line == -1: + line, index = self.getCursorPosition() + + self.SendScintilla( + QsciScintilla.SCI_FOLDCHILDREN, line, QsciScintilla.SC_FOLDACTION_EXPAND + ) + + def collapseFoldWithChildren(self, line=-1): + """ + Public slot to collapse the current fold including its children. + + @param line number of line to be expanded + @type int + """ + if line == -1: + line, index = self.getCursorPosition() + + self.SendScintilla( + QsciScintilla.SCI_FOLDCHILDREN, line, QsciScintilla.SC_FOLDACTION_CONTRACT + ) + + def __contextMenuExpandFoldWithChildren(self): + """ + Private slot to handle the context menu expand with children action. + """ + self.expandFoldWithChildren(self.line) + + def __contextMenuCollapseFoldWithChildren(self): + """ + Private slot to handle the context menu collapse with children action. + """ + self.collapseFoldWithChildren(self.line) + + ################################################################# + ## Macro handling methods + ################################################################# + + def __getMacroName(self): + """ + Private method to select a macro name from the list of macros. + + @return Tuple of macro name and a flag, indicating, if the user + pressed ok or canceled the operation. (string, boolean) + """ + qs = [] + for s in list(self.macros.keys()): + qs.append(s) + qs.sort() + return QInputDialog.getItem( + self, self.tr("Macro Name"), self.tr("Select a macro name:"), qs, 0, False + ) + + def macroRun(self): + """ + Public method to execute a macro. + """ + name, ok = self.__getMacroName() + if ok and name: + self.macros[name].play() + + def macroDelete(self): + """ + Public method to delete a macro. + """ + name, ok = self.__getMacroName() + if ok and name: + del self.macros[name] + + def macroLoad(self): + """ + Public method to load a macro from a file. + """ + configDir = Utilities.getConfigDir() + fname = EricFileDialog.getOpenFileName( + self, + self.tr("Load macro file"), + configDir, + self.tr("Macro files (*.macro)"), + ) + + if not fname: + return # user aborted + + try: + with open(fname, "r", encoding="utf-8") as f: + lines = f.readlines() + except OSError: + EricMessageBox.critical( + self, + self.tr("Error loading macro"), + self.tr("<p>The macro file <b>{0}</b> could not be read.</p>").format( + fname + ), + ) + return + + if len(lines) != 2: + EricMessageBox.critical( + self, + self.tr("Error loading macro"), + self.tr("<p>The macro file <b>{0}</b> is corrupt.</p>").format(fname), + ) + return + + macro = QsciMacro(lines[1], self) + self.macros[lines[0].strip()] = macro + + def macroSave(self): + """ + Public method to save a macro to a file. + """ + configDir = Utilities.getConfigDir() + + name, ok = self.__getMacroName() + if not ok or not name: + return # user abort + + fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( + self, + self.tr("Save macro file"), + configDir, + self.tr("Macro files (*.macro)"), + "", + EricFileDialog.DontConfirmOverwrite, + ) + + if not fname: + return # user aborted + + fpath = pathlib.Path(fname) + if not fpath.suffix: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fpath = fpath.with_suffix(ex) + if fpath.exists(): + res = EricMessageBox.yesNo( + self, + self.tr("Save macro"), + self.tr( + "<p>The macro file <b>{0}</b> already exists." " Overwrite it?</p>" + ).format(fpath), + icon=EricMessageBox.Warning, + ) + if not res: + return + + try: + with fpath.open("w", encoding="utf-8") as f: + f.write("{0}{1}".format(name, "\n")) + f.write(self.macros[name].save()) + except OSError: + EricMessageBox.critical( + self, + self.tr("Error saving macro"), + self.tr( + "<p>The macro file <b>{0}</b> could not be written.</p>" + ).format(fpath), + ) + return + + def macroRecordingStart(self): + """ + Public method to start macro recording. + """ + if self.recording: + res = EricMessageBox.yesNo( + self, + self.tr("Start Macro Recording"), + self.tr("Macro recording is already active. Start new?"), + icon=EricMessageBox.Warning, + yesDefault=True, + ) + if res: + self.macroRecordingStop() + else: + return + else: + self.recording = True + + self.curMacro = QsciMacro(self) + self.curMacro.startRecording() + + def macroRecordingStop(self): + """ + Public method to stop macro recording. + """ + if not self.recording: + return # we are not recording + + self.curMacro.endRecording() + self.recording = False + + name, ok = QInputDialog.getText( + self, + self.tr("Macro Recording"), + self.tr("Enter name of the macro:"), + QLineEdit.EchoMode.Normal, + ) + + if ok and name: + self.macros[name] = self.curMacro + + self.curMacro = None + + ################################################################# + ## Overwritten methods + ################################################################# + + def undo(self): + """ + Public method to undo the last recorded change. + """ + super().undo() + self.undoAvailable.emit(self.isUndoAvailable()) + self.redoAvailable.emit(self.isRedoAvailable()) + + def redo(self): + """ + Public method to redo the last recorded change. + """ + super().redo() + self.undoAvailable.emit(self.isUndoAvailable()) + self.redoAvailable.emit(self.isRedoAvailable()) + + def close(self, alsoDelete=False): + """ + Public method called when the window gets closed. + + This overwritten method redirects the action to our + ViewManager.closeEditor, which in turn calls our closeIt + method. + + @param alsoDelete ignored + @return flag indicating a successful close of the editor (boolean) + """ + return self.vm.closeEditor(self) + + def closeIt(self): + """ + Public method called by the viewmanager to finally get rid of us. + """ + if Preferences.getEditor("ClearBreaksOnClose") and not self.__clones: + self.__menuClearBreakpoints() + + for clone in self.__clones[:]: + self.removeClone(clone) + clone.removeClone(self) + + self.breakpointModel.rowsAboutToBeRemoved.disconnect(self.__deleteBreakPoints) + self.breakpointModel.dataAboutToBeChanged.disconnect( + self.__breakPointDataAboutToBeChanged + ) + self.breakpointModel.dataChanged.disconnect(self.__changeBreakPoints) + self.breakpointModel.rowsInserted.disconnect(self.__addBreakPoints) + + if self.syntaxCheckService is not None: + self.syntaxCheckService.syntaxChecked.disconnect( + self.__processSyntaxCheckResult + ) + self.syntaxCheckService.error.disconnect(self.__processSyntaxCheckError) + + if self.spell: + self.spell.stopIncrementalCheck() + + with contextlib.suppress(TypeError): + self.project.projectPropertiesChanged.disconnect( + self.__projectPropertiesChanged + ) + + if self.fileName: + self.taskViewer.clearFileTasks(self.fileName, True) + + super().close() + + def keyPressEvent(self, ev): + """ + Protected method to handle the user input a key at a time. + + @param ev key event + @type QKeyEvent + """ + + def encloseSelectedText(encString): + """ + Local function to enclose the current selection with some + characters. + + @param encString string to use to enclose the selection + (one or two characters) + @type str + """ + startChar = encString[0] + endChar = encString[1] if len(encString) == 2 else startChar + + sline, sindex, eline, eindex = self.getSelection() + replaceText = startChar + self.selectedText() + endChar + self.beginUndoAction() + self.replaceSelectedText(replaceText) + self.endUndoAction() + self.setSelection(sline, sindex + 1, eline, eindex + 1) + + txt = ev.text() + + # See it is text to insert. + if len(txt) and txt >= " ": + if self.hasSelectedText() and txt in Editor.EncloseChars: + encloseSelectedText(Editor.EncloseChars[txt]) + ev.accept() + return + + super().keyPressEvent(ev) + else: + ev.ignore() + + def focusInEvent(self, event): + """ + Protected method called when the editor receives focus. + + This method checks for modifications of the current file and + rereads it upon request. The cursor is placed at the current position + assuming, that it is in the vicinity of the old position after the + reread. + + @param event the event object + @type QFocusEvent + """ + self.recolor() + self.vm.editActGrp.setEnabled(True) + self.vm.editorActGrp.setEnabled(True) + self.vm.copyActGrp.setEnabled(True) + self.vm.viewActGrp.setEnabled(True) + self.vm.searchActGrp.setEnabled(True) + with contextlib.suppress(AttributeError): + self.setCaretWidth(self.caretWidth) + self.__updateReadOnly(False) + if ( + self.vm.editorsCheckFocusInEnabled() + and not self.inReopenPrompt + and self.fileName + and pathlib.Path(self.fileName).exists() + and pathlib.Path(self.fileName).stat().st_mtime != self.lastModified + ): + self.inReopenPrompt = True + if Preferences.getEditor("AutoReopen") and not self.isModified(): + self.refresh() + else: + msg = self.tr( + """<p>The file <b>{0}</b> has been changed while it""" + """ was opened in eric. Reread it?</p>""" + ).format(self.fileName) + yesDefault = True + if self.isModified(): + msg += self.tr( + """<br><b>Warning:</b> You will lose""" + """ your changes upon reopening it.""" + ) + yesDefault = False + res = EricMessageBox.yesNo( + self, + self.tr("File changed"), + msg, + icon=EricMessageBox.Warning, + yesDefault=yesDefault, + ) + if res: + self.refresh() + else: + # do not prompt for this change again... + self.lastModified = pathlib.Path(self.fileName).stat().st_mtime + self.inReopenPrompt = False + + self.setCursorFlashTime(QApplication.cursorFlashTime()) + + super().focusInEvent(event) + + def focusOutEvent(self, event): + """ + Protected method called when the editor loses focus. + + @param event the event object + @type QFocusEvent + """ + self.vm.editorActGrp.setEnabled(False) + self.setCaretWidth(0) + + self.cancelCallTips() + + super().focusOutEvent(event) + + def changeEvent(self, evt): + """ + Protected method called to process an event. + + This implements special handling for the events showMaximized, + showMinimized and showNormal. The windows caption is shortened + for the minimized mode and reset to the full filename for the + other modes. This is to make the editor windows work nicer + with the QWorkspace. + + @param evt the event, that was generated + @type QEvent + """ + if evt.type() == QEvent.Type.WindowStateChange and bool(self.fileName): + cap = ( + os.path.basename(self.fileName) + if self.windowState() == Qt.WindowState.WindowMinimized + else self.fileName + ) + if self.isReadOnly(): + cap = self.tr("{0} (ro)").format(cap) + self.setWindowTitle(cap) + + super().changeEvent(evt) + + def mousePressEvent(self, event): + """ + Protected method to handle the mouse press event. + + @param event the mouse press event + @type QMouseEvent + """ + if event.button() == Qt.MouseButton.XButton1: + self.undo() + event.accept() + elif event.button() == Qt.MouseButton.XButton2: + self.redo() + event.accept() + elif event.button() == Qt.MouseButton.LeftButton and bool( + event.modifiers() + & (Qt.KeyboardModifier.MetaModifier | Qt.KeyboardModifier.AltModifier) + ): + line, index = self.lineIndexFromPoint(event.position().toPoint()) + self.addCursor(line, index) + event.accept() + else: + self.vm.eventFilter(self, event) + super().mousePressEvent(event) + + def mouseDoubleClickEvent(self, evt): + """ + Protected method to handle mouse double click events. + + @param evt reference to the mouse event + @type QMouseEvent + """ + super().mouseDoubleClickEvent(evt) + + self.mouseDoubleClick.emit(evt.position().toPoint(), evt.buttons()) + + def wheelEvent(self, evt): + """ + Protected method to handle wheel events. + + @param evt reference to the wheel event + @type QWheelEvent + """ + delta = evt.angleDelta().y() + if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: + if delta < 0: + self.zoomOut() + elif delta > 0: + self.zoomIn() + evt.accept() + return + + if evt.modifiers() & Qt.KeyboardModifier.ShiftModifier: + if delta < 0: + self.gotoMethodClass(False) + elif delta > 0: + self.gotoMethodClass(True) + evt.accept() + return + + super().wheelEvent(evt) + + def event(self, evt): + """ + Public method handling events. + + @param evt reference to the event + @type QEvent + @return flag indicating, if the event was handled + @rtype bool + """ + if evt.type() == QEvent.Type.Gesture: + self.gestureEvent(evt) + return True + + return super().event(evt) + + def gestureEvent(self, evt): + """ + Protected method handling gesture events. + + @param evt reference to the gesture event + @type QGestureEvent + """ + pinch = evt.gesture(Qt.GestureType.PinchGesture) + if pinch: + if pinch.state() == Qt.GestureState.GestureStarted: + zoom = (self.getZoom() + 10) / 10.0 + pinch.setTotalScaleFactor(zoom) + elif pinch.state() == Qt.GestureState.GestureUpdated: + zoom = int(pinch.totalScaleFactor() * 10) - 10 + if zoom <= -9: + zoom = -9 + pinch.setTotalScaleFactor(0.1) + elif zoom >= 20: + zoom = 20 + pinch.setTotalScaleFactor(3.0) + self.zoomTo(zoom) + evt.accept() + + def resizeEvent(self, evt): + """ + Protected method handling resize events. + + @param evt reference to the resize event + @type QResizeEvent + """ + super().resizeEvent(evt) + self.__markerMap.calculateGeometry() + + def viewportEvent(self, evt): + """ + Protected method handling event of the viewport. + + @param evt reference to the event + @type QEvent + @return flag indiating that the event was handled + @rtype bool + """ + with contextlib.suppress(AttributeError): + self.__markerMap.calculateGeometry() + return super().viewportEvent(evt) + + def __updateReadOnly(self, bForce=True): + """ + Private method to update the readOnly information for this editor. + + If bForce is True, then updates everything regardless if + the attributes have actually changed, such as during + initialization time. A signal is emitted after the + caption change. + + @param bForce True to force change, False to only update and emit + signal if there was an attribute change. + """ + if self.fileName == "": + return + + readOnly = not os.access(self.fileName, os.W_OK) or self.isReadOnly() + if not bForce and (readOnly == self.isReadOnly()): + return + + cap = self.fileName + if readOnly: + cap = self.tr("{0} (ro)".format(cap)) + self.setReadOnly(readOnly) + self.setWindowTitle(cap) + self.captionChanged.emit(cap, self) + + def refresh(self): + """ + Public slot to refresh the editor contents. + """ + # save cursor position + cline, cindex = self.getCursorPosition() + + # save bookmarks and breakpoints and clear them + bmlist = self.getBookmarks() + self.clearBookmarks() + + # clear syntax error markers + self.clearSyntaxError() + + # clear flakes warning markers + self.clearWarnings() + + # clear breakpoint markers + for handle in list(self.breaks.keys()): + self.markerDeleteHandle(handle) + self.breaks.clear() + + if not os.path.exists(self.fileName): + # close the file, if it was deleted in the background + self.close() + return + + # reread the file + try: + self.readFile(self.fileName) + except OSError: + # do not prompt for this change again... + self.lastModified = QDateTime.currentDateTime() + self.setModified(False) + self.__convertTabs() + + # re-initialize the online change tracer + self.__reinitOnlineChangeTrace() + + # reset cursor position + self.setCursorPosition(cline, cindex) + self.ensureCursorVisible() + + # reset bookmarks and breakpoints to their old position + if bmlist: + for bm in bmlist: + self.toggleBookmark(bm) + self.__restoreBreakpoints() + + self.editorSaved.emit(self.fileName) + self.checkSyntax() + + self.__markerMap.update() + + self.refreshed.emit() + + def setMonospaced(self, on): + """ + Public method to set/reset a monospaced font. + + @param on flag to indicate usage of a monospace font (boolean) + """ + if on: + if not self.lexer_: + f = Preferences.getEditorOtherFonts("MonospacedFont") + self.monospacedStyles(f) + else: + if not self.lexer_: + self.clearStyles() + self.__setMarginsDisplay() + self.setFont(Preferences.getEditorOtherFonts("DefaultFont")) + + self.useMonospaced = on + + def clearStyles(self): + """ + Public method to set the styles according the selected Qt style + or the selected editor colours. + """ + super().clearStyles() + if Preferences.getEditor("OverrideEditAreaColours"): + self.setColor(Preferences.getEditorColour("EditAreaForeground")) + self.setPaper(Preferences.getEditorColour("EditAreaBackground")) + + ################################################################# + ## Drag and Drop Support + ################################################################# + + def dragEnterEvent(self, event): + """ + Protected method to handle the drag enter event. + + @param event the drag enter event (QDragEnterEvent) + """ + self.inDragDrop = event.mimeData().hasUrls() + if self.inDragDrop: + event.acceptProposedAction() + else: + super().dragEnterEvent(event) + + def dragMoveEvent(self, event): + """ + Protected method to handle the drag move event. + + @param event the drag move event (QDragMoveEvent) + """ + if self.inDragDrop: + event.accept() + else: + super().dragMoveEvent(event) + + def dragLeaveEvent(self, event): + """ + Protected method to handle the drag leave event. + + @param event the drag leave event (QDragLeaveEvent) + """ + if self.inDragDrop: + self.inDragDrop = False + event.accept() + else: + super().dragLeaveEvent(event) + + def dropEvent(self, event): + """ + Protected method to handle the drop event. + + @param event the drop event (QDropEvent) + """ + if event.mimeData().hasUrls(): + for url in event.mimeData().urls(): + fname = url.toLocalFile() + if fname: + if not pathlib.Path(fname).is_dir(): + self.vm.openSourceFile(fname) + else: + EricMessageBox.information( + self, + self.tr("Drop Error"), + self.tr("""<p><b>{0}</b> is not a file.</p>""").format( + fname + ), + ) + event.acceptProposedAction() + else: + super().dropEvent(event) + + self.inDragDrop = False + + ################################################################# + ## Support for Qt resources files + ################################################################# + + def __initContextMenuResources(self): + """ + Private method used to setup the Resources context sub menu. + + @return reference to the generated menu (QMenu) + """ + menu = QMenu(self.tr("Resources")) + + menu.addAction(self.tr("Add file..."), self.__addFileResource) + menu.addAction(self.tr("Add files..."), self.__addFileResources) + menu.addAction(self.tr("Add aliased file..."), self.__addFileAliasResource) + menu.addAction( + self.tr("Add localized resource..."), self.__addLocalizedResource + ) + menu.addSeparator() + menu.addAction(self.tr("Add resource frame"), self.__addResourceFrame) + + menu.aboutToShow.connect(self.__showContextMenuResources) + + return menu + + def __showContextMenuResources(self): + """ + Private slot handling the aboutToShow signal of the resources context + menu. + """ + self.showMenu.emit("Resources", self.resourcesMenu, self) + + def __addFileResource(self): + """ + Private method to handle the Add file context menu action. + """ + dirStr = os.path.dirname(self.fileName) + file = EricFileDialog.getOpenFileName( + self, self.tr("Add file resource"), dirStr, "" + ) + if file: + relFile = QDir(dirStr).relativeFilePath(file) + line, index = self.getCursorPosition() + self.insert(" <file>{0}</file>\n".format(relFile)) + self.setCursorPosition(line + 1, index) + + def __addFileResources(self): + """ + Private method to handle the Add files context menu action. + """ + dirStr = os.path.dirname(self.fileName) + files = EricFileDialog.getOpenFileNames( + self, self.tr("Add file resources"), dirStr, "" + ) + if files: + myDir = QDir(dirStr) + filesText = "" + for file in files: + relFile = myDir.relativeFilePath(file) + filesText += " <file>{0}</file>\n".format(relFile) + line, index = self.getCursorPosition() + self.insert(filesText) + self.setCursorPosition(line + len(files), index) + + def __addFileAliasResource(self): + """ + Private method to handle the Add aliased file context menu action. + """ + dirStr = os.path.dirname(self.fileName) + file = EricFileDialog.getOpenFileName( + self, self.tr("Add aliased file resource"), dirStr, "" + ) + if file: + relFile = QDir(dirStr).relativeFilePath(file) + alias, ok = QInputDialog.getText( + self, + self.tr("Add aliased file resource"), + self.tr("Alias for file <b>{0}</b>:").format(relFile), + QLineEdit.EchoMode.Normal, + relFile, + ) + if ok and alias: + line, index = self.getCursorPosition() + self.insert(' <file alias="{1}">{0}</file>\n'.format(relFile, alias)) + self.setCursorPosition(line + 1, index) + + def __addLocalizedResource(self): + """ + Private method to handle the Add localized resource context menu + action. + """ + from Project.AddLanguageDialog import AddLanguageDialog + + dlg = AddLanguageDialog(self) + if dlg.exec() == QDialog.DialogCode.Accepted: + lang = dlg.getSelectedLanguage() + line, index = self.getCursorPosition() + self.insert('<qresource lang="{0}">\n</qresource>\n'.format(lang)) + self.setCursorPosition(line + 2, index) + + def __addResourceFrame(self): + """ + Private method to handle the Add resource frame context menu action. + """ + line, index = self.getCursorPosition() + self.insert( + "<!DOCTYPE RCC>\n" + '<RCC version="1.0">\n' + "<qresource>\n" + "</qresource>\n" + "</RCC>\n" + ) + self.setCursorPosition(line + 5, index) + + ################################################################# + ## Support for diagrams below + ################################################################# + + def __showClassDiagram(self): + """ + Private method to handle the Class Diagram context menu action. + """ + from Graphics.UMLDialog import UMLDialog, UMLDialogType + + if not self.checkDirty(): + return + + self.classDiagram = UMLDialog( + UMLDialogType.CLASS_DIAGRAM, + self.project, + self.fileName, + self, + noAttrs=False, + ) + self.classDiagram.show() + + def __showPackageDiagram(self): + """ + Private method to handle the Package Diagram context menu action. + """ + from Graphics.UMLDialog import UMLDialog, UMLDialogType + + if not self.checkDirty(): + return + + package = ( + os.path.isdir(self.fileName) + and self.fileName + or os.path.dirname(self.fileName) + ) + res = EricMessageBox.yesNo( + self, + self.tr("Package Diagram"), + self.tr("""Include class attributes?"""), + yesDefault=True, + ) + self.packageDiagram = UMLDialog( + UMLDialogType.PACKAGE_DIAGRAM, self.project, package, self, noAttrs=not res + ) + self.packageDiagram.show() + + def __showImportsDiagram(self): + """ + Private method to handle the Imports Diagram context menu action. + """ + from Graphics.UMLDialog import UMLDialog, UMLDialogType + + if not self.checkDirty(): + return + + package = os.path.dirname(self.fileName) + res = EricMessageBox.yesNo( + self, + self.tr("Imports Diagram"), + self.tr("""Include imports from external modules?"""), + ) + self.importsDiagram = UMLDialog( + UMLDialogType.IMPORTS_DIAGRAM, + self.project, + package, + self, + showExternalImports=res, + ) + self.importsDiagram.show() + + def __showApplicationDiagram(self): + """ + Private method to handle the Imports Diagram context menu action. + """ + from Graphics.UMLDialog import UMLDialog, UMLDialogType + + res = EricMessageBox.yesNo( + self, + self.tr("Application Diagram"), + self.tr("""Include module names?"""), + yesDefault=True, + ) + self.applicationDiagram = UMLDialog( + UMLDialogType.APPLICATION_DIAGRAM, self.project, self, noModules=not res + ) + self.applicationDiagram.show() + + def __loadDiagram(self): + """ + Private slot to load a diagram from file. + """ + from Graphics.UMLDialog import UMLDialog, UMLDialogType + + self.loadedDiagram = UMLDialog( + UMLDialogType.NO_DIAGRAM, self.project, parent=self + ) + if self.loadedDiagram.load(): + self.loadedDiagram.show(fromFile=True) + else: + self.loadedDiagram = None + + ####################################################################### + ## Typing aids related methods below + ####################################################################### + + def __toggleTypingAids(self): + """ + Private slot to toggle the typing aids. + """ + if self.menuActs["TypingAidsEnabled"].isChecked(): + self.completer.setEnabled(True) + else: + self.completer.setEnabled(False) + + ####################################################################### + ## Auto-completing templates + ####################################################################### + + def editorCommand(self, cmd): + """ + Public method to perform a simple editor command. + + @param cmd the scintilla command to be performed + """ + if cmd == QsciScintilla.SCI_TAB: + try: + templateViewer = ericApp().getObject("TemplateViewer") + except KeyError: + # template viewer is not active + templateViewer = None + + if templateViewer is not None: + line, index = self.getCursorPosition() + tmplName = self.getWordLeft(line, index) + if tmplName: + if templateViewer.hasTemplate(tmplName, self.getLanguage()): + self.__applyTemplate(tmplName, self.getLanguage()) + return + else: + templateNames = templateViewer.getTemplateNames( + tmplName, self.getLanguage() + ) + if len(templateNames) == 1: + self.__applyTemplate(templateNames[0], self.getLanguage()) + return + elif len(templateNames) > 1: + self.showUserList( + TemplateCompletionListID, + [ + "{0}?{1:d}".format(t, self.TemplateImageID) + for t in templateNames + ], + ) + return + + elif cmd == QsciScintilla.SCI_DELETEBACK: + line, index = self.getCursorPosition() + text = self.text(line)[index - 1 : index + 1] + matchingPairs = ["()", "[]", "{}", "<>", "''", '""'] + # __IGNORE_WARNING_M613__ + if text in matchingPairs: + self.delete() + + super().editorCommand(cmd) + + def __applyTemplate(self, templateName, language): + """ + Private method to apply a template by name. + + @param templateName name of the template to apply (string) + @param language name of the language (group) to get the template + from (string) + """ + try: + templateViewer = ericApp().getObject("TemplateViewer") + except KeyError: + # template viewer is not active + return + + if templateViewer.hasTemplate(templateName, self.getLanguage()): + self.extendSelectionWordLeft() + templateViewer.applyNamedTemplate(templateName, self.getLanguage()) + + ####################################################################### + ## Project related methods + ####################################################################### + + def __projectPropertiesChanged(self): + """ + Private slot to handle changes of the project properties. + """ + if self.spell: + pwl, pel = self.project.getProjectDictionaries() + self.__setSpellingLanguage( + self.project.getProjectSpellLanguage(), pwl=pwl, pel=pel + ) + + editorConfigEol = self.__getEditorConfig("EOLMode", nodefault=True) + if editorConfigEol is not None: + self.setEolMode(editorConfigEol) + else: + self.setEolModeByEolString(self.project.getEolString()) + self.convertEols(self.eolMode()) + + def addedToProject(self): + """ + Public method to signal, that this editor has been added to a project. + """ + if self.spell: + pwl, pel = self.project.getProjectDictionaries() + self.__setSpellingLanguage( + self.project.getProjectSpellLanguage(), pwl=pwl, pel=pel + ) + + self.project.projectPropertiesChanged.connect(self.__projectPropertiesChanged) + + def projectOpened(self): + """ + Public slot to handle the opening of a project. + """ + if self.fileName and self.project.isProjectSource(self.fileName): + self.project.projectPropertiesChanged.connect( + self.__projectPropertiesChanged + ) + self.setSpellingForProject() + + def projectClosed(self): + """ + Public slot to handle the closing of a project. + """ + with contextlib.suppress(TypeError): + self.project.projectPropertiesChanged.disconnect( + self.__projectPropertiesChanged + ) + + ####################################################################### + ## Spell checking related methods + ####################################################################### + + def getSpellingLanguage(self): + """ + Public method to get the current spelling language. + + @return current spelling language + @rtype str + """ + if self.spell: + return self.spell.getLanguage() + + return "" + + def __setSpellingLanguage(self, language, pwl="", pel=""): + """ + Private slot to set the spell checking language. + + @param language spell checking language to be set (string) + @param pwl name of the personal/project word list (string) + @param pel name of the personal/project exclude list (string) + """ + if self.spell and self.spell.getLanguage() != language: + self.spell.setLanguage(language, pwl=pwl, pel=pel) + self.spell.checkDocumentIncrementally() + + def __setSpelling(self): + """ + Private method to initialize the spell checking functionality. + """ + if Preferences.getEditor("SpellCheckingEnabled"): + self.__spellCheckStringsOnly = Preferences.getEditor( + "SpellCheckStringsOnly" + ) + if self.spell is None: + self.spell = SpellChecker( + self, self.spellingIndicator, checkRegion=self.isSpellCheckRegion + ) + self.setSpellingForProject() + self.spell.setMinimumWordSize( + Preferences.getEditor("SpellCheckingMinWordSize") + ) + + self.setAutoSpellChecking() + else: + self.spell = None + self.clearAllIndicators(self.spellingIndicator) + + def setSpellingForProject(self): + """ + Public method to set the spell checking options for files belonging + to the current project. + """ + if ( + self.fileName + and self.project.isOpen() + and self.project.isProjectSource(self.fileName) + ): + pwl, pel = self.project.getProjectDictionaries() + self.__setSpellingLanguage( + self.project.getProjectSpellLanguage(), pwl=pwl, pel=pel + ) + + def setAutoSpellChecking(self): + """ + Public method to set the automatic spell checking. + """ + if Preferences.getEditor("AutoSpellCheckingEnabled"): + with contextlib.suppress(TypeError): + self.SCN_CHARADDED.connect( + self.__spellCharAdded, Qt.ConnectionType.UniqueConnection + ) + self.spell.checkDocumentIncrementally() + else: + with contextlib.suppress(TypeError): + self.SCN_CHARADDED.disconnect(self.__spellCharAdded) + self.clearAllIndicators(self.spellingIndicator) + + def isSpellCheckRegion(self, pos): + """ + Public method to check, if the given position is within a region, that + should be spell checked. + + For files with a configured full text file extension all regions will + be regarded as to be checked. Depending on configuration, all unknown + files (i.e. those without a file extension) will be checked fully as + well. + + @param pos position to be checked + @type int + @return flag indicating pos is in a spell check region + @rtype bool + """ + if self.__spellCheckStringsOnly: + if ( + self.__fileNameExtension + in Preferences.getEditor("FullSpellCheckExtensions") + ) or ( + not self.__fileNameExtension + and Preferences.getEditor("FullSpellCheckUnknown") + ): + return True + else: + style = self.styleAt(pos) + if self.lexer_ is not None: + return self.lexer_.isCommentStyle( + style + ) or self.lexer_.isStringStyle(style) + + return True + + @pyqtSlot(int) + def __spellCharAdded(self, charNumber): + """ + Private slot called to handle the user entering a character. + + @param charNumber value of the character entered (integer) + """ + if self.spell: + if not chr(charNumber).isalnum(): + self.spell.checkWord(self.positionBefore(self.currentPosition()), True) + elif self.hasIndicator(self.spellingIndicator, self.currentPosition()): + self.spell.checkWord(self.currentPosition()) + + def checkSpelling(self): + """ + Public slot to perform an interactive spell check of the document. + """ + if self.spell: + cline, cindex = self.getCursorPosition() + from .SpellCheckingDialog import SpellCheckingDialog + + dlg = SpellCheckingDialog(self.spell, 0, self.length(), self) + dlg.exec() + self.setCursorPosition(cline, cindex) + if Preferences.getEditor("AutoSpellCheckingEnabled"): + self.spell.checkDocumentIncrementally() + + def __checkSpellingSelection(self): + """ + Private slot to spell check the current selection. + """ + from .SpellCheckingDialog import SpellCheckingDialog + + sline, sindex, eline, eindex = self.getSelection() + startPos = self.positionFromLineIndex(sline, sindex) + endPos = self.positionFromLineIndex(eline, eindex) + dlg = SpellCheckingDialog(self.spell, startPos, endPos, self) + dlg.exec() + + def __checkSpellingWord(self): + """ + Private slot to check the word below the spelling context menu. + """ + from .SpellCheckingDialog import SpellCheckingDialog + + line, index = self.lineIndexFromPosition(self.spellingMenuPos) + wordStart, wordEnd = self.getWordBoundaries(line, index) + wordStartPos = self.positionFromLineIndex(line, wordStart) + wordEndPos = self.positionFromLineIndex(line, wordEnd) + dlg = SpellCheckingDialog(self.spell, wordStartPos, wordEndPos, self) + dlg.exec() + + def __showContextMenuSpelling(self): + """ + Private slot to set up the spelling menu before it is shown. + """ + self.spellingMenu.clear() + self.spellingSuggActs = [] + line, index = self.lineIndexFromPosition(self.spellingMenuPos) + word = self.getWord(line, index) + suggestions = self.spell.getSuggestions(word) + for suggestion in suggestions[:5]: + self.spellingSuggActs.append(self.spellingMenu.addAction(suggestion)) + if suggestions: + self.spellingMenu.addSeparator() + self.spellingMenu.addAction( + UI.PixmapCache.getIcon("spellchecking"), + self.tr("Check spelling..."), + self.__checkSpellingWord, + ) + self.spellingMenu.addAction( + self.tr("Add to dictionary"), self.__addToSpellingDictionary + ) + self.spellingMenu.addAction(self.tr("Ignore All"), self.__ignoreSpellingAlways) + + self.showMenu.emit("Spelling", self.spellingMenu, self) + + def __contextMenuSpellingTriggered(self, action): + """ + Private slot to handle the selection of a suggestion of the spelling + context menu. + + @param action reference to the action that was selected (QAction) + """ + if action in self.spellingSuggActs: + replacement = action.text() + line, index = self.lineIndexFromPosition(self.spellingMenuPos) + wordStart, wordEnd = self.getWordBoundaries(line, index) + self.setSelection(line, wordStart, line, wordEnd) + self.beginUndoAction() + self.removeSelectedText() + self.insert(replacement) + self.endUndoAction() + + def __addToSpellingDictionary(self): + """ + Private slot to add the word below the spelling context menu to the + dictionary. + """ + line, index = self.lineIndexFromPosition(self.spellingMenuPos) + word = self.getWord(line, index) + self.spell.add(word) + + wordStart, wordEnd = self.getWordBoundaries(line, index) + self.clearIndicator(self.spellingIndicator, line, wordStart, line, wordEnd) + if Preferences.getEditor("AutoSpellCheckingEnabled"): + self.spell.checkDocumentIncrementally() + + def __removeFromSpellingDictionary(self): + """ + Private slot to remove the word below the context menu to the + dictionary. + """ + line, index = self.lineIndexFromPosition(self.spellingMenuPos) + word = self.getWord(line, index) + self.spell.remove(word) + + if Preferences.getEditor("AutoSpellCheckingEnabled"): + self.spell.checkDocumentIncrementally() + + def __ignoreSpellingAlways(self): + """ + Private to always ignore the word below the spelling context menu. + """ + line, index = self.lineIndexFromPosition(self.spellingMenuPos) + word = self.getWord(line, index) + self.spell.ignoreAlways(word) + if Preferences.getEditor("AutoSpellCheckingEnabled"): + self.spell.checkDocumentIncrementally() + + ####################################################################### + ## Cooperation related methods + ####################################################################### + + def getSharingStatus(self): + """ + Public method to get some share status info. + + @return tuple indicating, if the editor is sharable, the sharing + status, if it is inside a locally initiated shared edit session + and if it is inside a remotely initiated shared edit session + (boolean, boolean, boolean, boolean) + """ + return ( + ( + bool(self.fileName) + and self.project.isOpen() + and self.project.isProjectFile(self.fileName) + ), + self.__isShared, + self.__inSharedEdit, + self.__inRemoteSharedEdit, + ) + + def shareConnected(self, connected): + """ + Public slot to handle a change of the connected state. + + @param connected flag indicating the connected state (boolean) + """ + if not connected: + self.__inRemoteSharedEdit = False + self.setReadOnly(False) + self.__updateReadOnly() + self.cancelSharedEdit(send=False) + self.__isSyncing = False + self.__receivedWhileSyncing = [] + + def shareEditor(self, share): + """ + Public slot to set the shared status of the editor. + + @param share flag indicating the share status (boolean) + """ + self.__isShared = share + if not share: + self.shareConnected(False) + + def startSharedEdit(self): + """ + Public slot to start a shared edit session for the editor. + """ + self.__inSharedEdit = True + self.__savedText = self.text() + hashStr = str( + QCryptographicHash.hash( + Utilities.encode(self.__savedText, self.encoding)[0], + QCryptographicHash.Algorithm.Sha1, + ).toHex(), + encoding="utf-8", + ) + self.__send(Editor.StartEditToken, hashStr) + + def sendSharedEdit(self): + """ + Public slot to end a shared edit session for the editor and + send the changes. + """ + commands = self.__calculateChanges(self.__savedText, self.text()) + self.__send(Editor.EndEditToken, commands) + self.__inSharedEdit = False + self.__savedText = "" + + def cancelSharedEdit(self, send=True): + """ + Public slot to cancel a shared edit session for the editor. + + @param send flag indicating to send the CancelEdit command (boolean) + """ + self.__inSharedEdit = False + self.__savedText = "" + if send: + self.__send(Editor.CancelEditToken) + + def __send(self, token, args=None): + """ + Private method to send an editor command to remote editors. + + @param token command token (string) + @param args arguments for the command (string) + """ + if self.vm.isConnected(): + msg = "" + if token in ( + Editor.StartEditToken, + Editor.EndEditToken, + Editor.RequestSyncToken, + Editor.SyncToken, + ): + msg = "{0}{1}{2}".format(token, Editor.Separator, args) + elif token == Editor.CancelEditToken: + msg = "{0}{1}c".format(token, Editor.Separator) + + self.vm.send(self.fileName, msg) + + def receive(self, command): + """ + Public slot to handle received editor commands. + + @param command command string (string) + """ + if self.__isShared: + if self.__isSyncing and not command.startswith( + Editor.SyncToken + Editor.Separator + ): + self.__receivedWhileSyncing.append(command) + else: + self.__dispatchCommand(command) + + def __dispatchCommand(self, command): + """ + Private method to dispatch received commands. + + @param command command to be processed (string) + """ + token, argsString = command.split(Editor.Separator, 1) + if token == Editor.StartEditToken: + self.__processStartEditCommand(argsString) + elif token == Editor.CancelEditToken: + self.shareConnected(False) + elif token == Editor.EndEditToken: + self.__processEndEditCommand(argsString) + elif token == Editor.RequestSyncToken: + self.__processRequestSyncCommand(argsString) + elif token == Editor.SyncToken: + self.__processSyncCommand(argsString) + + def __processStartEditCommand(self, argsString): + """ + Private slot to process a remote StartEdit command. + + @param argsString string containing the command parameters (string) + """ + if not self.__inSharedEdit and not self.__inRemoteSharedEdit: + self.__inRemoteSharedEdit = True + self.setReadOnly(True) + self.__updateReadOnly() + hashStr = str( + QCryptographicHash.hash( + Utilities.encode(self.text(), self.encoding)[0], + QCryptographicHash.Algorithm.Sha1, + ).toHex(), + encoding="utf-8", + ) + if hashStr != argsString: + # text is different to the remote site, request to sync it + self.__isSyncing = True + self.__send(Editor.RequestSyncToken, argsString) + + def __calculateChanges(self, old, new): + """ + Private method to determine change commands to convert old text into + new text. + + @param old old text (string) + @param new new text (string) + @return commands to change old into new (string) + """ + oldL = old.splitlines() + newL = new.splitlines() + matcher = difflib.SequenceMatcher(None, oldL, newL) + + formatStr = "@@{0} {1} {2} {3}" + commands = [] + for token, i1, i2, j1, j2 in matcher.get_opcodes(): + if token == "insert": # secok + commands.append(formatStr.format("i", j1, j2 - j1, -1)) + commands.extend(newL[j1:j2]) + elif token == "delete": # secok + commands.append(formatStr.format("d", j1, i2 - i1, -1)) + elif token == "replace": # secok + commands.append(formatStr.format("r", j1, i2 - i1, j2 - j1)) + commands.extend(newL[j1:j2]) + + return "\n".join(commands) + "\n" + + def __processEndEditCommand(self, argsString): + """ + Private slot to process a remote EndEdit command. + + @param argsString string containing the command parameters (string) + """ + commands = argsString.splitlines() + sep = self.getLineSeparator() + cur = self.getCursorPosition() + + self.setReadOnly(False) + self.beginUndoAction() + while commands: + commandLine = commands.pop(0) + if not commandLine.startswith("@@"): + continue + + args = commandLine.split() + command = args.pop(0) + pos, l1, l2 = [int(arg) for arg in args] + if command == "@@i": + txt = sep.join(commands[0:l1]) + sep + self.insertAt(txt, pos, 0) + del commands[0:l1] + elif command == "@@d": + self.setSelection(pos, 0, pos + l1, 0) + self.removeSelectedText() + elif command == "@@r": + self.setSelection(pos, 0, pos + l1, 0) + self.removeSelectedText() + txt = sep.join(commands[0:l2]) + sep + self.insertAt(txt, pos, 0) + del commands[0:l2] + self.endUndoAction() + self.__updateReadOnly() + self.__inRemoteSharedEdit = False + + self.setCursorPosition(*cur) + + def __processRequestSyncCommand(self, argsString): + """ + Private slot to process a remote RequestSync command. + + @param argsString string containing the command parameters (string) + """ + if self.__inSharedEdit: + hashStr = str( + QCryptographicHash.hash( + Utilities.encode(self.__savedText, self.encoding)[0], + QCryptographicHash.Algorithm.Sha1, + ).toHex(), + encoding="utf-8", + ) + + if hashStr == argsString: + self.__send(Editor.SyncToken, self.__savedText) + + def __processSyncCommand(self, argsString): + """ + Private slot to process a remote Sync command. + + @param argsString string containing the command parameters (string) + """ + if self.__isSyncing: + cur = self.getCursorPosition() + + self.setReadOnly(False) + self.beginUndoAction() + self.selectAll() + self.removeSelectedText() + self.insertAt(argsString, 0, 0) + self.endUndoAction() + self.setReadOnly(True) + + self.setCursorPosition(*cur) + + while self.__receivedWhileSyncing: + command = self.__receivedWhileSyncing.pop(0) + self.__dispatchCommand(command) + + self.__isSyncing = False + + ####################################################################### + ## Special search related methods + ####################################################################### + + def searchCurrentWordForward(self): + """ + Public slot to search the current word forward. + """ + self.__searchCurrentWord(forward=True) + + def searchCurrentWordBackward(self): + """ + Public slot to search the current word backward. + """ + self.__searchCurrentWord(forward=False) + + def __searchCurrentWord(self, forward=True): + """ + Private slot to search the next occurrence of the current word. + + @param forward flag indicating the search direction (boolean) + """ + self.hideFindIndicator() + line, index = self.getCursorPosition() + word = self.getCurrentWord() + wordStart, wordEnd = self.getCurrentWordBoundaries() + wordStartPos = self.positionFromLineIndex(line, wordStart) + wordEndPos = self.positionFromLineIndex(line, wordEnd) + + regExp = re.compile(r"\b{0}\b".format(word)) + startPos = wordEndPos if forward else wordStartPos + + matches = [m for m in regExp.finditer(self.text())] + if matches: + if forward: + matchesAfter = [m for m in matches if m.start() >= startPos] + if matchesAfter: + match = matchesAfter[0] + else: + # wrap around + match = matches[0] + else: + matchesBefore = [m for m in matches if m.start() < startPos] + if matchesBefore: + match = matchesBefore[-1] + else: + # wrap around + match = matches[-1] + line, index = self.lineIndexFromPosition(match.start()) + self.setSelection(line, index + len(match.group(0)), line, index) + self.showFindIndicator(line, index, line, index + len(match.group(0))) + + ####################################################################### + ## Sort related methods + ####################################################################### + + def sortLines(self): + """ + Public slot to sort the lines spanned by a rectangular selection. + """ + if not self.selectionIsRectangle(): + return + + from .SortOptionsDialog import SortOptionsDialog + + dlg = SortOptionsDialog() + if dlg.exec() == QDialog.DialogCode.Accepted: + ascending, alnum, caseSensitive = dlg.getData() + ( + origStartLine, + origStartIndex, + origEndLine, + origEndIndex, + ) = self.getRectangularSelection() + # convert to upper-left to lower-right + startLine = min(origStartLine, origEndLine) + startIndex = min(origStartIndex, origEndIndex) + endLine = max(origStartLine, origEndLine) + endIndex = max(origStartIndex, origEndIndex) + + # step 1: extract the text of the rectangular selection and + # the lines + selText = {} + txtLines = {} + for line in range(startLine, endLine + 1): + txtLines[line] = self.text(line) + txt = txtLines[line][startIndex:endIndex].strip() + if not alnum: + try: + txt = float(txt) + except ValueError: + EricMessageBox.critical( + self, + self.tr("Sort Lines"), + self.tr( + """The selection contains illegal data for a""" + """ numerical sort.""" + ), + ) + return + + if txt in selText: + selText[txt].append(line) + else: + selText[txt] = [line] + + # step 2: calculate the sort parameters + reverse = not ascending + if alnum and not caseSensitive: + keyFun = str.lower + else: + keyFun = None + + # step 3: sort the lines + eol = self.getLineSeparator() + lastWithEol = True + newLines = [] + for txt in sorted(selText.keys(), key=keyFun, reverse=reverse): + for line in selText[txt]: + txt = txtLines[line] + if not txt.endswith(eol): + lastWithEol = False + txt += eol + newLines.append(txt) + if not lastWithEol: + newLines[-1] = newLines[-1][: -len(eol)] + + # step 4: replace the lines by the sorted ones + self.setSelection(startLine, 0, endLine + 1, 0) + self.beginUndoAction() + self.replaceSelectedText("".join(newLines)) + self.endUndoAction() + + # step 5: reset the rectangular selection + self.setRectangularSelection( + origStartLine, origStartIndex, origEndLine, origEndIndex + ) + self.selectionChanged.emit() + + ####################################################################### + ## Mouse click handler related methods + ####################################################################### + + def mouseReleaseEvent(self, evt): + """ + Protected method calling a registered mouse click handler function. + + @param evt event object + @type QMouseEvent + """ + modifiers = evt.modifiers() + button = evt.button() + key = (modifiers, button) + + self.vm.eventFilter(self, evt) + super().mouseReleaseEvent(evt) + + if ( + button != Qt.MouseButton.NoButton + and Preferences.getEditor("MouseClickHandlersEnabled") + and key in self.__mouseClickHandlers + ): + evt.accept() + self.__mouseClickHandlers[key][1](self) + else: + super().mouseReleaseEvent(evt) + + def setMouseClickHandler(self, name, modifiers, button, function): + """ + Public method to set a mouse click handler. + + @param name name of the plug-in (or 'internal') setting this handler + @type str + @param modifiers keyboard modifiers of the handler + @type Qt.KeyboardModifiers or int + @param button mouse button of the handler + @type Qt.MouseButton or int + @param function handler function + @type func + @return flag indicating success + @rtype bool + """ + if button and button != Qt.MouseButton.NoButton: + key = (modifiers, button) + if key in self.__mouseClickHandlers: + EricMessageBox.warning( + self, + self.tr("Register Mouse Click Handler"), + self.tr( + """A mouse click handler for "{0}" was already""" + """ registered by "{1}". Aborting request by""" + """ "{2}"...""" + ).format( + MouseUtilities.MouseButtonModifier2String(modifiers, button), + self.__mouseClickHandlers[key][0], + name, + ), + ) + return False + + self.__mouseClickHandlers[key] = (name, function) + return True + + return False + + def getMouseClickHandler(self, modifiers, button): + """ + Public method to get a registered mouse click handler. + + @param modifiers keyboard modifiers of the handler + @type Qt.KeyboardModifiers + @param button mouse button of the handler + @type Qt.MouseButton + @return plug-in name and registered function + @rtype tuple of str and func + """ + key = (modifiers, button) + if key in self.__mouseClickHandlers: + return self.__mouseClickHandlers[key] + else: + return ("", None) + + def getMouseClickHandlers(self, name): + """ + Public method to get all registered mouse click handlers of + a plug-in. + + @param name name of the plug-in + @type str + @return registered mouse click handlers as list of modifiers, + mouse button and function + @rtype list of tuple of (Qt.KeyboardModifiers, Qt.MouseButton, func) + """ + lst = [] + for key, value in self.__mouseClickHandlers.items(): + if value[0] == name: + lst.append((key[0], key[1], value[1])) + return lst + + def removeMouseClickHandler(self, modifiers, button): + """ + Public method to un-registered a mouse click handler. + + @param modifiers keyboard modifiers of the handler + @type Qt.KeyboardModifiers + @param button mouse button of the handler + @type Qt.MouseButton + """ + key = (modifiers, button) + if key in self.__mouseClickHandlers: + del self.__mouseClickHandlers[key] + + def removeMouseClickHandlers(self, name): + """ + Public method to un-registered all mouse click handlers of + a plug-in. + + @param name name of the plug-in + @type str + """ + keys = [] + for key in self.__mouseClickHandlers: + if self.__mouseClickHandlers[key][0] == name: + keys.append(key) + for key in keys: + del self.__mouseClickHandlers[key] + + def gotoReferenceHandler(self, referencesList): + """ + Public method to handle a list of references to perform a goto. + + @param referencesList list of references for a 'goto' action + @type ReferenceItem + """ + references = [] + referencePositions = [] + + for reference in referencesList: + if ( + reference.modulePath != self.getFileName() + or self.getCursorPosition()[0] + 1 != reference.line + ): + if reference.modulePath == self.getFileName(): + references.append( + self.tr("{0:4d} {1}", "line number, source code").format( + reference.line, reference.codeLine.strip() + ) + ) + else: + references.append( + self.tr( + "{0:4d} {1}\n => {2}", + "line number, source code, file name", + ).format( + reference.line, + reference.codeLine.strip(), + self.project.getRelativePath(reference.modulePath), + ) + ) + referencePositions.append( + (reference.modulePath, reference.line, reference.column) + ) + + if references: + if self.isCallTipActive(): + self.cancelCallTips() + self.__referencesList = references + self.__referencesPositionsList = referencePositions + self.showUserList(ReferencesListID, references) + + ####################################################################### + ## Methods implementing a Shell interface + ####################################################################### + + def __executeSelection(self): + """ + Private slot to execute the selected text in the shell window. + """ + txt = self.selectedText() + ericApp().getObject("Shell").executeLines(txt) + + ####################################################################### + ## Methods implementing the interface to EditorConfig + ####################################################################### + + def __loadEditorConfig(self, fileName=""): + """ + Private method to load the EditorConfig properties. + + @param fileName name of the file + @type str + """ + if not fileName: + fileName = self.fileName + + self.__editorConfig = self.__loadEditorConfigObject(fileName) + + if fileName: + self.__setTabAndIndent() + + def __loadEditorConfigObject(self, fileName): + """ + Private method to load the EditorConfig properties for the given + file name. + + @param fileName name of the file + @type str + @return EditorConfig dictionary + @rtype dict + """ + editorConfig = {} + + if fileName: + try: + editorConfig = editorconfig.get_properties(fileName) + except editorconfig.EditorConfigError: + EricMessageBox.warning( + self, + self.tr("EditorConfig Properties"), + self.tr( + """<p>The EditorConfig properties for file""" + """ <b>{0}</b> could not be loaded.</p>""" + ).format(fileName), + ) + + return editorConfig + + def __getEditorConfig(self, option, nodefault=False, config=None): + """ + Private method to get the requested option via EditorConfig. + + If there is no EditorConfig defined, the equivalent built-in option + will be used (Preferences.getEditor() ). The option must be given as + the Preferences option key. The mapping to the EditorConfig option name + will be done within this method. + + @param option Preferences option key + @type str + @param nodefault flag indicating to not get the default value from + Preferences but return None instead + @type bool + @param config reference to an EditorConfig object or None + @type dict + @return value of requested setting or None if nothing was found and + nodefault parameter was True + @rtype any + """ + if config is None: + config = self.__editorConfig + + if not config: + if nodefault: + return None + else: + value = self.__getOverrideValue(option) + if value is None: + # no override + value = Preferences.getEditor(option) + return value + + try: + if option == "EOLMode": + value = config["end_of_line"] + if value == "lf": + value = QsciScintilla.EolMode.EolUnix + elif value == "crlf": + value = QsciScintilla.EolMode.EolWindows + elif value == "cr": + value = QsciScintilla.EolMode.EolMac + else: + value = None + elif option == "DefaultEncoding": + value = config["charset"] + elif option == "InsertFinalNewline": + value = Utilities.toBool(config["insert_final_newline"]) + elif option == "StripTrailingWhitespace": + value = Utilities.toBool(config["trim_trailing_whitespace"]) + elif option == "TabWidth": + value = int(config["tab_width"]) + elif option == "IndentWidth": + value = config["indent_size"] + if value == "tab": + value = self.__getEditorConfig("TabWidth", config=config) + else: + value = int(value) + elif option == "TabForIndentation": + value = config["indent_style"] == "tab" + except KeyError: + value = None + + if value is None and not nodefault: + # use Preferences in case of error + value = self.__getOverrideValue(option) + if value is None: + # no override + value = Preferences.getEditor(option) + + return value + + def getEditorConfig(self, option): + """ + Public method to get the requested option via EditorConfig. + + @param option Preferences option key + @type str + @return value of requested setting + @rtype any + """ + return self.__getEditorConfig(option) + + def __getOverrideValue(self, option): + """ + Private method to get an override value for the current file type. + + @param option Preferences option key + @type str + @return override value; None in case nothing is defined + @rtype any + """ + if option in ("TabWidth", "IndentWidth"): + overrides = Preferences.getEditor("TabIndentOverride") + language = self.filetype or self.apiLanguage + if language in overrides: + if option == "TabWidth": + return overrides[language][0] + elif option == "IndentWidth": + return overrides[language][1] + + return None + + ####################################################################### + ## Methods implementing the docstring generator interface + ####################################################################### + + def getDocstringGenerator(self): + """ + Public method to get a reference to the docstring generator. + + @return reference to the docstring generator + @rtype BaseDocstringGenerator + """ + if self.__docstringGenerator is None: + from . import DocstringGenerator + + self.__docstringGenerator = DocstringGenerator.getDocstringGenerator(self) + + return self.__docstringGenerator + + def insertDocstring(self): + """ + Public method to generate and insert a docstring for the function under + the cursor. + + Note: This method is called via a keyboard shortcut or through the + global 'Edit' menu. + """ + generator = self.getDocstringGenerator() + generator.insertDocstringFromShortcut(self.getCursorPosition()) + + @pyqtSlot() + def __insertDocstring(self): + """ + Private slot to generate and insert a docstring for the function under + the cursor. + """ + generator = self.getDocstringGenerator() + generator.insertDocstring(self.getCursorPosition(), fromStart=True) + + def __delayedDocstringMenuPopup(self, cursorPosition): + """ + Private method to test, if the user might want to insert a docstring. + + @param cursorPosition current cursor position (line and column) + @type tuple of (int, int) + """ + if Preferences.getEditor( + "DocstringAutoGenerate" + ) and self.getDocstringGenerator().isDocstringIntro(cursorPosition): + lineText2Cursor = self.text(cursorPosition[0])[: cursorPosition[1]] + + QTimer.singleShot( + 300, lambda: self.__popupDocstringMenu(lineText2Cursor, cursorPosition) + ) + + def __popupDocstringMenu(self, lastLineText, lastCursorPosition): + """ + Private slot to pop up a menu asking the user, if a docstring should be + inserted. + + @param lastLineText line contents when the delay timer was started + @type str + @param lastCursorPosition position of the cursor when the delay timer + was started (line and index) + @type tuple of (int, int) + """ + cursorPosition = self.getCursorPosition() + if lastCursorPosition != cursorPosition: + return + + if self.text(cursorPosition[0])[: cursorPosition[1]] != lastLineText: + return + + generator = self.getDocstringGenerator() + if generator.hasFunctionDefinition(cursorPosition): + from .DocstringGenerator.BaseDocstringGenerator import ( + DocstringMenuForEnterOnly, + ) + + docstringMenu = DocstringMenuForEnterOnly(self) + act = docstringMenu.addAction( + UI.PixmapCache.getIcon("fileText"), + self.tr("Generate Docstring"), + lambda: generator.insertDocstring(cursorPosition, fromStart=False), + ) + docstringMenu.setActiveAction(act) + docstringMenu.popup(self.mapToGlobal(self.getGlobalCursorPosition())) + + ####################################################################### + ## Methods implementing the mouse hover help interface + ####################################################################### + + @pyqtSlot(int, int, int) + def __showMouseHoverHelp(self, pos, x, y): + """ + Private slot showing code information about the symbol under the + cursor. + + @param pos mouse position into the document + @type int + @param x x-value of mouse screen position + @type int + @param y y-value of mouse screen position + @type int + """ + if ( + not self.isCallTipActive() + and not self.isListActive() + and not self.menu.isVisible() + and not self.spellingMenu.isVisible() + ): + if self.__mouseHoverHelp is not None and pos > 0 and y > 0: + line, index = self.lineIndexFromPosition(pos) + if index > 0: + self.__mouseHoverHelp(self, line, index) + else: + self.__cancelMouseHoverHelp() + else: + self.__cancelMouseHoverHelp() + + def __cancelMouseHoverHelp(self): + """ + Private slot cancelling the display of mouse hover help. + """ + if self.__showingMouseHoverHelp: + self.cancelCallTips() + self.__showingMouseHoverHelp = False + + def registerMouseHoverHelpFunction(self, func): + """ + Public method to register a mouse hover help function. + + Note: Only one plugin should provide this function. Otherwise + the last one wins. + + @param func function accepting a reference to the calling editor and + the line and column position (zero based each) + @type func + """ + self.__mouseHoverHelp = func + + def unregisterMouseHoverHelpFunction(self, func): + """ + Public method to unregister a mouse hover help function. + + @param func function accepting a reference to the calling editor and + the line and column position (zero based each) + @type func + """ + if self.__mouseHoverHelp is func: + self.__mouseHoverHelp = None + + def showMouseHoverHelpData(self, line, index, data): + """ + Public method to show the mouse hover help data. + + @param line line of mouse cursor position + @type int + @param index column of mouse cursor position + @type int + @param data information text to be shown + @type str + """ + if data and self.hasFocus() and not self.isListActive(): + pos = self.positionFromLineIndex(line, index) + self.SendScintilla( + QsciScintilla.SCI_CALLTIPSHOW, pos, self._encodeString(data) + ) + self.__showingMouseHoverHelp = True + else: + self.__cancelMouseHoverHelp() + + ####################################################################### + ## Methods implementing the Black code formatting interface + ####################################################################### + + def __performFormatWithBlack(self, action): + """ + Private method to format the source code using the 'Black' tool. + + Following actions are supported. + <ul> + <li>BlackFormattingAction.Format - the code reformatting is performed</li> + <li>BlackFormattingAction.Check - a check is performed, if code formatting + is necessary</li> + <li>BlackFormattingAction.Diff - a unified diff of potential code formatting + changes is generated</li> + </ul> + + @param action formatting operation to be performed + @type BlackFormattingAction + """ + from CodeFormatting.BlackConfigurationDialog import BlackConfigurationDialog + from CodeFormatting.BlackFormattingDialog import BlackFormattingDialog + + if not self.isModified() or self.saveFile(): + withProject = ( + self.fileName + and self.project.isOpen() + and self.project.isProjectSource(self.fileName) + ) + dlg = BlackConfigurationDialog(withProject=withProject) + if dlg.exec() == QDialog.DialogCode.Accepted: + config = dlg.getConfiguration() + + formattingDialog = BlackFormattingDialog( + config, [self.fileName], project=self.project, action=action + ) + formattingDialog.exec()