Skip to content

Commit a60e74a

Browse files
committedMay 11, 2013
[pyqgis-console] a simple syntax checker for the editor
- some fixes and code cleanup
1 parent adb2653 commit a60e74a

File tree

4 files changed

+140
-74
lines changed

4 files changed

+140
-74
lines changed
 

‎images/images.qrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102
<file>themes/default/console/iconSearchEditorConsole.png</file>
103103
<file>themes/default/console/iconSearchNextEditorConsole.png</file>
104104
<file>themes/default/console/iconSearchPrevEditorConsole.png</file>
105+
<file>themes/default/console/iconSyntaxErrorConsole.png</file>
105106
<file>themes/default/extents.png</file>
106107
<file>themes/default/favourites.png</file>
107108
<file>themes/default/geographic.png</file>
Loading

‎python/console/console.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def __init__(self, parent=None):
160160
saveFileBt = QCoreApplication.translate("PythonConsole", "Save")
161161
self.saveFileButton = QAction(self)
162162
self.saveFileButton.setCheckable(False)
163-
self.saveFileButton.setEnabled(True)
163+
self.saveFileButton.setEnabled(False)
164164
self.saveFileButton.setIcon(QgsApplication.getThemeIcon("console/iconSaveConsole.png"))
165165
self.saveFileButton.setMenuRole(QAction.PreferencesRole)
166166
self.saveFileButton.setIconVisibleInMenu(True)
@@ -537,6 +537,11 @@ def _textFindChanged(self):
537537
self.findPrevButton.setEnabled(False)
538538

539539
def onClickGoToLine(self, item, column):
540+
if item.text(1) == 'syntaxError':
541+
check = self.tabEditorWidget.currentWidget().newEditor.syntaxCheck()
542+
if check:
543+
self.tabEditorWidget.currentWidget().save()
544+
return
540545
linenr = int(item.text(1))
541546
itemName = str(item.text(0))
542547
charPos = itemName.find(' ')

‎python/console/console_editor.py

Lines changed: 133 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from PyQt4.Qsci import (QsciScintilla,
2525
QsciScintillaBase,
2626
QsciLexerPython,
27-
QsciAPIs)
27+
QsciAPIs,
28+
QsciStyle)
2829
from qgis.core import QgsApplication
2930
from qgis.gui import QgsMessageBar
3031
import sys
@@ -73,6 +74,7 @@ def eventFilter(self, obj, event):
7374
return QObject.eventFilter(self, obj, event)
7475

7576
class Editor(QsciScintilla):
77+
MARKER_NUM = 6
7678
def __init__(self, parent=None):
7779
super(Editor,self).__init__(parent)
7880
self.parent = parent
@@ -81,6 +83,9 @@ def __init__(self, parent=None):
8183
self.opening = ['(', '{', '[', "'", '"']
8284
self.closing = [')', '}', ']', "'", '"']
8385

86+
## List of marker line to be deleted from check syntax
87+
self.bufferMarkerLine = []
88+
8489
self.settings = QSettings()
8590

8691
# Enable non-ascii chars for editor
@@ -95,24 +100,17 @@ def __init__(self, parent=None):
95100
self.setMarginsFont(font)
96101
# Margin 0 is used for line numbers
97102
#fm = QFontMetrics(font)
98-
#fontmetrics = QFontMetrics(font)
103+
fontmetrics = QFontMetrics(font)
99104
self.setMarginsFont(font)
100-
self.setMarginWidth(1, "00000")
101-
self.setMarginLineNumbers(1, True)
105+
self.setMarginWidth(0, fontmetrics.width("0000") + 5)
106+
self.setMarginLineNumbers(0, True)
102107
self.setMarginsForegroundColor(QColor("#3E3EE3"))
103108
self.setMarginsBackgroundColor(QColor("#f9f9f9"))
104109
self.setCaretLineVisible(True)
105110
self.setCaretLineBackgroundColor(QColor("#fcf3ed"))
106111

107-
# Clickable margin 1 for showing markers
108-
# self.setMarginSensitivity(1, True)
109-
# self.connect(self,
110-
# SIGNAL('marginClicked(int, int, Qt::KeyboardModifiers)'),
111-
# self.on_margin_clicked)
112-
# self.markerDefine(QsciScintilla.RightArrow,
113-
# self.ARROW_MARKER_NUM)
114-
# self.setMarkerBackgroundColor(QColor("#ee1111"),
115-
# self.ARROW_MARKER_NUM)
112+
self.markerDefine(QgsApplication.getThemePixmap("console/iconSyntaxErrorConsole.png"),
113+
self.MARKER_NUM)
116114

