Repository: spyder-ide/spyder

Issue:
Outline explorer only populate its content after a file is changed on latest 4.x
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

### Issue Report Checklist

## Problem Description

After PR #13109, the outline explorer populate its content only after a change is made to a file.

It would be great if the content of the outline explorer could be populated for all files that are open on startup.

![outline_explorer_populated](https://user-images.githubusercontent.com/10170372/94730448-3c9af080-0331-11eb-94bd-28fcf2766a29.gif)


## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 4.2.0.dev0 2b8b7f669
* Python version: 3.7.6 64-bit
* Qt version: 5.12.5
* PyQt5 version: 5.12.3
* Operating System: Windows 10



Discussion:
Thanks @jnsebgosselin for the feedback! @ccordoba12 @andfoy is there something that we can do here?
Yes, we are aware of this bug, I think this is a problem with the LSP requests, which are being sent before the server is fully initialized. I can add a workaround on PR https://github.com/spyder-ide/spyder/pull/13885 as well
@jnsebgosselin, do you have any further comments regarding the new outline explorer improvements?
No, I think everything else is looking good. Thanks for the work you've done here. I'm glad that the OE was finally migrated to use the LSP, I think this is neat and I hope this will be easier to maintain too.
Dwelling more into this subject, I think this issue is more related to PR https://github.com/spyder-ide/spyder/pull/13828, I don't know what @ccordoba12 thinks about it
> Dwelling more into this subject, I think this issue is more related to PR #13828, I don't know what @ccordoba12 thinks about it

Yeah, I think it is. I'll take a look at this problem in that PR.

@andfoy, please don't add a workaround for now (it shouldn't be necessary).
> No, I think everything else is looking good. Thanks for the work you've done here. I'm glad that the OE was finally migrated to use the LSP, I think this is neat and I hope this will be easier to maintain too.

Yes, we think it's going to be much easier to maintain and extend.
@ccordoba12 PR #13938 fixed the problem when **starting** Spyder, thank you very much!

However, the same problem is still present when **switching projects**.

Resolving patch:
diff --git a/spyder/plugins/outlineexplorer/plugin.py b/spyder/plugins/outlineexplorer/plugin.py
--- a/spyder/plugins/outlineexplorer/plugin.py
+++ b/spyder/plugins/outlineexplorer/plugin.py
@@ -130,3 +130,7 @@ def start_symbol_services(self, capabilities, language):
     def stop_symbol_services(self, language):
         """Disable LSP symbols functionality."""
         self.explorer.stop_symbol_services(language)
+
+    def update_all_editors(self):
+        """Update all editors with an associated LSP server."""
+        self.explorer.update_all_editors()
diff --git a/spyder/plugins/outlineexplorer/widgets.py b/spyder/plugins/outlineexplorer/widgets.py
--- a/spyder/plugins/outlineexplorer/widgets.py
+++ b/spyder/plugins/outlineexplorer/widgets.py
@@ -461,7 +461,13 @@ def set_current_editor(self, editor, update):
         """Bind editor instance"""
         editor_id = editor.get_id()
 
-        item = self.editor_items[editor_id].node
+        # Don't fail if editor doesn't exist anymore. This
+        # happens when switching projects.
+        try:
+            item = self.editor_items[editor_id].node
+        except KeyError:
+            return
+
         if not self.freeze:
             self.scrollToItem(item)
             self.root_item_selected(item)
@@ -541,10 +547,21 @@ def update_editors(self, language):
         if self.editors_to_update.get(language):
             editor = self.editors_to_update[language][0]
             if editor.info is not None:
-                self.update_editor(editor.info, editor)
+                # Editor could be not there anymore after switching
+                # projects
+                try:
+                    self.update_editor(editor.info, editor)
+                except KeyError:
+                    pass
                 self.editors_to_update[language].remove(editor)
             self.update_timers[language].start()
 
