Wed, 02 Sep 2020 18:13:12 +0200
Started implementing the editor outline widget.
--- a/eric6.e4p Wed Sep 02 17:58:19 2020 +0200 +++ b/eric6.e4p Wed Sep 02 18:13:12 2020 +0200 @@ -811,6 +811,8 @@ <Source>eric6/QScintilla/EditorAssembly.py</Source> <Source>eric6/QScintilla/EditorButtonsWidget.py</Source> <Source>eric6/QScintilla/EditorMarkerMap.py</Source> + <Source>eric6/QScintilla/EditorOutline.py</Source> + <Source>eric6/QScintilla/EditorOutlineModel.py</Source> <Source>eric6/QScintilla/Exporters/ExporterBase.py</Source> <Source>eric6/QScintilla/Exporters/ExporterHTML.py</Source> <Source>eric6/QScintilla/Exporters/ExporterODT.py</Source> @@ -2101,9 +2103,6 @@ <Other>eric6/APIs/MicroPython/circuitpython.api</Other> <Other>eric6/APIs/MicroPython/microbit.api</Other> <Other>eric6/APIs/MicroPython/micropython.api</Other> - <Other>eric6/APIs/Python/zope-2.10.7.api</Other> - <Other>eric6/APIs/Python/zope-2.11.2.api</Other> - <Other>eric6/APIs/Python/zope-3.3.1.api</Other> <Other>eric6/APIs/Python3/PyQt4.bas</Other> <Other>eric6/APIs/Python3/PyQt5.bas</Other> <Other>eric6/APIs/Python3/PyQtChart.bas</Other> @@ -2111,6 +2110,9 @@ <Other>eric6/APIs/Python3/QScintilla2.bas</Other> <Other>eric6/APIs/Python3/eric6.api</Other> <Other>eric6/APIs/Python3/eric6.bas</Other> + <Other>eric6/APIs/Python/zope-2.10.7.api</Other> + <Other>eric6/APIs/Python/zope-2.11.2.api</Other> + <Other>eric6/APIs/Python/zope-3.3.1.api</Other> <Other>eric6/APIs/QSS/qss.api</Other> <Other>eric6/APIs/Ruby/Ruby-1.8.7.api</Other> <Other>eric6/APIs/Ruby/Ruby-1.8.7.bas</Other>
--- a/eric6/QScintilla/EditorAssembly.py Wed Sep 02 17:58:19 2020 +0200 +++ b/eric6/QScintilla/EditorAssembly.py Wed Sep 02 18:13:12 2020 +0200 @@ -42,16 +42,26 @@ from .EditorButtonsWidget import EditorButtonsWidget from .Editor import Editor + from .EditorOutline import EditorOutlineView self.__editor = Editor(dbs, fn, vm, filetype, editor, tv) self.__buttonsWidget = EditorButtonsWidget(self.__editor, self) self.__globalsCombo = QComboBox() self.__membersCombo = QComboBox() + self.__sourceOutline = EditorOutlineView(self.__editor) + # TODO: make this configurable + self.__sourceOutline.setMaximumWidth(200) self.__layout.addWidget(self.__buttonsWidget, 1, 0, -1, 1) self.__layout.addWidget(self.__globalsCombo, 0, 1) self.__layout.addWidget(self.__membersCombo, 0, 2) - self.__layout.addWidget(self.__editor, 1, 1, 1, -1) + self.__layout.addWidget(self.__editor, 1, 1, 1, 2) + self.__layout.addWidget(self.__sourceOutline, 0, 3, -1, -1) + + if not self.__sourceOutline.isSupportedLanguage( + self.__editor.getLanguage() + ): + self.__sourceOutline.hide() self.setFocusProxy(self.__editor) @@ -66,6 +76,7 @@ self.__parseTimer.setSingleShot(True) self.__parseTimer.setInterval(5 * 1000) self.__parseTimer.timeout.connect(self.__parseEditor) + self.__parseTimer.timeout.connect(self.__sourceOutline.repopulate) self.__editor.textChanged.connect(self.__resetParseTimer) self.__editor.refreshed.connect(self.__resetParseTimer)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/QScintilla/EditorOutline.py Wed Sep 02 18:13:12 2020 +0200 @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an outline widget for source code navigation of the editor. +""" + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTreeView, QAbstractItemView + +from UI.BrowserSortFilterProxyModel import BrowserSortFilterProxyModel +from UI.BrowserModel import BrowserImportsItem, BrowserGlobalsItem + +from .EditorOutlineModel import EditorOutlineModel + + +# TODO: implement context menu +class EditorOutlineView(QTreeView): + """ + Class implementing an outline widget for source code navigation of the + editor. + """ + def __init__(self, editor, parent=None): + """ + Constructor + + @param editor reference to the editor widget + @type Editor + @param parent reference to the parent widget + @type QWidget + """ + super(EditorOutlineView, self).__init__(parent) + + self.__editor = editor + + self.__model = EditorOutlineModel(editor) + self.__sortModel = BrowserSortFilterProxyModel() + self.__sortModel.setSourceModel(self.__model) + self.setModel(self.__sortModel) + + self.setRootIsDecorated(True) + self.setAlternatingRowColors(True) + + header = self.header() + header.setSortIndicator(0, Qt.AscendingOrder) + header.setSortIndicatorShown(True) + header.setSectionsClickable(True) + + self.setSortingEnabled(True) + + self.setSelectionMode(QAbstractItemView.SingleSelection) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + + self.activated.connect(self.__gotoItem) + self.expanded.connect(self.__resizeColumns) + self.collapsed.connect(self.__resizeColumns) + + self.__resizeColumns() + + self.__expandedNames = [] + self.__currentItemName = "" + + def __resizeColumns(self): + """ + Private slot to resize the view when items get expanded or collapsed. + """ + self.resizeColumnToContents(0) + + def isPopulated(self): + """ + Public method to check, if the model is populated. + + @return flag indicating a populated model + @rtype bool + """ + return self.__model.isPopulated() + + def repopulate(self): + """ + Public slot to repopulate the model. + """ + if self.isPopulated(): + self.__prepareRepopulate() + self.__model.repopulate() + self.__completeRepopulate() + + def __prepareRepopulate(self): + """ + Private slot to prepare to repopulate the outline view. + """ + itm = self.__currentItem() + if itm is not None: + self.__currentItemName = itm.data(0) + + self.__expandedNames = [] + + childIndex = self.model().index(0, 0) + while childIndex.isValid(): + if self.isExpanded(childIndex): + self.__expandedNames.append( + self.model().item(childIndex).data(0)) + childIndex = self.indexBelow(childIndex) + + def __completeRepopulate(self): + """ + Private slot to complete the repopulate of the outline view. + """ + childIndex = self.model().index(0, 0) + while childIndex.isValid(): + name = self.model().item(childIndex).data(0) + if (self.__currentItemName and self.__currentItemName == name): + self.setCurrentIndex(childIndex) + if name in self.__expandedNames: + self.setExpanded(childIndex, True) + childIndex = self.indexBelow(childIndex) + + self.__expandedNames = [] + self.__currentItemName = "" + + def isSupportedLanguage(self, language): + """ + Public method to check, if outlining a given language is supported. + + @param language source language to be checked + @type str + @return flag indicating support + @rtype bool + """ + return language in EditorOutlineModel.SupportedLanguages + + def __gotoItem(self, index): + """ + Private slot to set the editor cursor. + + @param index index of the item to set the cursor for + @type QModelIndex + """ + if index.isValid(): + itm = self.model().item(index) + if itm: + try: + lineno = itm.lineno() + self.__editor.gotoLine(lineno) + except AttributeError: + # don't care + pass + + def mouseDoubleClickEvent(self, mouseEvent): + """ + Protected method of QAbstractItemView. + + Reimplemented to disable expanding/collapsing of items when + double-clicking. Instead the double-clicked entry is opened. + + @param mouseEvent the mouse event (QMouseEvent) + """ + index = self.indexAt(mouseEvent.pos()) + if index.isValid(): + itm = self.model().item(index) + if isinstance(itm, (BrowserImportsItem, BrowserGlobalsItem)): + self.setExpanded(index, not self.isExpanded(index)) + else: + self.__gotoItem(index) + + def __currentItem(self): + """ + Private method to get a reference to the current item. + + @return reference to the current item + @rtype BrowserItem + """ + itm = self.model().item(self.currentIndex()) + return itm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/QScintilla/EditorOutlineModel.py Wed Sep 02 18:13:12 2020 +0200 @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the editor outline model. +""" + +import os + +from PyQt5.QtCore import QCoreApplication, QModelIndex + +from UI.BrowserModel import ( + BrowserModel, BrowserItem, BrowserClassItem, BrowserCodingItem, + BrowserGlobalsItem, BrowserImportsItem, BrowserImportItem +) + + +class EditorOutlineModel(BrowserModel): + """ + Class implementing the editor outline model. + """ + SupportedLanguages = ( + "IDL", "JavaScript", "Protocol", "Python3", "MicroPython", "Ruby", + ) + + def __init__(self, editor): + """ + Constructor + + @param editor reference to the editor containing the source text + @type Editor + """ + super(EditorOutlineModel, self).__init__(nopopulate=True) + + self.__editor = editor + + self.__filename = self.__editor.getFileName() + self.__module = os.path.basename(self.__filename) + + self.__populated = False + + rootData = QCoreApplication.translate("EditorOutlineModel", "Name") + self.rootItem = BrowserItem(None, rootData) + + self.__populateModel() + + def __populateModel(self, repopulate=False): + """ + Private slot to populate the model. + + @param repopulate flag indicating a repopulation + @type bool + """ + language = self.__editor.getLanguage() + if language in EditorOutlineModel.SupportedLanguages: + if language in ("Python3", "MicroPython"): + from Utilities.ClassBrowsers import pyclbr + dictionary = pyclbr.scan(self.__editor.text(), self.__filename, + self.__module) + pyclbr._modules.clear() + + keys = list(dictionary.keys()) + if len(keys) > 0: + parentItem = self.rootItem + + if repopulate: + self.beginInsertRows( + QModelIndex(), + 0, len(keys) - 1) + + for key in keys: + if key.startswith("@@"): + # special treatment done later + continue + cl = dictionary[key] + try: + if cl.module == self.__module: + node = BrowserClassItem( + parentItem, cl, self.__filename) + self._addItem(node, parentItem) + except AttributeError: + pass + if "@@Coding@@" in keys: + node = BrowserCodingItem( + parentItem, + QCoreApplication.translate( + "EditorOutlineModel", "Coding: {0}") + .format(dictionary["@@Coding@@"].coding)) + self._addItem(node, parentItem) + if "@@Globals@@" in keys: + node = BrowserGlobalsItem( + parentItem, + dictionary["@@Globals@@"].globals, + QCoreApplication.translate( + "EditorOutlineModel", "Globals")) + self._addItem(node, parentItem) + if "@@Import@@" in keys or "@@ImportFrom@@" in keys: + node = BrowserImportsItem( + parentItem, + QCoreApplication.translate( + "EditorOutlineModel", "Imports")) + self._addItem(node, parentItem) + if "@@Import@@" in keys: + for importedModule in ( + dictionary["@@Import@@"].getImports().values() + ): + m_node = BrowserImportItem( + node, + importedModule.importedModuleName, + importedModule.file, + importedModule.linenos) + self._addItem(m_node, node) + for importedName, linenos in ( + importedModule.importedNames.items() + ): + mn_node = BrowserImportItem( + m_node, + importedName, + importedModule.file, + linenos, + isModule=False) + self._addItem(mn_node, m_node) + if repopulate: + self.endInsertRows() + + self.__populated = True + else: + self.clear() + self.__populated = False + + def isPopulated(self): + """ + Public method to check, if the model is populated. + + @return flag indicating a populated model + @rtype bool + """ + return self.__populated + + def repopulate(self): + """ + Public slot to repopulate the model. + """ + self.clear() + self.__populateModel(repopulate=True)
--- a/eric6/Utilities/ClassBrowsers/idlclbr.py Wed Sep 02 17:58:19 2020 +0200 +++ b/eric6/Utilities/ClassBrowsers/idlclbr.py Wed Sep 02 18:13:12 2020 +0200 @@ -199,6 +199,7 @@ VisibilityMixin.__init__(self) +# TODO: extract scan function (see pyclbr) def readmodule_ex(module, path=None): """ Read a CORBA IDL file and return a dictionary of classes, functions and
--- a/eric6/Utilities/ClassBrowsers/jsclbr.py Wed Sep 02 17:58:19 2020 +0200 +++ b/eric6/Utilities/ClassBrowsers/jsclbr.py Wed Sep 02 18:13:12 2020 +0200 @@ -279,6 +279,7 @@ self.__file, var.line)) +# TODO: extract scan function (see pyclbr) def readmodule_ex(module, path=None): """ Read a JavaScript file and return a dictionary of functions and variables.
--- a/eric6/Utilities/ClassBrowsers/protoclbr.py Wed Sep 02 17:58:19 2020 +0200 +++ b/eric6/Utilities/ClassBrowsers/protoclbr.py Wed Sep 02 18:13:12 2020 +0200 @@ -190,6 +190,7 @@ VisibilityMixin.__init__(self) +# TODO: extract scan function (see pyclbr) def readmodule_ex(module, path=None): """ Read a ProtoBuf protocol file and return a dictionary of messages, enums,
--- a/eric6/Utilities/ClassBrowsers/pyclbr.py Wed Sep 02 17:58:19 2020 +0200 +++ b/eric6/Utilities/ClassBrowsers/pyclbr.py Wed Sep 02 18:13:12 2020 +0200 @@ -343,16 +343,13 @@ """ global _modules - dictionary = {} - dict_counts = {} - if module in _modules: # we've seen this module before... return _modules[module] if module in sys.builtin_module_names: # this is a built-in module - _modules[module] = dictionary - return dictionary + _modules[module] = {} + return {} # search the path for the module path = [] if path is None else path[:] @@ -371,24 +368,45 @@ f.close() if type not in SUPPORTED_TYPES: # not Python source, can't do anything with this module - _modules[module] = dictionary - return dictionary + _modules[module] = {} + return {} + + try: + src = Utilities.readEncodedFile(file)[0] + except (UnicodeError, IOError): + # can't do anything with this module + _modules[module] = {} + return {} + + _modules[module] = scan(src, file, module) + return _modules[module] + - _modules[module] = dictionary +def scan(src, file, module): + """ + Public method to scan the given source text. + + @param src source text to be scanned + @type str + @param file file name associated with the source text + @type str + @param module module name associated with the source text + @type str + @return dictionary containing the extracted data + @rtype dict + """ + # convert eol markers the Python style + src = src.replace("\r\n", "\n").replace("\r", "\n") + + dictionary = {} + dict_counts = {} + classstack = [] # stack of (class, indent) pairs conditionalsstack = [] # stack of indents of conditional defines deltastack = [] deltaindent = 0 deltaindentcalculated = 0 - try: - src = Utilities.readEncodedFile(file)[0] - except (UnicodeError, IOError): - # can't do anything with this module - _modules[module] = dictionary - return dictionary - # convert eol markers the Python style - src = src.replace("\r\n", "\n").replace("\r", "\n") - + lineno, last_lineno_pos = 1, 0 lastGlobalEntry = None cur_obj = None
--- a/eric6/Utilities/ClassBrowsers/rbclbr.py Wed Sep 02 17:58:19 2020 +0200 +++ b/eric6/Utilities/ClassBrowsers/rbclbr.py Wed Sep 02 18:13:12 2020 +0200 @@ -250,6 +250,7 @@ self.setPrivate() +# TODO: extract scan function (see pyclbr) def readmodule_ex(module, path=None): """ Read a Ruby file and return a dictionary of classes, functions and modules.