117115
self.setMinimumHeight(120)
118116
#self.setMinimumWidth(300)
@@ -138,7 +136,7 @@ def __init__(self, parent=None):
138136
self.settingsEditor()
139137

140138
# Annotations
141-
#self.setAnnotationDisplay(QsciScintilla.ANNOTATION_BOXED)
139+
self.setAnnotationDisplay(QsciScintilla.ANNOTATION_BOXED)
142140

143141
# Indentation
144142
self.setAutoIndent(True)
@@ -170,13 +168,17 @@ def __init__(self, parent=None):
170168
self.runScriptScut.setContext(Qt.WidgetShortcut)
171169
self.runScriptScut.activated.connect(self.runScriptCode)
172170

171+
self.syntaxCheckScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_4), self)
172+
self.syntaxCheckScut.setContext(Qt.WidgetShortcut)
173+
self.syntaxCheckScut.activated.connect(self.syntaxCheck)
173174
self.commentScut = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_3), self)
174175
self.commentScut.setContext(Qt.WidgetShortcut)
175176
self.commentScut.activated.connect(self.parent.pc.commentCode)
176177
self.uncommentScut = QShortcut(QKeySequence(Qt.SHIFT + Qt.CTRL + Qt.Key_3), self)
177178
self.uncommentScut.setContext(Qt.WidgetShortcut)
178179
self.uncommentScut.activated.connect(self.parent.pc.uncommentCode)
179180
self.modificationChanged.connect(self.parent.modified)
181+
self.modificationAttempted.connect(self.fileReadOnly)
180182

181183
def settingsEditor(self):
182184
# Set Python lexer
@@ -206,13 +208,6 @@ def autoCompleteKeyBinding(self):
206208
elif radioButtonSource == 'fromDocAPI':
207209
self.autoCompleteFromAll()
208210

209-
def on_margin_clicked(self, nmargin, nline, modifiers):
210-
# Toggle marker for the line the margin was clicked on
211-
if self.markersAtLine(nline) != 0:
212-
self.markerDelete(nline, self.ARROW_MARKER_NUM)
213-
else:
214-
self.markerAdd(nline, self.ARROW_MARKER_NUM)
215-
216211
def setLexers(self):
217212
from qgis.core import QgsApplication
218213