+    def update_all_editors(self):
+        """Update all editors with LSP support."""
+        for language in self._languages:
+            self.set_editors_to_update(language)
+            self.update_timers[language].start()
+
     @Slot(list)
     def update_editor(self, items, editor=None):
         """
@@ -807,6 +824,14 @@ def clicked(self, item):
             self.root_item_selected(item)
         self.activated(item)
 
+    def set_editors_to_update(self, language):
+        """Set editors to update per language."""
+        to_update = []
+        for editor in self.editor_ids.keys():
+            if editor.get_language().lower() == language:
+                to_update.append(editor)
+        self.editors_to_update[language] = to_update
+
     def start_symbol_services(self, language):
         """Show symbols for all `language` files."""
         # Save all languages that can send info to this pane.
@@ -821,12 +846,8 @@ def start_symbol_services(self, language):
         timer.timeout.connect(lambda: self.update_editors(language))
         self.update_timers[language] = timer
 
-        # Select editors to update per language
-        to_update = []
-        for editor in self.editor_ids.keys():
-            if editor.get_language().lower() == language:
-                to_update.append(editor)
-        self.editors_to_update[language] = to_update
+        # Set editors that need to be updated per language
+        self.set_editors_to_update(language)
 
         # Start timer
         timer.start()
@@ -954,3 +975,7 @@ def start_symbol_services(self, language):
     def stop_symbol_services(self, language):
         """Disable LSP symbols functionality."""
         self.treewidget.stop_symbol_services(language)
+
+    def update_all_editors(self):
+        """Update all editors with an associated LSP server."""
+        self.treewidget.update_all_editors()
diff --git a/spyder/plugins/projects/plugin.py b/spyder/plugins/projects/plugin.py
--- a/spyder/plugins/projects/plugin.py
+++ b/spyder/plugins/projects/plugin.py
@@ -164,6 +164,8 @@ def register_plugin(self):
         self.sig_project_loaded.connect(
             lambda v: self.main.editor.setup_open_files())
         self.sig_project_loaded.connect(self.update_explorer)
+        self.sig_project_loaded.connect(
+            lambda v: self.main.outlineexplorer.update_all_editors())
         self.sig_project_closed[object].connect(
             lambda v: self.main.workingdirectory.chdir(
                 self.get_last_working_dir()))
@@ -174,6 +176,8 @@ def register_plugin(self):
                               update_kind=WorkspaceUpdateKind.DELETION))
         self.sig_project_closed.connect(
             lambda v: self.main.editor.setup_open_files())
+        self.sig_project_closed.connect(
+            lambda v: self.main.outlineexplorer.update_all_editors())
         self.recent_project_menu.aboutToShow.connect(self.setup_menu_actions)
 
         self.main.restore_scrollbar_position.connect(


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -3625,8 +3625,11 @@ def test_tour_message(main_window, qtbot):
 @pytest.mark.use_introspection
 @pytest.mark.preload_project
 @pytest.mark.skipif(os.name == 'nt', reason="Fails on Windows")
-def test_outline_at_startup(main_window, qtbot):
-    """Test that all files in the Outline pane are updated at startup."""
+def test_update_outline(main_window, qtbot, tmpdir):
+    """
+    Test that files in the Outline pane are updated at startup and
+    after switching projects.
+    """
     # Show outline explorer
     outline_explorer = main_window.outlineexplorer
     outline_explorer._toggle_view_action.setChecked(True)
@@ -3669,6 +3672,25 @@ def test_outline_at_startup(main_window, qtbot):
     # Assert spinner is not shown
     assert not outline_explorer.explorer.loading_widget.isSpinning()
 
+    # Set one file as session without projects
+    prev_file = tmpdir.join("foo.py")
+    prev_file.write("def zz(x):\n"
+                    "    return x**2\n")
+    CONF.set('editor', 'filenames', [str(prev_file)])
+
+    # Close project to open that file automatically
+    main_window.projects.close_project()
+
+    # Wait a bit for its tree to be filled
+    qtbot.wait(1000)
+
+    # Assert the editor was filled
+    editor = list(treewidget.editor_ids.keys())[0]
+    assert len(treewidget.editor_tree_cache[editor.get_id()]) > 0
+
+    # Remove test file from session
+    CONF.set('editor', 'filenames', [])
+
 
 if __name__ == "__main__":
     pytest.main()