From a60e74a01ed0e2e9b3c67f2729ffec7ff3dfcb01 Mon Sep 17 00:00:00 2001 From: Salvatore Larosa Date: Wed, 8 May 2013 23:31:07 +0200 Subject: [PATCH] [pyqgis-console] a simple syntax checker for the editor - some fixes and code cleanup --- images/images.qrc | 1 + .../console/iconSyntaxErrorConsole.png | Bin 0 -> 566 bytes python/console/console.py | 7 +- python/console/console_editor.py | 206 +++++++++++------- 4 files changed, 140 insertions(+), 74 deletions(-) create mode 100644 images/themes/default/console/iconSyntaxErrorConsole.png diff --git a/images/images.qrc b/images/images.qrc index 48bde211e4f..c7f06ce5e8f 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -102,6 +102,7 @@ themes/default/console/iconSearchEditorConsole.png themes/default/console/iconSearchNextEditorConsole.png themes/default/console/iconSearchPrevEditorConsole.png + themes/default/console/iconSyntaxErrorConsole.png themes/default/extents.png themes/default/favourites.png themes/default/geographic.png diff --git a/images/themes/default/console/iconSyntaxErrorConsole.png b/images/themes/default/console/iconSyntaxErrorConsole.png new file mode 100644 index 0000000000000000000000000000000000000000..02d965da8a9664f17ed3c62972e698db4c8399a9 GIT binary patch literal 566 zcmV-60?GY}P)CW~Z516kC?HLVna@0h|{ z+|=~TL#wVxmIoD#xtMKMGA~(>T#)GIe#vXebIEb7)II3k>&c!>)l%+sFE2tV$F6% zv9Z&jWski>q@5#h@7Z12I9yT`CF?4xL!_AzxEC$ZE=ktay!U>Huv_Qv4ICLd`;A2n zyeoq>PrWkhkjh`u2F!QmP7XC#K-HqvEOm@j6kFANaN8!cUD6`%VF@SuRQ$kctG+rT zdD3s;)0wpV*GT94Jg~D{dnP%mnY#P`wsN_+r0kZns^*OQh$d$71-CS{-PLwg(i!7F zmT6li+)Le;l>N+vW~!F6!HaP6|I0KSMN!8V$){12KiYQ?Rt&}x!~g&Q07*qoM6N<$ Ef|=a}=Kufz literal 0 HcmV?d00001 diff --git a/python/console/console.py b/python/console/console.py index 65376d25ed5..aa98e948ee2 100644 --- a/python/console/console.py +++ b/python/console/console.py @@ -160,7 +160,7 @@ class PythonConsoleWidget(QWidget): saveFileBt = QCoreApplication.translate("PythonConsole", "Save") self.saveFileButton = QAction(self) self.saveFileButton.setCheckable(False) - self.saveFileButton.setEnabled(True) + self.saveFileButton.setEnabled(False) self.saveFileButton.setIcon(QgsApplication.getThemeIcon("console/iconSaveConsole.png")) self.saveFileButton.setMenuRole(QAction.PreferencesRole) self.saveFileButton.setIconVisibleInMenu(True) @@ -537,6 +537,11 @@ class PythonConsoleWidget(QWidget): self.findPrevButton.setEnabled(False) def onClickGoToLine(self, item, column): + if item.text(1) == 'syntaxError': + check = self.tabEditorWidget.currentWidget().newEditor.syntaxCheck() + if check: + self.tabEditorWidget.currentWidget().save() + return linenr = int(item.text(1)) itemName = str(item.text(0)) charPos = itemName.find(' ') diff --git a/python/console/console_editor.py b/python/console/console_editor.py index 500208ef5b6..79e1e5782bd 100644 --- a/python/console/console_editor.py +++ b/python/console/console_editor.py @@ -24,7 +24,8 @@ from PyQt4.QtGui import * from PyQt4.Qsci import (QsciScintilla, QsciScintillaBase, QsciLexerPython, - QsciAPIs) + QsciAPIs, + QsciStyle) from qgis.core import QgsApplication from qgis.gui import QgsMessageBar import sys @@ -73,6 +74,7 @@ class KeyFilter(QObject): return QObject.eventFilter(self, obj, event) class Editor(QsciScintilla): + MARKER_NUM = 6 def __init__(self, parent=None): super(Editor,self).__init__(parent) self.parent = parent @@ -81,6 +83,9 @@ class Editor(QsciScintilla): self.opening = ['(', '{', '[', "'", '"'] self.closing = [')', '}', ']', "'", '"'] + ## List of marker line to be deleted from check syntax + self.bufferMarkerLine = [] + self.settings = QSettings() # Enable non-ascii chars for editor @@ -95,24 +100,17 @@ class Editor(QsciScintilla): self.setMarginsFont(font) # Margin 0 is used for line numbers #fm = QFontMetrics(font) - #fontmetrics = QFontMetrics(font) + fontmetrics = QFontMetrics(font) self.setMarginsFont(font) - self.setMarginWidth(1, "00000") - self.setMarginLineNumbers(1, True) + self.setMarginWidth(0, fontmetrics.width("0000") + 5) + self.setMarginLineNumbers(0, True) self.setMarginsForegroundColor(QColor("#3E3EE3")) self.setMarginsBackgroundColor(QColor("#f9f9f9")) self.setCaretLineVisible(True) self.setCaretLineBackgroundColor(QColor("#fcf3ed")) - # Clickable margin 1 for showing markers -# self.setMarginSensitivity(1, True) -# self.connect(self, -# SIGNAL('marginClicked(int, int, Qt::KeyboardModifiers)'), -# self.on_margin_clicked) -# self.markerDefine(QsciScintilla.RightArrow, -# self.ARROW_MARKER_NUM) -# self.setMarkerBackgroundColor(QColor("#ee1111"), -# self.ARROW_MARKER_NUM) + self.markerDefine(QgsApplication.getThemePixmap("console/iconSyntaxErrorConsole.png"), + self.MARKER_NUM) self.setMinimumHeight(120) #self.setMinimumWidth(300) @@ -138,7 +136,7 @@ class Editor(QsciScintilla): self.settingsEditor() # Annotations - #self.setAnnotationDisplay(QsciScintilla.ANNOTATION_BOXED) + self.setAnnotationDisplay(QsciScintilla.ANNOTATION_BOXED) # Indentation self.setAutoIndent(True) @@ -170,6 +168,9 @@ class Editor(QsciScintilla): self.runScriptScut.setContext(Qt.WidgetShortcut) self.runScriptScut.activated.connect(self.runScriptCode) + self.syntaxCheckScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_4), self) + self.syntaxCheckScut.setContext(Qt.WidgetShortcut) + self.syntaxCheckScut.activated.connect(self.syntaxCheck) self.commentScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_3), self) self.commentScut.setContext(Qt.WidgetShortcut) self.commentScut.activated.connect(self.parent.pc.commentCode) @@ -177,6 +178,7 @@ class Editor(QsciScintilla): self.uncommentScut.setContext(Qt.WidgetShortcut) self.uncommentScut.activated.connect(self.parent.pc.uncommentCode) self.modificationChanged.connect(self.parent.modified) + self.modificationAttempted.connect(self.fileReadOnly) def settingsEditor(self): # Set Python lexer @@ -206,13 +208,6 @@ class Editor(QsciScintilla): elif radioButtonSource == 'fromDocAPI': self.autoCompleteFromAll() - def on_margin_clicked(self, nmargin, nline, modifiers): - # Toggle marker for the line the margin was clicked on - if self.markersAtLine(nline) != 0: - self.markerDelete(nline, self.ARROW_MARKER_NUM) - else: - self.markerAdd(nline, self.ARROW_MARKER_NUM) - def setLexers(self): from qgis.core import QgsApplication @@ -275,6 +270,7 @@ class Editor(QsciScintilla): iconUncommentEditor = QgsApplication.getThemeIcon("console/iconUncommentEditorConsole.png") iconSettings = QgsApplication.getThemeIcon("console/iconSettingsConsole.png") iconFind = QgsApplication.getThemeIcon("console/iconSearchEditorConsole.png") + iconSyntaxCk = QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.png") hideEditorAction = menu.addAction("Hide Editor", self.hideEditor) # menu.addSeparator() @@ -284,9 +280,12 @@ class Editor(QsciScintilla): # closeTabAction = menu.addAction("Close Tab", # self.parent.close, 'Ctrl+W') menu.addSeparator() + syntaxCheck = menu.addAction(iconSyntaxCk, "Check Syntax", + self.syntaxCheck, 'Ctrl+4') + menu.addSeparator() runSelected = menu.addAction(iconRun, - "Enter selected", - self.runSelectedCode, 'Ctrl+E') + "Enter selected", + self.runSelectedCode, 'Ctrl+E') runScript = menu.addAction(iconRunScript, "Run Script", self.runScriptCode, 'Shift+Ctrl+E') @@ -317,7 +316,7 @@ class Editor(QsciScintilla): self.codepad) menu.addSeparator() showCodeInspection = menu.addAction("Hide/Show Object list", - self.objectListEditor) + self.objectListEditor) menu.addSeparator() selectAllAction = menu.addAction("Select All", self.selectAll, @@ -326,6 +325,7 @@ class Editor(QsciScintilla): settingsDialog = menu.addAction(iconSettings, "Settings", self.parent.pc.openSettings) + syntaxCheck.setEnabled(False) pasteAction.setEnabled(False) codePadAction.setEnabled(False) cutAction.setEnabled(False) @@ -344,6 +344,7 @@ class Editor(QsciScintilla): codePadAction.setEnabled(True) if not self.text() == '': selectAllAction.setEnabled(True) + syntaxCheck.setEnabled(True) if self.isUndoAvailable(): undoAction.setEnabled(True) if self.isRedoAvailable(): @@ -479,8 +480,8 @@ class Editor(QsciScintilla): pass else: raise e - tmpFileTr = QCoreApplication.translate('PythonConsole', ' [Temporary file saved in ') if tmp: + tmpFileTr = QCoreApplication.translate('PythonConsole', ' [Temporary file saved in ') name = name + tmpFileTr + dir + ']' if _traceback: msgTraceTr = QCoreApplication.translate('PythonConsole', '## Script error: %1').arg(name) @@ -500,9 +501,9 @@ class Editor(QsciScintilla): os.remove(filename) except IOError, error: IOErrorTr = QCoreApplication.translate('PythonConsole', - 'Cannot execute file %1. Error: %2') \ - .arg(filename).arg(error.strerror) - print IOErrorTr + 'Cannot execute file %1. Error: %2\n') \ + .arg(str(filename)).arg(error.strerror) + print '## Error: ' + IOErrorTr except: s = traceback.format_exc() print '## Error: ' @@ -518,7 +519,7 @@ class Editor(QsciScintilla): msgEditorBlank = QCoreApplication.translate('PythonConsole', 'Hey, type something for running !') msgEditorUnsaved = QCoreApplication.translate('PythonConsole', - 'You have to save the file before running.') + 'You have to save the file before running.') if not autoSave: if filename is None: if not self.isModified(): @@ -531,10 +532,12 @@ class Editor(QsciScintilla): self.parent.pc.callWidgetMessageBarEditor(msgEditorUnsaved, 0, True) return else: - self._runSubProcess(filename) + if self.syntaxCheck(fromContextMenu=False): + self._runSubProcess(filename) else: - tmpFile = self.createTempFile() - self._runSubProcess(tmpFile, True) + if self.syntaxCheck(fromContextMenu=False): + tmpFile = self.createTempFile() + self._runSubProcess(tmpFile, True) def runSelectedCode(self): cmd = self.selectedText() @@ -559,6 +562,54 @@ class Editor(QsciScintilla): self.ensureLineVisible(linenr) self.setFocus() + def syntaxCheck(self, filename=None, fromContextMenu=True): + eline = None + ecolumn = 0 + edescr = '' + source = unicode(self.text()) + try: + if not filename: + filename = self.parent.tw.currentWidget().path + #source = open(filename, 'r').read() + '\n' + if type(source) == type(u""): + source = source.encode('utf-8') + compile(source, str(filename), 'exec') + except SyntaxError, detail: + s = traceback.format_exception_only(SyntaxError, detail) + fn = detail.filename + eline = detail.lineno and detail.lineno or 1 + ecolumn = detail.offset and detail.offset or 1 + edescr = detail.msg + if eline != None: + eline -= 1 + for markerLine in self.bufferMarkerLine: + self.markerDelete(markerLine) + self.clearAnnotations(markerLine) + self.bufferMarkerLine.remove(markerLine) + if (eline) not in self.bufferMarkerLine: + self.bufferMarkerLine.append(eline) + self.markerAdd(eline, self.MARKER_NUM) + loadFont = self.settings.value("pythonConsole/fontfamilytextEditor", + "Monospace").toString() + styleAnn = QsciStyle(-1,"Annotation", + QColor(255,0,0), + QColor(255,200,0), + QFont(loadFont, 8,-1,True), + True) + self.annotate(eline, edescr, styleAnn) + self.setCursorPosition(eline, ecolumn-1) + #self.setSelection(eline, ecolumn, eline, self.lineLength(eline)-1) + self.ensureLineVisible(eline) + #self.ensureCursorVisible() + return False + else: + self.markerDeleteAll() + self.clearAnnotations() + if fromContextMenu: + msgText = QCoreApplication.translate('PythonConsole', 'Syntax ok') + self.parent.pc.callWidgetMessageBarEditor(msgText, 0, True) + return True + def keyPressEvent(self, e): t = unicode(e.text()) ## Close bracket automatically @@ -581,15 +632,11 @@ class Editor(QsciScintilla): self.selectAll() #fileReplaced = self.selectedText() self.removeSelectedText() + file = open(pathfile, "r") + fileLines = file.readlines() + file.close() QApplication.setOverrideCursor(Qt.WaitCursor) - try: - file = open(pathfile, "r").readlines() - except IOError, error: - IOErrorTr = QCoreApplication.translate('PythonConsole', - 'The file %1 could not be opened. Error: %2') \ - .arg(pathfile).arg(error.strerror) - print IOErrorTr - for line in reversed(file): + for line in reversed(fileLines): self.insert(line) QApplication.restoreOverrideCursor() self.setModified(True) @@ -603,12 +650,18 @@ class Editor(QsciScintilla): self.parent.pc.callWidgetMessageBarEditor(msgText, 1, False) QsciScintilla.focusInEvent(self, e) + def fileReadOnly(self): + msgText = QCoreApplication.translate('PythonConsole', + 'Read only file, please save to different file first.') + self.parent.pc.callWidgetMessageBarEditor(msgText, 1, False) + class EditorTab(QWidget): - def __init__(self, parent, parentConsole, filename, *args): - QWidget.__init__(self, parent=None, *args) + def __init__(self, parent, parentConsole, filename, readOnly): + super(EditorTab, self).__init__(parent) self.tw = parent self.pc = parentConsole self.path = None + self.readOnly = readOnly self.fileExcuteList = {} self.fileExcuteList = dict() @@ -638,17 +691,13 @@ class EditorTab(QWidget): self.setEventFilter(self.keyFilter) def loadFile(self, filename, modified): - try: - fn = open(unicode(filename), "rb") - except IOError, error: - IOErrorTr = QCoreApplication.translate('PythonConsole', - 'The file %1 could not be opened. Error: %2') \ - .arg(filename).arg(error.strerror) - print IOErrorTr - QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + fn = open(unicode(filename), "rb") txt = fn.read() fn.close() + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self.newEditor.setText(txt) + if self.readOnly: + self.newEditor.setReadOnly(self.readOnly) QApplication.restoreOverrideCursor() self.newEditor.setModified(modified) self.newEditor.mtime = os.stat(filename).st_mtime @@ -795,7 +844,7 @@ class EditorTabWidget(QTabWidget): self.connect(self, SIGNAL("tabCloseRequested(int)"), self._removeTab) self.connect(self, SIGNAL('currentChanged(int)'), self._currentWidgetChanged) - # Open button + # New Editor button self.newTabButton = QToolButton() txtToolTipNewTab = QCoreApplication.translate("PythonConsole", "New Editor") @@ -859,7 +908,9 @@ class EditorTabWidget(QTabWidget): self._removeTab(0) def enableSaveIfModified(self, tab): - self.parent.saveFileButton.setEnabled(self.widget(tab).newEditor.isModified()) + tabWidget = self.widget(tab) + if tabWidget: + self.parent.saveFileButton.setEnabled(tabWidget.newEditor.isModified()) def enableToolBarEditor(self, enable): if self.topFrame.isVisible(): @@ -867,23 +918,32 @@ class EditorTabWidget(QTabWidget): self.parent.toolBarEditor.setEnabled(enable) def newTabEditor(self, tabName=None, filename=None): + readOnly = False + if filename: + readOnly = not QFileInfo(filename).isWritable() + try: + fn = open(unicode(filename), "rb") + txt = fn.read() + fn.close() + except IOError, error: + IOErrorTr = QCoreApplication.translate('PythonConsole', + 'The file %1 could not be opened. Error: %2\n') \ + .arg(str(filename)).arg(error.strerror) + print '## Error: ' + sys.stderr.write(IOErrorTr) + return + nr = self.count() if not tabName: tabName = QCoreApplication.translate('PythonConsole', 'Untitled-%1').arg(nr) -# if self.count() < 1: -# self.setTabsClosable(False) -# else: -# if not self.tabsClosable(): -# self.setTabsClosable(True) - self.tab = EditorTab(self, self.parent, filename) + self.tab = EditorTab(self, self.parent, filename, readOnly) self.iconTab = QgsApplication.getThemeIcon('console/iconTabEditorConsole.png') - self.addTab(self.tab, self.iconTab, tabName) + self.addTab(self.tab, self.iconTab, tabName + ' (ro)' if readOnly else tabName) self.setCurrentWidget(self.tab) if filename: self.setTabToolTip(self.currentIndex(), unicode(filename)) else: self.setTabToolTip(self.currentIndex(), tabName) - self.parent.saveFileButton.setEnabled(False) def tabModified(self, tab, modified): index = self.indexOf(tab) @@ -892,15 +952,8 @@ class EditorTabWidget(QTabWidget): self.parent.saveFileButton.setEnabled(modified) def closeTab(self, tab): - # Check if file has been saved - #if isModified: - #self.checkSaveFile() - #else: - #if self.indexOf(tab) > 0: if self.count() < 2: - #self.setTabsClosable(False) self.removeTab(self.indexOf(tab)) - #pass self.newTabEditor() else: self.removeTab(self.indexOf(tab)) @@ -926,13 +979,14 @@ class EditorTabWidget(QTabWidget): return else: self.parent.updateTabListScript(self.widget(tab).path) - self.removeTab(tab) + if self.count() <= 1: + self.removeTab(tab) + self.newTabEditor() else: if self.widget(tab).path is not None or \ self.widget(tab).path in self.restoreTabList: self.parent.updateTabListScript(self.widget(tab).path) if self.count() <= 1: -# self.setTabsClosable(False) self.removeTab(tab) self.newTabEditor() else: @@ -950,7 +1004,6 @@ class EditorTabWidget(QTabWidget): if currWidget: currWidget.setFocus(Qt.TabFocusReason) if currWidget.path in self.restoreTabList: - #print currWidget.path self.parent.updateTabListScript(currWidget.path) def restoreTabs(self): @@ -1055,9 +1108,16 @@ class EditorTabWidget(QTabWidget): if found: sys.path.remove(pathFile) except: - s = traceback.format_exc() - print '## Error: ' - sys.stderr.write(s) + msgItem = QTreeWidgetItem() + msgItem.setText(0, QCoreApplication.translate("PythonConsole", "Check Syntax")) + msgItem.setText(1, 'syntaxError') + iconWarning = QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.png") + msgItem.setIcon(0, iconWarning) + self.parent.listClassMethod.addTopLevelItem(msgItem) + #s = traceback.format_exc() + #print '## Error: ' + #sys.stderr.write(s) + #pass def refreshSettingsEditor(self): countTab = self.count()