@@ -275,6 +270,7 @@ def contextMenuEvent(self, e):
275270
iconUncommentEditor = QgsApplication.getThemeIcon("console/iconUncommentEditorConsole.png")
276271
iconSettings = QgsApplication.getThemeIcon("console/iconSettingsConsole.png")
277272
iconFind = QgsApplication.getThemeIcon("console/iconSearchEditorConsole.png")
273+
iconSyntaxCk = QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.png")
278274
hideEditorAction = menu.addAction("Hide Editor",
279275
self.hideEditor)
280276
# menu.addSeparator()
@@ -284,9 +280,12 @@ def contextMenuEvent(self, e):
284280
# closeTabAction = menu.addAction("Close Tab",
285281
# self.parent.close, 'Ctrl+W')
286282
menu.addSeparator()
283+
syntaxCheck = menu.addAction(iconSyntaxCk, "Check Syntax",
284+
self.syntaxCheck, 'Ctrl+4')
285+
menu.addSeparator()
287286
runSelected = menu.addAction(iconRun,
288-
"Enter selected",
289-
self.runSelectedCode, 'Ctrl+E')
287+
"Enter selected",
288+
self.runSelectedCode, 'Ctrl+E')
290289
runScript = menu.addAction(iconRunScript,
291290
"Run Script",
292291
self.runScriptCode, 'Shift+Ctrl+E')
@@ -317,7 +316,7 @@ def contextMenuEvent(self, e):
317316
self.codepad)
318317
menu.addSeparator()
319318
showCodeInspection = menu.addAction("Hide/Show Object list",
320-
self.objectListEditor)
319+
self.objectListEditor)
321320
menu.addSeparator()
322321
selectAllAction = menu.addAction("Select All",
323322
self.selectAll,
@@ -326,6 +325,7 @@ def contextMenuEvent(self, e):
326325
settingsDialog = menu.addAction(iconSettings,
327326
"Settings",
328327
self.parent.pc.openSettings)
328+
syntaxCheck.setEnabled(False)
329329
pasteAction.setEnabled(False)
330330
codePadAction.setEnabled(False)
331331
cutAction.setEnabled(False)
@@ -344,6 +344,7 @@ def contextMenuEvent(self, e):
344344
codePadAction.setEnabled(True)
345345
if not self.text() == '':
346346
selectAllAction.setEnabled(True)
347+
syntaxCheck.setEnabled(True)
347348
if self.isUndoAvailable():
348349
undoAction.setEnabled(True)
349350
if self.isRedoAvailable():
@@ -479,8 +480,8 @@ def _runSubProcess(self, filename, tmp=False):
479480
pass
480481
else:
481482
raise e
482-
tmpFileTr = QCoreApplication.translate('PythonConsole', ' [Temporary file saved in ')
483483
if tmp:
484+
tmpFileTr = QCoreApplication.translate('PythonConsole', ' [Temporary file saved in ')
484485
name = name + tmpFileTr + dir + ']'
485486
if _traceback:
486487
msgTraceTr = QCoreApplication.translate('PythonConsole', '## Script error: %1').arg(name)
@@ -500,9 +501,9 @@ def _runSubProcess(self, filename, tmp=False):
500501
os.remove(filename)
501502
except IOError, error:
502503
IOErrorTr = QCoreApplication.translate('PythonConsole',
503-
'Cannot execute file %1. Error: %2') \
504-
.arg(filename).arg(error.strerror)
505-
print IOErrorTr
504+
'Cannot execute file %1. Error: %2\n') \
505+
.arg(str(filename)).arg(error.strerror)
506+
print '## Error: ' + IOErrorTr
506507
except:
507508
s = traceback.format_exc()
508509
print '## Error: '
@@ -518,7 +519,7 @@ def runScriptCode(self):
518519
msgEditorBlank = QCoreApplication.translate('PythonConsole',
519520
'Hey, type something for running !')
520521
msgEditorUnsaved = QCoreApplication.translate('PythonConsole',
521-
'You have to save the file before running.')
522+
'You have to save the file before running.')
522523
if not autoSave:
523524
if filename is None:
524525
if not self.isModified():
@@ -531,10 +532,12 @@ def runScriptCode(self):
531532
self.parent.pc.callWidgetMessageBarEditor(msgEditorUnsaved, 0, True)
532533
return
533534
else:
534-
self._runSubProcess(filename)
535+
if self.syntaxCheck(fromContextMenu=False):
536+
self._runSubProcess(filename)
535537
else:
536-
tmpFile = self.createTempFile()
537-
self._runSubProcess(tmpFile, True)
538+
if self.syntaxCheck(fromContextMenu=False):
539+
tmpFile = self.createTempFile()
540+
self._runSubProcess(tmpFile, True)
538541

539542
def runSelectedCode(self):
540543
cmd = self.selectedText()
@@ -559,6 +562,54 @@ def goToLine(self, objName, linenr):
559562
self.ensureLineVisible(linenr)
560563
self.setFocus()
561564

