Continued implementing the editor outline widget.

Fri, 04 Sep 2020 18:50:43 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 04 Sep 2020 18:50:43 +0200
changeset 7690
a59680062837
parent 7689
147236d850a4
child 7692
94f0017f9372

Continued implementing the editor outline widget.

eric6.e4p file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/PyProfile.py file | annotate | diff | comparison | revisions
eric6/Preferences/ConfigurationPages/EditorGeneralPage.py file | annotate | diff | comparison | revisions
eric6/Preferences/ConfigurationPages/EditorGeneralPage.ui file | annotate | diff | comparison | revisions
eric6/Preferences/__init__.py file | annotate | diff | comparison | revisions
eric6/QScintilla/Editor.py 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/QScintilla/MiniEditor.py file | annotate | diff | comparison | revisions
eric6/UI/BrowserModel.py file | annotate | diff | comparison | revisions
eric6/Utilities/ClassBrowsers/ClbrBaseClasses.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	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6.e4p	Fri Sep 04 18:50:43 2020 +0200
@@ -2103,6 +2103,9 @@
     <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>
@@ -2110,9 +2113,6 @@
     <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/DebugClients/Python/PyProfile.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/DebugClients/Python/PyProfile.py	Fri Sep 04 18:50:43 2020 +0200
@@ -10,7 +10,7 @@
 import marshal
 import profile
 import atexit
-import pickle
+import pickle           # secok
 
 
 class PyProfile(profile.Profile):
--- a/eric6/Preferences/ConfigurationPages/EditorGeneralPage.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/Preferences/ConfigurationPages/EditorGeneralPage.py	Fri Sep 04 18:50:43 2020 +0200
@@ -55,6 +55,11 @@
         self.comment0CheckBox.setChecked(
             Preferences.getEditor("CommentColumn0"))
         
+        self.sourceOutlineGroupBox.setChecked(
+            Preferences.getEditor("ShowSourceOutline"))
+        self.sourceOutlineWidthSpinBox.setValue(
+            Preferences.getEditor("SourceOutlineWidth"))
+        
         virtualSpaceOptions = Preferences.getEditor("VirtualSpaceOptions")
         self.vsSelectionCheckBox.setChecked(
             virtualSpaceOptions & QsciScintillaBase.SCVS_RECTANGULARSELECTION)
@@ -89,6 +94,13 @@
             "CommentColumn0",
             self.comment0CheckBox.isChecked())
         
+        Preferences.setEditor(
+            "ShowSourceOutline",
+            self.sourceOutlineGroupBox.isChecked())
+        Preferences.setEditor(
+            "SourceOutlineWidth",
+            self.sourceOutlineWidthSpinBox.value())
+        
         virtualSpaceOptions = QsciScintillaBase.SCVS_NONE
         if self.vsSelectionCheckBox.isChecked():
             virtualSpaceOptions |= QsciScintillaBase.SCVS_RECTANGULARSELECTION
--- a/eric6/Preferences/ConfigurationPages/EditorGeneralPage.ui	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/Preferences/ConfigurationPages/EditorGeneralPage.ui	Fri Sep 04 18:50:43 2020 +0200
@@ -170,7 +170,7 @@
           </column>
           <column>
            <property name="text">
-            <string notr="true"> </string>
+            <string notr="true"/>
            </property>
           </column>
          </widget>
@@ -276,6 +276,60 @@
     </widget>
    </item>
    <item>
+    <widget class="QGroupBox" name="sourceOutlineGroupBox">
+     <property name="toolTip">
+      <string>Select to enable the source code outline view</string>
+     </property>
+     <property name="title">
+      <string>Source Code Outline</string>
+     </property>
+     <property name="checkable">
+      <bool>true</bool>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout_2">
+      <item>
+       <widget class="QLabel" name="label_2">
+        <property name="text">
+         <string>Default Width:</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QSpinBox" name="sourceOutlineWidthSpinBox">
+        <property name="toolTip">
+         <string>Enter the default width of the source code outline view</string>
+        </property>
+        <property name="alignment">
+         <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+        </property>
+        <property name="minimum">
+         <number>50</number>
+        </property>
+        <property name="maximum">
+         <number>498</number>
+        </property>
+        <property name="singleStep">
+         <number>50</number>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>397</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
     <widget class="QGroupBox" name="groupBox">
      <property name="title">
       <string>Comments</string>
@@ -362,6 +416,8 @@
   <tabstop>tabforindentationCheckBox</tabstop>
   <tabstop>tabindentsCheckBox</tabstop>
   <tabstop>converttabsCheckBox</tabstop>
+  <tabstop>sourceOutlineGroupBox</tabstop>
+  <tabstop>sourceOutlineWidthSpinBox</tabstop>
   <tabstop>comment0CheckBox</tabstop>
   <tabstop>vsSelectionCheckBox</tabstop>
   <tabstop>vsUserCheckBox</tabstop>
--- a/eric6/Preferences/__init__.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/Preferences/__init__.py	Fri Sep 04 18:50:43 2020 +0200
@@ -494,6 +494,9 @@
         "ShowMarkerCoverage": True,
         "ShowMarkerSearch": True,
         
+        "ShowSourceOutline": True,
+        "SourceOutlineWidth": 200,
+        
         # All (most) lexers
         "AllFoldCompact": True,
         
@@ -2061,7 +2064,7 @@
                  "OnlineSyntaxCheckInterval", "OnlineChangeTraceInterval",
                  "WrapLongLinesMode", "WrapVisualFlag", "WrapIndentMode",
                  "WrapStartIndent", "CallTipsPosition", "VirtualSpaceOptions",
