Started implementing the editor outline widget.

Wed, 02 Sep 2020 18:13:12 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 02 Sep 2020 18:13:12 +0200
changeset 7685
0b6e8c0d6403
parent 7683
2fca14bea889
child 7686
379d402162ca

Started implementing the editor outline widget.

eric6.e4p file | annotate | diff | comparison | revisions
eric6/QScintilla/EditorAssembly.py file | annotate | diff | comparison | revisions
eric6/QScintilla/EditorOutline.py file | annotate | diff | comparison | revisions
eric6/QScintilla/EditorOutlineModel.py file | annotate | diff | comparison | revisions
eric6/Utilities/ClassBrowsers/idlclbr.py file | annotate | diff | comparison | revisions
eric6/Utilities/ClassBrowsers/jsclbr.py file | annotate | diff | comparison | revisions
eric6/Utilities/ClassBrowsers/protoclbr.py file | annotate | diff | comparison | revisions
eric6/Utilities/ClassBrowsers/pyclbr.py file | annotate | diff | comparison | revisions
eric6/Utilities/ClassBrowsers/rbclbr.py file | annotate | diff | comparison | revisions
--- 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.

eric ide

mercurial