565+
def syntaxCheck(self, filename=None, fromContextMenu=True):
566+
eline = None
567+
ecolumn = 0
568+
edescr = ''
569+
source = unicode(self.text())
570+
try:
571+
if not filename:
572+
filename = self.parent.tw.currentWidget().path
573+
#source = open(filename, 'r').read() + '\n'
574+
if type(source) == type(u""):
575+
source = source.encode('utf-8')
576+
compile(source, str(filename), 'exec')
577+
except SyntaxError, detail:
578+
s = traceback.format_exception_only(SyntaxError, detail)
579+
fn = detail.filename
580+
eline = detail.lineno and detail.lineno or 1
581+
ecolumn = detail.offset and detail.offset or 1
582+
edescr = detail.msg
583+
if eline != None:
584+
eline -= 1
585+
for markerLine in self.bufferMarkerLine:
586+
self.markerDelete(markerLine)
587+
self.clearAnnotations(markerLine)
588+
self.bufferMarkerLine.remove(markerLine)
589+
if (eline) not in self.bufferMarkerLine:
590+
self.bufferMarkerLine.append(eline)
591+
self.markerAdd(eline, self.MARKER_NUM)
592+
loadFont = self.settings.value("pythonConsole/fontfamilytextEditor",
593+
"Monospace").toString()
594+
styleAnn = QsciStyle(-1,"Annotation",
595+
QColor(255,0,0),
596+
QColor(255,200,0),
597+
QFont(loadFont, 8,-1,True),
598+
True)
599+
self.annotate(eline, edescr, styleAnn)
600+
self.setCursorPosition(eline, ecolumn-1)
601+
#self.setSelection(eline, ecolumn, eline, self.lineLength(eline)-1)
602+
self.ensureLineVisible(eline)
603+
#self.ensureCursorVisible()
604+
return False
605+
else:
606+
self.markerDeleteAll()
607+
self.clearAnnotations()
608+
if fromContextMenu:
609+
msgText = QCoreApplication.translate('PythonConsole', 'Syntax ok')
610+
self.parent.pc.callWidgetMessageBarEditor(msgText, 0, True)
611+
return True
612+
562613
def keyPressEvent(self, e):
563614
t = unicode(e.text())
564615
## Close bracket automatically
@@ -581,15 +632,11 @@ def focusInEvent(self, e):
581632
self.selectAll()
582633
#fileReplaced = self.selectedText()
583634
self.removeSelectedText()
635+
file = open(pathfile, "r")
636+
fileLines = file.readlines()
637+
file.close()
584638
QApplication.setOverrideCursor(Qt.WaitCursor)
585-
try:
586-
file = open(pathfile, "r").readlines()
587-
except IOError, error:
588-
IOErrorTr = QCoreApplication.translate('PythonConsole',
589-
'The file %1 could not be opened. Error: %2') \
590-
.arg(pathfile).arg(error.strerror)
591-
print IOErrorTr
592-
for line in reversed(file):
639+
for line in reversed(fileLines):
593640
self.insert(line)
594641
QApplication.restoreOverrideCursor()
595642
self.setModified(True)
@@ -603,12 +650,18 @@ def focusInEvent(self, e):
603650
self.parent.pc.callWidgetMessageBarEditor(msgText, 1, False)
604651
QsciScintilla.focusInEvent(self, e)
605652

653+
def fileReadOnly(self):
654+
msgText = QCoreApplication.translate('PythonConsole',
655+
'Read only file, please save to different file first.')
656+
self.parent.pc.callWidgetMessageBarEditor(msgText, 1, False)
657+
606658
class EditorTab(QWidget):
607-
def __init__(self, parent, parentConsole, filename, *args):
608-
QWidget.__init__(self, parent=None, *args)
659+
def __init__(self, parent, parentConsole, filename, readOnly):
660+
super(EditorTab, self).__init__(parent)
609661
self.tw = parent
610662
self.pc = parentConsole
611663
self.path = None
664+
self.readOnly = readOnly
612665

613666
self.fileExcuteList = {}
614667
self.fileExcuteList = dict()
@@ -638,17 +691,13 @@ def __init__(self, parent, parentConsole, filename, *args):
638691
self.setEventFilter(self.keyFilter)
639692

640693
def loadFile(self, filename, modified):
641-
try:
642-
fn = open(unicode(filename), "rb")
643-
except IOError, error:
644-
IOErrorTr = QCoreApplication.translate('PythonConsole',
645-
'The file <b>%1</b> could not be opened. Error: %2') \
646-
.arg(filename).arg(error.strerror)
647-
print IOErrorTr
648-
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
694+
fn = open(unicode(filename), "rb")
649695
txt = fn.read()
650696
fn.close()
697+
QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
651698
self.newEditor.setText(txt)
699+
if self.readOnly:
700+
self.newEditor.setReadOnly(self.readOnly)
652701
QApplication.restoreOverrideCursor()
653702
self.newEditor.setModified(modified)
654703
self.newEditor.mtime = os.stat(filename).st_mtime
@@ -795,7 +844,7 @@ def __init__(self, parent):
795844
self.connect(self, SIGNAL("tabCloseRequested(int)"), self._removeTab)
796845
self.connect(self, SIGNAL('currentChanged(int)'), self._currentWidgetChanged)
797846

798-
# Open button
847+
# New Editor button
799848
self.newTabButton = QToolButton()
800849
txtToolTipNewTab = QCoreApplication.translate("PythonConsole",
801850
"New Editor")
@@ -859,31 +908,42 @@ def closeAll(self):
859908
self._removeTab(0)
860909

861910
def enableSaveIfModified(self, tab):
862-
self.parent.saveFileButton.setEnabled(self.widget(tab).newEditor.isModified())
911+
tabWidget = self.widget(tab)
912+
if tabWidget:
913+
self.parent.saveFileButton.setEnabled(tabWidget.newEditor.isModified())
863914

864915
def enableToolBarEditor(self, enable):
865916
if self.topFrame.isVisible():
866917
enable = False
867918
self.parent.toolBarEditor.setEnabled(enable)
868919