-                 "PreviewRefreshWaitTimer"]:
+                 "PreviewRefreshWaitTimer", "SourceOutlineWidth"]:
         return int(prefClass.settings.value(
             "Editor/" + key, prefClass.editorDefaults[key]))
     elif key in ["AdditionalOpenFilters", "AdditionalSaveFilters",
--- a/eric6/QScintilla/Editor.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/QScintilla/Editor.py	Fri Sep 04 18:50:43 2020 +0200
@@ -1833,14 +1833,19 @@
             lang = self.lexer_.name()
             if normalized:
                 # adjust some Pygments lexer names
-                if lang == "Python 3":
+                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 3"
+                    lang = "Python"
+                elif lang == "Protocol":
+                    lang = "Protocol Buffer"
         return lang
     
     def getApiLanguage(self):
--- a/eric6/QScintilla/EditorAssembly.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/QScintilla/EditorAssembly.py	Fri Sep 04 18:50:43 2020 +0200
@@ -12,7 +12,10 @@
 from PyQt5.QtCore import QTimer
 from PyQt5.QtWidgets import QWidget, QGridLayout, QComboBox
 
+from E5Gui.E5Application import e5App
+
 import UI.PixmapCache
+import Preferences
 
 
 class EditorAssembly(QWidget):
@@ -26,13 +29,18 @@
         Constructor
         
         @param dbs reference to the debug server object
-        @param fn name of the file to be opened (string). If it is None,
-            a new (empty) editor is opened
+        @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
-            (ViewManager.ViewManager)
-        @param filetype type of the source file (string)
+        @type ViewManager.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
         """
         super(EditorAssembly, self).__init__()
         
@@ -44,13 +52,16 @@
         from .Editor import Editor
         from .EditorOutline import EditorOutlineView
         
+        self.__showOutline = Preferences.getEditor("ShowSourceOutline")
+        
         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.__sourceOutline = EditorOutlineView(
+            self.__editor, populate=self.__showOutline)
+        self.__sourceOutline.setMaximumWidth(
+            Preferences.getEditor("SourceOutlineWidth"))
         
         self.__layout.addWidget(self.__buttonsWidget, 1, 0, -1, 1)
         self.__layout.addWidget(self.__globalsCombo, 0, 1)
@@ -58,25 +69,14 @@
         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)
         
         self.__module = None
         
-        self.__globalsCombo.activated[int].connect(self.__globalsActivated)
-        self.__membersCombo.activated[int].connect(self.__membersActivated)
-        self.__editor.cursorLineChanged.connect(self.__editorCursorLineChanged)
-        
         self.__shutdownTimerCalled = False
         self.__parseTimer = QTimer(self)
         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)
         
@@ -85,7 +85,11 @@
         self.__globalsBoundaries = {}
         self.__membersBoundaries = {}
         
-        QTimer.singleShot(0, self.__parseEditor)
+        self.__activateOutline(self.__showOutline)
+        self.__activateCombos(not self.__showOutline)
+        
+        e5App().getObject("UserInterface").preferencesChanged.connect(
+            self.__preferencesChanged)
     
     def shutdownTimer(self):
         """
@@ -93,7 +97,6 @@
         """
         self.__parseTimer.stop()
         if not self.__shutdownTimerCalled:
-            self.__parseTimer.timeout.disconnect(self.__parseEditor)
             self.__editor.textChanged.disconnect(self.__resetParseTimer)
             self.__editor.refreshed.disconnect(self.__resetParseTimer)
             self.__shutdownTimerCalled = True
@@ -102,18 +105,74 @@
         """
         Public method to get the reference to the editor widget.
         
-        @return reference to the editor widget (Editor)
+        @return reference to the editor widget
+        @rtype Editor
         """
         return self.__editor
     
+    def __preferencesChanged(self):
+        """
+        Private slot handling a change of preferences.
+        """
+        showOutline = Preferences.getEditor("ShowSourceOutline")
+        if showOutline != self.__showOutline:
+            self.__showOutline = showOutline
+            self.__activateOutline(self.__showOutline)
+            self.__activateCombos(not self.__showOutline)
+    
+    #######################################################################
+    ## Methods dealing with the navigation combos below
+    #######################################################################
+    
+    def __activateCombos(self, activate):
+        """
+        Private slot to activate the navigation combo boxes.
+        
+        @param activate flag indicating to activate the combo boxes
+        @type bool
+        """
+        self.__globalsCombo.setVisible(activate)
+        self.__membersCombo.setVisible(activate)
+        if activate:
+            self.__globalsCombo.activated[int].connect(
+                self.__globalsActivated)
+            self.__membersCombo.activated[int].connect(
+                self.__membersActivated)
+            self.__editor.cursorLineChanged.connect(
+                self.__editorCursorLineChanged)
+            self.__parseTimer.timeout.connect(self.__parseEditor)
+            
+            self.__parseEditor()
+            
+            line, _ = self.__editor.getCursorPosition()
+            self.__editorCursorLineChanged(line)
+        else:
+            try:
+                self.__globalsCombo.activated[int].disconnect(
+                    self.__globalsActivated)
+                self.__membersCombo.activated[int].disconnect(
+                    self.__membersActivated)
+                self.__editor.cursorLineChanged.disconnect(
+                    self.__editorCursorLineChanged)
+                self.__parseTimer.timeout.disconnect(self.__parseEditor)
+            except TypeError:
+                # signals were not connected
+                pass
+            
+            self.__globalsCombo.clear()
+            self.__membersCombo.clear()
+            self.__globalsBoundaries = {}
+            self.__membersBoundaries = {}
+    
     def __globalsActivated(self, index, moveCursor=True):
         """
         Private method to jump to the line of the selected global entry and to
         populate the members combo box.
         
-        @param index index of the selected entry (integer)
-        @keyparam moveCursor flag indicating to move the editor cursor
-            (boolean)
+        @param index index of the selected entry
+        @type int
+        @param moveCursor flag indicating to move the editor cursor
+        @type bool
         """
         # step 1: go to the line of the selected entry
         lineno = self.__globalsCombo.itemData(index)
@@ -206,9 +265,10 @@
         """
         Private method to jump to the line of the selected members entry.
         
-        @param index index of the selected entry (integer)
-        @keyparam moveCursor flag indicating to move the editor cursor
-            (boolean)
+        @param index index of the selected entry
+        @type int
+        @param moveCursor flag indicating to move the editor cursor
+        @type bool
         """
         lineno = self.__membersCombo.itemData(index)
         if lineno is not None and moveCursor:
@@ -333,7 +393,8 @@
         """
         Private slot handling a line change of the cursor of the editor.
         
-        @param lineno line number of the cursor (integer)
+        @param lineno line number of the cursor
+        @type int
         """
         lineno += 1     # cursor position is zero based, code info one based
         
@@ -358,3 +419,47 @@
                 break
         self.__membersCombo.setCurrentIndex(indexFound)
         self.__membersActivated(indexFound, moveCursor=False)
+    
+    #######################################################################
+    ## Methods dealing with the source outline below
+    #######################################################################
+    
+    def __activateOutline(self, activate):
+        """
+        Private slot to activate the source outline view.
+        
+        @param activate flag indicating to activate the source outline view
+        @type bool
+        """
+        self.__sourceOutline.setActive(activate)
+        
+        if activate:
+            self.__sourceOutline.setVisible(
+                self.__sourceOutline.isSupportedLanguage(
+                    self.__editor.getLanguage()
+                )
+            )
+            
+            self.__parseTimer.timeout.connect(self.__sourceOutline.repopulate)
+            self.__editor.languageChanged.connect(self.__editorChanged)
+            self.__editor.editorRenamed.connect(self.__editorChanged)
+        else:
+            self.__sourceOutline.hide()
+            
+            try:
+                self.__parseTimer.timeout.disconnect(
+                    self.__sourceOutline.repopulate)
+                self.__editor.languageChanged.disconnect(self.__editorChanged)
+                self.__editor.editorRenamed.disconnect(self.__editorChanged)
+            except TypeError:
+                # signals were not connected
+                pass
+    
+    def __editorChanged(self):
+        """
+        Private slot handling changes of the editor language or file name.
+        """
+        supported = self.__sourceOutline.isSupportedLanguage(
+            self.__editor.getLanguage())
+        
+        self.__sourceOutline.setVisible(supported)
--- a/eric6/QScintilla/EditorOutline.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/QScintilla/EditorOutline.py	Fri Sep 04 18:50:43 2020 +0200
@@ -7,7 +7,7 @@
 Module implementing an outline widget for source code navigation of the editor.
 """
 
-from PyQt5.QtCore import Qt, QCoreApplication, QItemSelectionModel
+from PyQt5.QtCore import pyqtSlot, Qt, QCoreApplication
 from PyQt5.QtWidgets import QTreeView, QAbstractItemView, QMenu, QApplication
 
 from UI.BrowserSortFilterProxyModel import BrowserSortFilterProxyModel
@@ -19,27 +19,27 @@
 from .EditorOutlineModel import EditorOutlineModel
 
 
-# TODO: handle editor signal 'cursorLineChanged'
-# TODO: handle editor signal 'languageChanged'
-# TODO: handle editor signal 'refreshed'
-# TODO: handle editor signal 'editorRenamed'
 class EditorOutlineView(QTreeView):
     """
     Class implementing an outline widget for source code navigation of the
     editor.
     """
-    def __init__(self, editor, parent=None):
+    WidthIncrement = 50
+    
+    def __init__(self, editor, populate=True, parent=None):
         """
         Constructor
         
         @param editor reference to the editor widget
         @type Editor
+        @param populate flag indicating to populate the outline
+        @type bool
         @param parent reference to the parent widget
         @type QWidget
         """
         super(EditorOutlineView, self).__init__(parent)
         
-        self.__model = EditorOutlineModel(editor)
+        self.__model = EditorOutlineModel(editor, populate=populate)
         self.__sortModel = BrowserSortFilterProxyModel()
         self.__sortModel.setSourceModel(self.__model)
         self.setModel(self.__sortModel)
@@ -70,6 +70,36 @@
         
         self.__expandedNames = []
         self.__currentItemName = ""
+        self.__signalsConnected = False
+    
+    def setActive(self, active):
+        """
+        Public method to activate or deactivate the outline view.
+        
+        @param active flag indicating the requested action
+        @type bool
+        """
+        if active and not self.__signalsConnected:
+            editor = self.__model.editor()
+            editor.refreshed.connect(self.repopulate)
+            editor.languageChanged.connect(self.__editorLanguageChanged)
+            editor.editorRenamed.connect(self.__editorRenamed)
+            editor.cursorLineChanged.connect(self.__editorCursorLineChanged)
+            
+            self.__model.repopulate()
+            self.__resizeColumns()
+            
+            line, _ = editor.getCursorPosition()
+            self.__editorCursorLineChanged(line)
+        
+        elif not active and self.__signalsConnected:
+            editor = self.__model.editor()
+            editor.refreshed.disconnect(self.repopulate)
+            editor.languageChanged.disconnect(self.__editorLanguageChanged)
+            editor.editorRenamed.disconnect(self.__editorRenamed)
+            editor.cursorLineChanged.disconnect(self.__editorCursorLineChanged)
+            
+            self.__model.clear()
     
     def __resizeColumns(self):
         """
@@ -124,6 +154,7 @@
             if name in self.__expandedNames:
                 self.setExpanded(childIndex, True)
             childIndex = self.indexBelow(childIndex)
+        self.__resizeColumns()
         
         self.__expandedNames = []
         self.__currentItemName = ""
@@ -205,6 +236,15 @@
             QCoreApplication.translate(
                 'EditorOutlineView', 'Copy Path to Clipboard'),
             self.__copyToClipboard)
+        self.__menu.addSeparator()
+        self.__menu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Increment Width'),
+            self.__incWidth)
+        self.__menu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Decrement Width'),
+            self.__decWidth)
         
         # create the attribute/import menu
         self.__gotoMenu = QMenu(
@@ -224,6 +264,15 @@
             QCoreApplication.translate(
                 'EditorOutlineView', 'Copy Path to Clipboard'),
             self.__copyToClipboard)
+        self.__attributeMenu.addSeparator()
+        self.__attributeMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Increment Width'),
+            self.__incWidth)
+        self.__attributeMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Decrement Width'),
+            self.__decWidth)
         
         # create the background menu
         self.__backMenu = QMenu(self)
@@ -235,6 +284,15 @@
             QCoreApplication.translate(
                 'EditorOutlineView', 'Copy Path to Clipboard'),
             self.__copyToClipboard)
+        self.__backMenu.addSeparator()
+        self.__backMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Increment Width'),
+            self.__incWidth)
+        self.__backMenu.addAction(
+            QCoreApplication.translate(
+                'EditorOutlineView', 'Decrement Width'),
+            self.__decWidth)
     
     def __contextMenuRequested(self, coord):
         """
@@ -243,15 +301,11 @@
         @param coord position of the mouse pointer
         @type QPoint
         """
+        index = self.indexAt(coord)
         coord = self.mapToGlobal(coord)
-        index = self.indexAt(coord)
         
         if index.isValid():
             self.setCurrentIndex(index)
-            flags = QItemSelectionModel.SelectionFlags(
-                QItemSelectionModel.ClearAndSelect |
-                QItemSelectionModel.Rows)
-            self.selectionModel().select(index, flags)
             
             itm = self.model().item(index)
             if isinstance(
@@ -312,3 +366,55 @@
         if fn:
             cb = QApplication.clipboard()
             cb.setText(fn)
+    
+    def __incWidth(self):
+        """
+        Private method to increment the width of the outline.
+        """
+        self.setMaximumWidth(
+            self.maximumWidth() + EditorOutlineView.WidthIncrement)
+        self.updateGeometry()
+    
+    def __decWidth(self):
+        """
+        Private method to decrement the width of the outline.
+        """
+        self.setMaximumWidth(
+            self.maximumWidth() - EditorOutlineView.WidthIncrement)
+        self.updateGeometry()
+    
+    #######################################################################
+    ## Methods handling editor signals below
+    #######################################################################
+    
+    @pyqtSlot()
+    def __editorLanguageChanged(self):
+        """
+        Private slot handling a change of the associated editors source code
+        language.
+        """
+        self.__model.repopulate()
+        self.__resizeColumns()
+    
+    @pyqtSlot()
+    def __editorRenamed(self):
+        """
+        Private slot handling a renaming of the associated editor.
+        """
+        self.__model.repopulate()
+        self.__resizeColumns()
+    
+    @pyqtSlot(int)
+    def __editorCursorLineChanged(self, lineno):
+        """
+        Private method to highlight a node given its line number.
+        
+        @param lineno zero based line number of the item
+        @type int
+        """
+        sindex = self.__model.itemIndexByLine(lineno + 1)
+        if sindex.isValid():
+            index = self.model().mapFromSource(sindex)
+            if index.isValid():
+                self.setCurrentIndex(index)
+                self.scrollTo(index)
--- a/eric6/QScintilla/EditorOutlineModel.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/QScintilla/EditorOutlineModel.py	Fri Sep 04 18:50:43 2020 +0200
@@ -13,7 +13,8 @@
 
 from UI.BrowserModel import (
     BrowserModel, BrowserItem, BrowserClassItem, BrowserCodingItem,
-    BrowserGlobalsItem, BrowserImportsItem, BrowserImportItem
+    BrowserGlobalsItem, BrowserImportsItem, BrowserImportItem,
+    BrowserClassAttributesItem, BrowserMethodItem
 )
 
 
@@ -25,12 +26,14 @@
         "IDL", "JavaScript", "Protocol", "Python3", "MicroPython", "Ruby",
     )
     
-    def __init__(self, editor):
+    def __init__(self, editor, populate=True):
         """
         Constructor
         
         @param editor reference to the editor containing the source text
         @type Editor
+        @param populate flag indicating to populate the outline
+        @type bool
         """
         super(EditorOutlineModel, self).__init__(nopopulate=True)
         
@@ -41,7 +44,8 @@
         rootData = QCoreApplication.translate("EditorOutlineModel", "Name")
         self.rootItem = BrowserItem(None, rootData)
         
-        self.__populateModel()
+        if populate:
+            self.__populateModel()
     
     def __populateModel(self, repopulate=False):
         """
@@ -55,10 +59,30 @@
         
         language = self.__editor.getLanguage()
         if language in EditorOutlineModel.SupportedLanguages:
-            if language in ("Python3", "MicroPython"):
+            if language == "IDL":
+                from Utilities.ClassBrowsers import idlclbr
+                dictionary = idlclbr.scan(
+                    self.__editor.text(), self.__filename, self.__module)
+                idlclbr._modules.clear()
+            elif language == "ProtoBuf":
+                from Utilities.ClassBrowsers import protoclbr
+                dictionary = protoclbr.scan(
+                    self.__editor.text(), self.__filename, self.__module)
+                protoclbr._modules.clear()
+            elif language == "Ruby":
+                from Utilities.ClassBrowsers import rbclbr
+                dictionary = rbclbr.scan(
+                    self.__editor.text(), self.__filename, self.__module)
+                rbclbr._modules.clear()
+            elif language == "JavaScript":
+                from Utilities.ClassBrowsers import jsclbr
+                dictionary = jsclbr.scan(
+                    self.__editor.text(), self.__filename, self.__module)
+                jsclbr._modules.clear()
+            elif language in ("Python3", "MicroPython"):
                 from Utilities.ClassBrowsers import pyclbr
-                dictionary = pyclbr.scan(self.__editor.text(), self.__filename,
-                                         self.__module)
+                dictionary = pyclbr.scan(
+                    self.__editor.text(), self.__filename, self.__module)
                 pyclbr._modules.clear()
             
             keys = list(dictionary.keys())
@@ -87,7 +111,8 @@
                         parentItem,
                         QCoreApplication.translate(
                             "EditorOutlineModel", "Coding: {0}")
-                        .format(dictionary["@@Coding@@"].coding))
+                        .format(dictionary["@@Coding@@"].coding),
+                        dictionary["@@Coding@@"].linenumber)
                     self._addItem(node, parentItem)
                 if "@@Globals@@" in keys:
                     node = BrowserGlobalsItem(
@@ -163,3 +188,79 @@
         @rtype str
         """
         return self.__filename
+    
+    def itemIndexByLine(self, lineno):
+        """
+        Public method to find an item's index given a line number.
+        
+        @param lineno one based line number of the item
+        @type int
+        @return index of the item found
+        @rtype QModelIndex
+        """
+        def findItem(lineno, parent):
+            """
+            Function to iteratively search for an item containing the given
+            line.
+            
+            @param lineno one based line number of the item
+            @type int
+            @param parent reference to the parent item
+            @type BrowserItem
+            @return found item or None
+            @rtype BrowserItem
+            """
+            if not parent.isPopulated():
+                if parent.isLazyPopulated():
+                    self.populateItem(parent)
+                else:
+                    return None
+            for child in parent.children():
+                if isinstance(child, BrowserClassAttributesItem):
+                    itm = findItem(lineno, child)
+                    if itm is not None:
+                        return itm
+                elif isinstance(child, (BrowserClassItem, BrowserMethodItem)):
+                    start, end = child.boundaries()
+                    if end == -1:
+                        end = 1000000   # assume end of file
+                    if start <= lineno <= end:
+                        itm = findItem(lineno, child)
+                        if itm is not None:
+                            return itm
+                        else:
+                            return child
+                elif hasattr(child, "linenos"):
+                    if lineno in child.linenos():
+                        return child
+                elif hasattr(child, "lineno"):
+                    if lineno == child.lineno():
+                        return child
+            else:
+                return None
+        
+        if self.__populated:
+            for rootChild in self.rootItem.children():
+                itm = None
+                if isinstance(rootChild, BrowserClassItem):
+                    start, end = rootChild.boundaries()
+                    if end == -1:
+                        end = 1000000   # assume end of file
+                    if start <= lineno <= end:
+                        itm = findItem(lineno, rootChild)
+                        if itm is None:
+                            itm = rootChild
+                elif isinstance(rootChild,
+                                (BrowserImportsItem, BrowserGlobalsItem)):
+                    itm = findItem(lineno, rootChild)
+                elif (
+                    isinstance(rootChild, BrowserCodingItem) and
+                    lineno == rootChild.lineno()
+                ):
+                    itm = rootChild
+                if itm is not None:
+                    return self.createIndex(itm.row(), 0, itm)
+            else:
+                return QModelIndex()
+        
+        return QModelIndex()
--- a/eric6/QScintilla/MiniEditor.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/QScintilla/MiniEditor.py	Fri Sep 04 18:50:43 2020 +0200
@@ -18,7 +18,7 @@
 from PyQt5.QtGui import QCursor, QKeySequence, QPalette, QFont
 from PyQt5.QtWidgets import (
     QWidget, QWhatsThis, QActionGroup, QDialog, QInputDialog, QApplication,
-    QMenu, QVBoxLayout, QLabel
+    QMenu, QVBoxLayout, QHBoxLayout, QLabel
 )
 from PyQt5.QtPrintSupport import QPrinter, QPrintDialog, QAbstractPrintDialog
 from PyQt5.Qsci import QsciScintilla
@@ -114,8 +114,20 @@
     Class implementing a minimalistic editor for simple editing tasks.
     
     @signal editorSaved() emitted after the file has been saved
+    @signal languageChanged(str) emitted when the editors language was set. The
+        language is passed as a parameter.
+    @signal editorRenamed(str) emitted after the editor got a new name
+        (i.e. after a 'Save As')
+    @signal cursorLineChanged(int) emitted when the cursor line was changed
+    
+    @signal refreshed() dummy signal to emulate the Editor interface
     """
     editorSaved = pyqtSignal()
+    languageChanged = pyqtSignal(str)
+    editorRenamed = pyqtSignal(str)
+    cursorLineChanged = pyqtSignal(int)
+    
+    refreshed = pyqtSignal()
     
     def __init__(self, filename="", filetype="", parent=None, name=None):
         """
@@ -139,6 +151,12 @@
         self.__textEdit.setSearchIndicator = self.setSearchIndicator
         self.__textEdit.setUtf8(True)
         
+        self.getCursorPosition = self.__textEdit.getCursorPosition
+        self.text = self.__textEdit.text
+        
+        self.__curFile = filename
+        self.__lastLine = 0
+        
         self.srHistory = {
             "search": [],
             "replace": []
@@ -147,10 +165,21 @@
         self.__searchWidget = SearchReplaceWidget(False, self, self)
         self.__replaceWidget = SearchReplaceWidget(True, self, self)
         
+        from .EditorOutline import EditorOutlineView
+        self.__sourceOutline = EditorOutlineView(self, populate=False)
+        self.__sourceOutline.setMaximumWidth(
+            Preferences.getEditor("SourceOutlineWidth"))
+        
+        hlayout = QHBoxLayout()
+        hlayout.setContentsMargins(0, 0, 0, 0)
+        hlayout.setSpacing(1)
+        hlayout.addWidget(self.__textEdit)
+        hlayout.addWidget(self.__sourceOutline)
+        
         centralWidget = QWidget()
         layout = QVBoxLayout()
         layout.setContentsMargins(1, 1, 1, 1)
-        layout.addWidget(self.__textEdit)
+        layout.addLayout(hlayout)
         layout.addWidget(self.__searchWidget)
         layout.addWidget(self.__replaceWidget)
         centralWidget.setLayout(layout)
@@ -161,7 +190,6 @@
         self.lexer_ = None
         self.apiLanguage = ""
         self.filetype = ""
-        self.__curFile = filename
         
         self.__loadEditorConfig(filename)
         
@@ -189,6 +217,11 @@
         self.__markOccurrencesTimer.timeout.connect(self.__markOccurrences)
         self.__markedText = ""
         
+        self.__changeTimer = QTimer(self)
+        self.__changeTimer.setSingleShot(True)
+        self.__changeTimer.setInterval(5 * 1000)
+        self.__textEdit.textChanged.connect(self.__resetChangeTimer)
+        
         self.__textEdit.textChanged.connect(self.__documentWasModified)
         self.__textEdit.modificationChanged.connect(self.__modificationChanged)
         self.__textEdit.cursorPositionChanged.connect(
@@ -211,6 +244,16 @@
             self.encoding = self.__getEditorConfig("DefaultEncoding")
         
         self.__checkActions()
+        
+        self.__sourceOutline.setActive(True)
+        self.__sourceOutline.setVisible(
+            self.__sourceOutline.isSupportedLanguage(
+                self.getLanguage()
+            )
+        )
+        self.__changeTimer.timeout.connect(self.__sourceOutline.repopulate)
+        self.languageChanged.connect(self.__editorChanged)
+        self.editorRenamed.connect(self.__editorChanged)
 
     def closeEvent(self, event):
         """
@@ -265,7 +308,11 @@
         if not fileName:
             return False
         
-        return self.__saveFile(fileName)
+        result = self.__saveFile(fileName)
+        
+        self.editorRenamed.emit(fileName)
+        
+        return result
     
     def __saveCopy(self):
         """
@@ -2531,6 +2578,10 @@
         if Preferences.getEditor("MarkOccurrencesEnabled"):
             self.__markOccurrencesTimer.stop()
             self.__markOccurrencesTimer.start()
+        
+        if self.__lastLine != line:
+            self.cursorLineChanged.emit(line)
+            self.__lastLine = line
     
     def __undo(self):
         """
@@ -2899,7 +2950,8 @@
         """
         if self.apiLanguage.startswith("Pygments|"):
             self.pygmentsSelAct.setText(
-                self.tr("Alternatives ({0})").format(self.getLanguage()))
+                self.tr("Alternatives ({0})").format(
+                    self.getLanguage(normalized=False)))
         else:
             self.pygmentsSelAct.setText(self.tr("Alternatives"))
     
@@ -2907,12 +2959,14 @@
         """
         Private method to select a specific pygments lexer.
         
-        @return name of the selected pygments lexer (string)
+        @return name of the selected pygments lexer
+        @rtype str
         """
         from pygments.lexers import get_all_lexers
         lexerList = sorted(lex[0] for lex in get_all_lexers())
         try:
-            lexerSel = lexerList.index(self.getLanguage())
+            lexerSel = lexerList.index(
+                self.getLanguage(normalized=False, forPygments=True))
         except ValueError:
             lexerSel = 0
         lexerName, ok = QInputDialog.getItem(
@@ -2968,6 +3022,8 @@
             self.__textEdit.setPaper(
                 Preferences.getEditorColour("EditAreaBackground"))
         
+        self.languageChanged.emit(self.apiLanguage)
+        
     def setLanguage(self, filename, initTextDisplay=True, pyname=""):
         """
         Public method to set a lexer language.
@@ -2986,20 +3042,43 @@
         if initTextDisplay:
             self.__setTextDisplay()
             self.__setMargins()
+        
+        self.languageChanged.emit(self.apiLanguage)
 
-    def getLanguage(self):
+    def getLanguage(self, normalized=True, forPygments=False):
         """
         Public method to retrieve the language of the editor.
         
-        @return language of the editor (string)
+        @param normalized flag indicating to normalize some Pygments
+            lexer names
+        @type bool
+        @param forPygments flag indicating to normalize some lexer
+            names for Pygments
+        @type bool
+        @return language of the editor
+        @rtype str
         """
         if (
             self.apiLanguage == "Guessed" or
             self.apiLanguage.startswith("Pygments|")
         ):
-            return self.lexer_.name()
+            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:
-            return self.apiLanguage
+            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 __checkLanguage(self):
         """
@@ -3055,7 +3134,16 @@
         if pyname:
             self.apiLanguage = "Pygments|{0}".format(pyname)
         else:
-            self.apiLanguage = self.lexer_.language()
+            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.__textEdit.setLexer(self.lexer_)
         if self.lexer_.lexer() == "container" or self.lexer_.lexer() is None:
             self.__textEdit.SCN_STYLENEEDED.connect(self.__styleNeeded)
@@ -3495,3 +3583,23 @@
                     return overrides[self.filetype][1]
         
         return None
+    
+    #######################################################################
+    ## Methods supporting the outline view below
+    #######################################################################
+    
+    def __resetChangeTimer(self):
+        """
+        Private slot to reset the parse timer.
+        """
+        self.__changeTimer.stop()
+        self.__changeTimer.start()
+    
+    def __editorChanged(self):
+        """
+        Private slot handling changes of the editor language or file name.
+        """
+        supported = self.__sourceOutline.isSupportedLanguage(
+            self.getLanguage())
+        
+        self.__sourceOutline.setVisible(supported)
--- a/eric6/UI/BrowserModel.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/UI/BrowserModel.py	Fri Sep 04 18:50:43 2020 +0200
@@ -199,7 +199,7 @@
         childItem = index.internalPointer()
         parentItem = childItem.parent()
         
-        if parentItem == self.rootItem:
+        if parentItem is None or parentItem == self.rootItem:
             return QModelIndex()
         
         return self.createIndex(parentItem.row(), 0, parentItem)
@@ -657,7 +657,8 @@
                 node = BrowserCodingItem(
                     parentItem,
                     QCoreApplication.translate("BrowserModel", "Coding: {0}")
-                    .format(dictionary["@@Coding@@"].coding))
+                    .format(dictionary["@@Coding@@"].coding),
+                    dictionary["@@Coding@@"].linenumber)
                 self._addItem(node, parentItem)
             if "@@Globals@@" in keys:
                 node = BrowserGlobalsItem(
@@ -693,7 +694,8 @@
             if repopulate:
                 self.endInsertRows()
         parentItem._populated = True
-        if (parentItem.type_ == BrowserItemFile and
+        if (
+            parentItem.type_ == BrowserItemFile and
             fileName not in self.watchedFileItems
         ):
             # watch the file only in the file browser not the project viewer
@@ -755,21 +757,6 @@
         for name in list(cl.methods.keys()):
             keys.append((name, 'm'))
         
-        if len(keys) > 0:
-            if repopulate:
-                self.beginInsertRows(
-                    self.createIndex(parentItem.row(), 0, parentItem),
-                    0, len(keys) - 1)
-            for key, kind in keys:
-                if kind == 'c':
-                    node = BrowserClassItem(parentItem, cl.classes[key], file_)
-                elif kind == 'm':
-                    node = BrowserMethodItem(parentItem, cl.methods[key],
-                                             file_)
-                self._addItem(node, parentItem)
-            if repopulate:
-                self.endInsertRows()
-        
         if len(cl.attributes):
             node = BrowserClassAttributesItem(
                 parentItem, cl.attributes,
@@ -790,6 +777,21 @@
                     node, self.createIndex(parentItem.row(), 0, parentItem))
             else:
                 self._addItem(node, parentItem)
+        
+        if len(keys) > 0:
+            if repopulate:
+                self.beginInsertRows(
+                    self.createIndex(parentItem.row(), 0, parentItem),
+                    0, len(keys) - 1)
+            for key, kind in keys:
+                if kind == 'c':
+                    node = BrowserClassItem(parentItem, cl.classes[key], file_)
+                elif kind == 'm':
+                    node = BrowserMethodItem(parentItem, cl.methods[key],
+                                             file_)
+                self._addItem(node, parentItem)
+            if repopulate:
+                self.endInsertRows()
 
     def populateMethodItem(self, parentItem, repopulate=False):
         """
@@ -1862,17 +1864,32 @@
     """
     Class implementing the data structure for browser coding items.
     """
-    def __init__(self, parent, text):
+    def __init__(self, parent, text, linenumber):
         """
         Constructor
         
         @param parent parent item
-        @param text text to be shown by this item (string)
+        @type BrowserItem
+        @param text text to be shown by this item
+        @type str
+        @param linenumber line number of the coding line
+        @type int
         """
         BrowserItem.__init__(self, parent, text)
         
         self.type_ = BrowserItemCoding
         self.icon = UI.PixmapCache.getIcon("textencoding")
+        
+        self.__lineno = linenumber
+    
+    def lineno(self):
+        """
+        Public method returning the line number of the coding line.
+        
+        @return line number defining the coding line
+        @rtype int
+        """
+        return self.__lineno
     
     def lessThan(self, other, column, order):
         """
--- a/eric6/Utilities/ClassBrowsers/ClbrBaseClasses.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/Utilities/ClassBrowsers/ClbrBaseClasses.py	Fri Sep 04 18:50:43 2020 +0200
@@ -335,6 +335,7 @@
         """
         ClbrBase.__init__(self, module, "Coding", file, lineno)
         self.coding = coding
+        self.linenumber = lineno
 
 
 class Enum(ClbrBase):
--- a/eric6/Utilities/ClassBrowsers/idlclbr.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/Utilities/ClassBrowsers/idlclbr.py	Fri Sep 04 18:50:43 2020 +0200
@@ -199,7 +199,6 @@
         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
@@ -214,9 +213,6 @@
     """
     global _modules
     
-    dictionary = {}
-    dict_counts = {}
-
     if module in _modules:
         # we've seen this file before...
         return _modules[module]
@@ -229,21 +225,42 @@
         f.close()
     if type not in SUPPORTED_TYPES:
         # not CORBA IDL source, can't do anything with this module
-        _modules[module] = dictionary
-        return dictionary
+        _modules[module] = {}
+        return {}
 
-    _modules[module] = dictionary
-    classstack = []  # stack of (class, indent) pairs
-    indent = 0
     try:
         src = Utilities.readEncodedFile(file)[0]
     except (UnicodeError, IOError):
         # can't do anything with this module
-        _modules[module] = dictionary
-        return dictionary
+        _modules[module] = {}
+        return {}
+    
+    _modules[module] = scan(src, file, module)
+    return _modules[module]
+
+
+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
+    indent = 0
+    
     lineno, last_lineno_pos = 1, 0
     lastGlobalEntry = None
     cur_obj = None
--- a/eric6/Utilities/ClassBrowsers/jsclbr.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/Utilities/ClassBrowsers/jsclbr.py	Fri Sep 04 18:50:43 2020 +0200
@@ -279,7 +279,6 @@
                                   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.
@@ -290,8 +289,6 @@
     """
     global _modules
     
-    dictionary = {}
-
     if module in _modules:
         # we've seen this file before...
         return _modules[module]
@@ -304,19 +301,38 @@
         f.close()
     if type not in SUPPORTED_TYPES:
         # not CORBA IDL source, can't do anything with this module
-        _modules[module] = dictionary
-        return dictionary
+        _modules[module] = {}
+        return {}
 
-    _modules[module] = dictionary
     try:
         src = Utilities.readEncodedFile(file)[0]
     except (UnicodeError, IOError):
         # can't do anything with this module
-        _modules[module] = dictionary
-        return dictionary
+        _modules[module] = {}
+        return {}
+    
+    _modules[module] = scan(src, file, module)
+    return _modules[module]
+
+
+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 = {}
+
     visitor = Visitor(src, module, file)
     dictionary = visitor.parse()
     _modules[module] = dictionary
--- a/eric6/Utilities/ClassBrowsers/protoclbr.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/Utilities/ClassBrowsers/protoclbr.py	Fri Sep 04 18:50:43 2020 +0200
@@ -190,7 +190,6 @@
         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,
@@ -205,8 +204,6 @@
     """
     global _modules
     
-    dictionary = {}
-
     if module in _modules:
         # we've seen this file before...
         return _modules[module]
@@ -219,21 +216,41 @@
         f.close()
     if type not in SUPPORTED_TYPES:
         # not ProtoBuf protocol source, can't do anything with this module
-        _modules[module] = dictionary
-        return dictionary
+        _modules[module] = {}
+        return {}
 
-    _modules[module] = dictionary
-    classstack = []  # stack of (class, indent) pairs
-    indent = 0
     try:
         src = Utilities.readEncodedFile(file)[0]
     except (UnicodeError, IOError):
         # can't do anything with this module
-        _modules[module] = dictionary
-        return dictionary
+        _modules[module] = {}
+        return {}
+    
+    _modules[module] = scan(src, file, module)
+    return _modules[module]
+
+
+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 = {}
+
+    classstack = []  # stack of (class, indent) pairs
+    indent = 0
+
     lineno, last_lineno_pos = 1, 0
     lastGlobalEntry = None
     cur_obj = None
--- a/eric6/Utilities/ClassBrowsers/pyclbr.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/Utilities/ClassBrowsers/pyclbr.py	Fri Sep 04 18:50:43 2020 +0200
@@ -25,7 +25,11 @@
 
 _getnext = re.compile(
     r"""
-    (?P<String>
+   (?P<CodingLine>
+        ^ \# \s* [*_-]* \s* coding[:=] \s* (?P<Coding> [-\w_.]+ ) \s* [*_-]* $
+    )
+
+|   (?P<String>
         \# .*? $   # ignore everything in comments
     |
         \""" [^"\\]* (?:
@@ -127,10 +131,6 @@
             (?: \( \s* .*? \s* \) )
             |
             (?: [^#;\\\n]* (?: \\\n )* )* )
-    )
-
-|   (?P<CodingLine>
-        ^ \# \s* [*_-]* \s* coding[:=] \s* (?P<Coding> [-\w_.]+ ) \s* [*_-]* $
     )""",
     re.VERBOSE | re.DOTALL | re.MULTILINE).search
 
@@ -335,11 +335,16 @@
     module and return a dictionary with one entry for each class
     found in the module.
     
-    @param module name of the module file (string)
-    @param path path the module should be searched in (list of strings)
+    @param module name of the module file
+    @type str
+    @param path path the module should be searched in
+    @type list of str
     @param inpackage flag indicating a module inside a package is scanned
-    @param isPyFile flag indicating a Python file (boolean)
+    @type bool
+    @param isPyFile flag indicating a Python file
+    @type bool
     @return the resulting dictionary
+    @rtype dict
     """
     global _modules
     
--- a/eric6/Utilities/ClassBrowsers/rbclbr.py	Fri Sep 04 18:48:52 2020 +0200
+++ b/eric6/Utilities/ClassBrowsers/rbclbr.py	Fri Sep 04 18:50:43 2020 +0200
@@ -250,7 +250,6 @@
         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.
@@ -261,9 +260,6 @@
     """
     global _modules
     
-    dictionary = {}
-    dict_counts = {}
-
     if module in _modules:
         # we've seen this file before...
         return _modules[module]
@@ -276,22 +272,43 @@
         f.close()
     if type not in SUPPORTED_TYPES:
         # not Ruby source, can't do anything with this module
-        _modules[module] = dictionary
-        return dictionary
+        _modules[module] = {}
+        return {}
 
-    _modules[module] = dictionary
-    classstack = []  # stack of (class, indent) pairs
-    acstack = []    # stack of (access control, indent) pairs
-    indent = 0
     try:
         src = Utilities.readEncodedFile(file)[0]
     except (UnicodeError, IOError):
         # can't do anything with this module
-        _modules[module] = dictionary
-        return dictionary
+        _modules[module] = {}
+        return {}
+    
+    _modules[module] = scan(src, file, module)
+    return _modules[module]
+
+
+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
+    acstack = []    # stack of (access control, indent) pairs
+    indent = 0
+
     lineno, last_lineno_pos = 1, 0
     cur_obj = None
     lastGlobalEntry = None

eric ide

mercurial