Repository: spyder-ide/spyder

Issue:
Outline explorer not synchronized at startup
A small GUI bug.

When starting spyder with several files open, and the "structure panel" switched on. The GUI ends up, just after booting, with first file tab and apparently last parsed structure (from last tab), hence leading to a non synchronization between the 2. Switching twice file tabs resynchronizes everything hopefully.

(current mainstream)


Discussion:
Thanks for reporting. We'll take a look at it in a future release.
I think this bug is occurring in the master branch (and not in the 3.x) because of the reordering of the files that is done in the editor plugin during the setup of the previously opened files in the editor [here](https://github.com/spyder-ide/spyder/blob/master/spyder/plugins/editor.py#L2879). What this does basically is to move the previously focused file in the editor stack to index 0 in the tabbar. From the docstring of the `reorder_filenames` method:

```
"""Take the last session filenames and put the last open on first.

It takes a list of filenames and using the current filename from the 
layout settings, sets the one that had focused last in the position 0. 
It also reorders the current lines for each file (supposing that they 
are in the same order as the filenames) and sets them back in the 
layout settings.
"""
```

I think the bug occurs because the outline explorer is not updated after this reordering, which is done programmatically. I will investigate further to confirm that, but before I do, I would like to understand why we are now moving the previously focused file to index 0 in the first place? I find this an annoying behavior. I would prefer that the order of the files in the tabbar when I open Spyder stays as it was when I last closed it. I always end up moving the file at index 0 back where it was previously and where I want it to be. @ccordoba12, what is the reasoning behind this new "smart" behavior in Spyder 4?

![image](https://user-images.githubusercontent.com/10170372/42039102-998e929e-7aba-11e8-93ed-503f2ce77ffb.png)

> what is the reasoning behind this new "smart" behavior in Spyder 4?

A user suggested that it's annoying to see the focus in the Editor changing continually when Spyder is starting. That's why we keep the focus in the last opened file (I think, or is it the first one? I don't remember) in Spyder 4, while the others are loaded in the background.
> A user suggested that it's annoying to see the focus in the Editor changing continually when Spyder is starting. That's why we keep the focus in the last opened file (I think, or is it the first one? I don't remember) in Spyder 4, while the others are loaded in the background.

I agree, but I think this would be better if we could solve this "file-focus-changing" issue without actually moving the last focused file to index 0. Having Spyder messing up with the order of the files every-time it is started is annoying.

Would you be opened to change this if I can find a better solution (and I would also fix this issue in the same time)?
Sure, I have no problem with that.
Alright, I'll try to take a look at this during this weekend.

Resolving patch:
diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py
--- a/spyder/plugins/editor/plugin.py
+++ b/spyder/plugins/editor/plugin.py
@@ -2063,7 +2063,8 @@ def change_max_recent_files(self):
     @Slot(str, int, str)
     @Slot(str, int, str, object)
     def load(self, filenames=None, goto=None, word='',
-             editorwindow=None, processevents=True, start_column=None):
+             editorwindow=None, processevents=True, start_column=None,
+             set_focus=True, add_where='end'):
         """
         Load a text file
         editorwindow: load in this editorwindow (useful when clicking on
@@ -2155,10 +2156,7 @@ def _convert(fname):
 
         for index, filename in enumerate(filenames):
             # -- Do not open an already opened file
-            if index == 0:  # this is the last file focused in previous session
-                focus = True
-            else:
-                focus = False
+            focus = set_focus and index == 0
             current_editor = self.set_current_filename(filename,
                                                        editorwindow,
                                                        focus=focus)
@@ -2171,7 +2169,8 @@ def _convert(fname):
                 # Creating the editor widget in the first editorstack
                 # (the one that can't be destroyed), then cloning this
                 # editor widget in all other editorstacks:
-                finfo = self.editorstacks[0].load(filename, set_current=False)
+                finfo = self.editorstacks[0].load(
+                    filename, set_current=False, add_where=add_where)
                 finfo.path = self.main.get_spyder_pythonpath()
                 self._clone_file_everywhere(finfo)
                 current_editor = current_es.set_current_filename(filename,
@@ -2934,7 +2933,7 @@ def setup_open_files(self):
         self.set_create_new_file_if_empty(False)
         active_project_path = None
         if self.projects is not None:
-             active_project_path = self.projects.get_active_project_path()
+            active_project_path = self.projects.get_active_project_path()
 
         if active_project_path:
             filenames = self.projects.get_project_filenames()
@@ -2943,10 +2942,27 @@ def setup_open_files(self):
         self.close_all_files()
 
         if filenames and any([osp.isfile(f) for f in filenames]):
-            filenames = self.reorder_filenames(filenames)
             layout = self.get_option('layout_settings', None)
             is_vertical, cfname, clines = layout.get('splitsettings')[0]
-            self.load(filenames, goto=clines)
+            if cfname in filenames:
+                index = filenames.index(cfname)
+                # First we load the last focused file.
+                self.load(filenames[index], goto=clines[index], set_focus=True)
+                # Then we load the files located to the left of the last
+                # focused file in the tabbar, while keeping the focus on
+                # the last focused file.
+                if index > 0:
+                    self.load(filenames[index::-1], goto=clines[index::-1],
+                              set_focus=False, add_where='start')
+                # Finally we load the files located to the right of the last
+                # focused file in the tabbar, while keeping the focus on
+                # the last focused file.
+                if index < (len(filenames) - 1):
+                    self.load(filenames[index+1:], goto=clines[index:],
+                              set_focus=False, add_where='end')
+            else:
+                self.load(filenames, goto=clines)
+            self.get_current_editorstack()._refresh_outlineexplorer()
             if layout is not None:
                 self.editorsplitter.set_layout_settings(layout,
                                                         dont_goto=filenames[0])
@@ -2959,39 +2975,6 @@ def setup_open_files(self):
             self.__load_temp_file()
         self.set_create_new_file_if_empty(True)
 
-    def reorder_filenames(self, filenames):
-        """Take the last session filenames and put the last open on first.
-
-        It takes a list of filenames and using the current filename from the
-        layout settings, sets the one that had focused last in the position 0.
-        It also reorders the current lines for each file (supposing that they
-        are in the same order as the filenames) and sets them back in the
-        layout settings.
-        """
-        layout = self.get_option('layout_settings', None)
-        if layout is None:
-            return filenames
-        splitsettings = layout.get('splitsettings')
-        index_first_file = 0
-        reordered_splitsettings = []
-        for index, (is_vertical, cfname, clines) in enumerate(splitsettings):
-            #the first element of filenames is now the one that last had focus
-            if index == 0:
-                if cfname in filenames:
-                    index_first_file = filenames.index(cfname)
-                    filenames.pop(index_first_file)
-                    filenames.insert(0, cfname)
-                    clines_first_file = clines[index_first_file]
-                    clines.pop(index_first_file)
-                    clines.insert(0, clines_first_file)
-                else:
-                    cfname = filenames[0]
-                    index_first_file = 0
-            reordered_splitsettings.append((is_vertical, cfname, clines))
-        layout['splitsettings'] = reordered_splitsettings
-        self.set_option('layout_settings', layout)
-        return filenames
-
     def save_open_files(self):
         """Save the list of open files"""
         self.set_option('filenames', self.get_open_filenames())
diff --git a/spyder/plugins/editor/widgets/editor.py b/spyder/plugins/editor/widgets/editor.py
--- a/spyder/plugins/editor/widgets/editor.py
+++ b/spyder/plugins/editor/widgets/editor.py
@@ -1295,9 +1295,10 @@ def get_tab_tip(self, filename, is_modified=None, is_readonly=None):
         else:
             return text % (osp.basename(filename), osp.dirname(filename))
 
-    def add_to_data(self, finfo, set_current):
+    def add_to_data(self, finfo, set_current, add_where='end'):
         finfo.editor.oe_proxy = None
-        self.data.append(finfo)
+        index = 0 if add_where == 'start' else len(self.data)
+        self.data.insert(index, finfo)
         index = self.data.index(finfo)
         editor = finfo.editor
         self.tabs.insertTab(index, editor, self.get_tab_text(index))
@@ -2264,7 +2265,7 @@ def revert(self):
         self.reload(index)
 
     def create_new_editor(self, fname, enc, txt, set_current, new=False,
-                          cloned_from=None):
+                          cloned_from=None, add_where='end'):
         """
         Create a new editor instance
         Returns finfo object (instead of editor as in previous releases)
@@ -2281,7 +2282,7 @@ def create_new_editor(self, fname, enc, txt, set_current, new=False,
         finfo = FileInfo(fname, enc, editor, new, self.threadmanager,
                          self.introspector)
 
-        self.add_to_data(finfo, set_current)
+        self.add_to_data(finfo, set_current, add_where)
         finfo.send_to_help.connect(self.send_to_help)
         finfo.analysis_results_changed.connect(
                                   lambda: self.analysis_results_changed.emit())
@@ -2410,7 +2411,7 @@ def new(self, filename, encoding, text, default_content=False,
             finfo.editor.document().setModified(False)
         return finfo
 
-    def load(self, filename, set_current=True):
+    def load(self, filename, set_current=True, add_where='end'):
         """
         Load filename, create an editor instance and return it
         *Warning* This is loading file, creating editor but not executing
@@ -2420,7 +2421,8 @@ def load(self, filename, set_current=True):
         filename = osp.abspath(to_text_string(filename))
         self.starting_long_process.emit(_("Loading %s...") % filename)
         text, enc = encoding.read(filename)
-        finfo = self.create_new_editor(filename, enc, text, set_current)
+        finfo = self.create_new_editor(filename, enc, text, set_current,
+                                       add_where=add_where)
         index = self.data.index(finfo)
         self._refresh_outlineexplorer(index, update=True)
         self.ending_long_process.emit("")


Test patch:
diff --git a/spyder/plugins/editor/tests/test_plugin.py b/spyder/plugins/editor/tests/test_plugin.py
--- a/spyder/plugins/editor/tests/test_plugin.py
+++ b/spyder/plugins/editor/tests/test_plugin.py
@@ -23,6 +23,7 @@
 from spyder.utils.qthelpers import qapplication
 
 
+# ---- Qt Test Fixtures
 @pytest.fixture
 def setup_editor(qtbot, monkeypatch):
     """Set up the Editor plugin."""
@@ -54,6 +55,22 @@ def get_spyder_pythonpath(*args):
     yield editor, qtbot
 
 
+@pytest.fixture(scope="module")
+def python_files(tmpdir_factory):
+    """Create and save some python codes in temporary files."""
+    tmpdir = tmpdir_factory.mktemp("files")
+    tmpdir = osp.normcase(tmpdir.strpath)
+
+    filenames = [osp.join(tmpdir, f) for f in
+                 ('file1.py', 'file2.py', 'file3.py', 'file4.py')]
+    for filename in filenames:
+        with open(filename, 'w') as f:
+            f.write("# -*- coding: utf-8 -*-\n"
+                    "print(Hello World!)\n")
+
+    return filenames, tmpdir
+
+
 def test_basic_initialization(setup_editor):
     """Test Editor plugin initialization."""
     editor, qtbot = setup_editor
@@ -63,36 +80,40 @@ def test_basic_initialization(setup_editor):
 
 
 @pytest.mark.parametrize(
-    'filenames, expected_filenames, splitsettings, expected_splitsettings', [
-        (['/file1.py', '/file2.py', '/file3.py', '/file4.py'],
-         ['/file1.py', '/file2.py', '/file3.py', '/file4.py'],
-         [(False, '/other_file.py', [2, 4, 8, 12])],
-         [(False, '/file1.py', [2, 4, 8, 12])]),
-        (['/file1.py', '/file2.py', '/file3.py', '/file4.py'],
-         ['/file2.py', '/file1.py', '/file3.py', '/file4.py'],
-         [(False, '/file2.py', [2, 4, 8, 12])],
-         [(False, '/file2.py', [4, 2, 8, 12])]),
-        (['/file1.py', '/file2.py', '/file3.py', '/file4.py'],
-         ['/file1.py', '/file2.py', '/file3.py', '/file4.py'],
-         None,
-         None, )
-    ])
-def test_reorder_filenames(setup_editor, expected_filenames, filenames,
-                           splitsettings, expected_splitsettings):
+    'last_focused_filename, expected_current_filename',
+    [('other_file.py', 'file1.py'),
+     ('file1.py', 'file1.py'),
+     ('file2.py', 'file2.py'),
+     ('file4.py', 'file4.py')
+     ])
+def test_setup_open_files(setup_editor, last_focused_filename,
+                          expected_current_filename, python_files):
+    """Test Editor plugin open files setup.
+
+    Test that the file order is preserved during the Editor plugin setup and
+    that the current file correspond to the last focused file.
+    """
     editor, qtbot = setup_editor
-
-    def get_option(*args):
-        if splitsettings is None:
-            return None
-        return {'splitsettings': splitsettings}
-
-    def set_option(name, layout_settings):
-        assert layout_settings['splitsettings'] == expected_splitsettings
-
+    expected_filenames, tmpdir = python_files
+    expected_current_filename = osp.join(tmpdir, expected_current_filename)
+
+    def get_option(option, default=None):
+        splitsettings = [(False,
+                          osp.join(tmpdir, last_focused_filename),
+                          [1, 1, 1, 1])]
+        return {'layout_settings': {'splitsettings': splitsettings},
+                'filenames': expected_filenames,
+                'max_recent_files': 20
+                }.get(option)
     editor.get_option = get_option
-    editor.set_option = set_option
 
-    assert expected_filenames == editor.reorder_filenames(filenames)
+    editor.setup_open_files()
+    current_filename = editor.get_current_editorstack().get_current_filename()
+    current_filename = osp.normcase(current_filename)
+    assert current_filename == expected_current_filename
+    filenames = editor.get_current_editorstack().get_filenames()
+    filenames = [osp.normcase(f) for f in filenames]
+    assert filenames == expected_filenames
 
 
 def test_renamed_tree(setup_editor, mocker):
@@ -142,3 +163,8 @@ def test_no_template(setup_editor):
 
     # Revert template back
     shutil.move(osp.join(osp.dirname(template), 'template.py.old'), template)
+
+
+if __name__ == "__main__":
+    import os
+    pytest.main(['-x', os.path.basename(__file__), '-vv', '-rw'])