869920
def newTabEditor(self, tabName=None, filename=None):
921+
readOnly = False
922+
if filename:
923+
readOnly = not QFileInfo(filename).isWritable()
924+
try:
925+
fn = open(unicode(filename), "rb")
926+
txt = fn.read()
927+
fn.close()
928+
except IOError, error:
929+
IOErrorTr = QCoreApplication.translate('PythonConsole',
930+
'The file %1 could not be opened. Error: %2\n') \
931+
.arg(str(filename)).arg(error.strerror)
932+
print '## Error: '
933+
sys.stderr.write(IOErrorTr)
934+
return
935+
870936
nr = self.count()
871937
if not tabName:
872938
tabName = QCoreApplication.translate('PythonConsole', 'Untitled-%1').arg(nr)
873-
# if self.count() < 1:
874-
# self.setTabsClosable(False)
875-
# else:
876-
# if not self.tabsClosable():
877-
# self.setTabsClosable(True)
878-
self.tab = EditorTab(self, self.parent, filename)
939+
self.tab = EditorTab(self, self.parent, filename, readOnly)
879940
self.iconTab = QgsApplication.getThemeIcon('console/iconTabEditorConsole.png')
880-
self.addTab(self.tab, self.iconTab, tabName)
941+
self.addTab(self.tab, self.iconTab, tabName + ' (ro)' if readOnly else tabName)
881942
self.setCurrentWidget(self.tab)
882943
if filename:
883944
self.setTabToolTip(self.currentIndex(), unicode(filename))
884945
else:
885946
self.setTabToolTip(self.currentIndex(), tabName)
886-
self.parent.saveFileButton.setEnabled(False)
887947

888948
def tabModified(self, tab, modified):
889949
index = self.indexOf(tab)
@@ -892,15 +952,8 @@ def tabModified(self, tab, modified):
892952
self.parent.saveFileButton.setEnabled(modified)
893953

894954
def closeTab(self, tab):
895-
# Check if file has been saved
896-
#if isModified:
897-
#self.checkSaveFile()
898-
#else:
899-
#if self.indexOf(tab) > 0:
900955
if self.count() < 2:
901-
#self.setTabsClosable(False)
902956
self.removeTab(self.indexOf(tab))
903-
#pass
904957
self.newTabEditor()
905958
else:
906959
self.removeTab(self.indexOf(tab))
@@ -926,13 +979,14 @@ def _removeTab(self, tab, tab2index=False):
926979
return
927980
else:
928981
self.parent.updateTabListScript(self.widget(tab).path)
929-
self.removeTab(tab)
982+
if self.count() <= 1:
983+
self.removeTab(tab)
984+
self.newTabEditor()
930985
else:
931986
if self.widget(tab).path is not None or \
932987
self.widget(tab).path in self.restoreTabList:
933988
self.parent.updateTabListScript(self.widget(tab).path)
934989
if self.count() <= 1:
935-
# self.setTabsClosable(False)
936990
self.removeTab(tab)
937991
self.newTabEditor()
938992
else:
@@ -950,7 +1004,6 @@ def closeCurrentWidget(self):
9501004
if currWidget:
9511005
currWidget.setFocus(Qt.TabFocusReason)
9521006
if currWidget.path in self.restoreTabList:
953-
#print currWidget.path
9541007
self.parent.updateTabListScript(currWidget.path)
9551008

9561009
def restoreTabs(self):
@@ -1055,9 +1108,16 @@ def listObject(self, tab):
10551108
if found:
10561109
sys.path.remove(pathFile)
10571110
except:
1058-
s = traceback.format_exc()
1059-
print '## Error: '
1060-
sys.stderr.write(s)
1111+
msgItem = QTreeWidgetItem()
1112+
msgItem.setText(0, QCoreApplication.translate("PythonConsole", "Check Syntax"))
1113+
msgItem.setText(1, 'syntaxError')
1114+
iconWarning = QgsApplication.getThemeIcon("console/iconSyntaxErrorConsole.png")
1115+
msgItem.setIcon(0, iconWarning)
1116+
self.parent.listClassMethod.addTopLevelItem(msgItem)
1117+
#s = traceback.format_exc()
1118+
#print '## Error: '
1119+
#sys.stderr.write(s)
1120+
#pass
10611121

10621122
def refreshSettingsEditor(self):
10631123
countTab = self.count()

0 commit comments

Comments
 (0)
Please sign in to comment.