Repository: spyder-ide/spyder

Issue:
Modified file flag ('*') disappears from tab in the Editor
If we open and edit several files and then perform 'Save as' for **one** of them, 'modified' flag ('*') after the filename will disappear for **all** files, even unsaved ones. It reappears if we switch to the tab with unsaved file.

This does not happen for newly created files and during simple 'Save' procedure.

Spyder 3.0.0dev
Python 2.7



Discussion:
And another scenario and the same strange behavior:

If we create (but not save) several files, modify one of them and do 'Save as' for it, '*' will appear for unmodified files.

@mariacamilaremolinagutierrez, this is a hard one so you don't need to have it ready for next week. In two weeks would be fine :-)


Resolving patch:
diff --git a/spyder/utils/sourcecode.py b/spyder/utils/sourcecode.py
--- a/spyder/utils/sourcecode.py
+++ b/spyder/utils/sourcecode.py
@@ -9,7 +9,12 @@
 """
 
 import re
+import os
+import sys
 
+from spyder.py3compat import PY2
+if PY2:
+    from itertools import izip as zip
 
 # Order is important:
 EOL_CHARS = (("\r\n", 'nt'), ("\n", 'posix'), ("\r", 'mac'))
@@ -120,12 +125,102 @@ def get_identifiers(source_code):
     valid = re.compile(r'[a-zA-Z_]')
     return [token for token in tokens if re.match(valid, token)]
 
+def path_components(path):
+    """
+    Return the individual components of a given file path
+    string (for the local operating system).
 
-if __name__ == '__main__':
-    code = 'import functools\nfunctools.partial'
-    assert get_primary_at(code, len(code)) == 'functools.partial'
-    assert set(get_identifiers(code)) == set(['import', 'functools',
-                                              'functools.partial'])
-    assert split_source(code) == ['import functools', 'functools.partial']
-    code = code.replace('\n', '\r\n')
-    assert split_source(code) == ['import functools', 'functools.partial']
+    Taken from http://stackoverflow.com/q/21498939/438386
+    """
+    components = []
+    # The loop guarantees that the returned components can be
+    # os.path.joined with the path separator and point to the same
+    # location:    
+    while True:
+        (new_path, tail) = os.path.split(path)  # Works on any platform
+        components.append(tail)        
+        if new_path == path:  # Root (including drive, on Windows) reached
+            break
+        path = new_path
+    components.append(new_path)    
+    components.reverse()  # First component first 
+    return components
+
+def differentiate_prefix(path_components0, path_components1):
+    """
+    Return the differentiated prefix of the given two iterables. 
+     
+    Taken from http://stackoverflow.com/q/21498939/438386
+    """
+    longest_prefix = []
+    root_comparison = False
+    common_elmt = None
+    for index, (elmt0, elmt1) in enumerate(zip(path_components0, path_components1)):
+        if elmt0 != elmt1:
+            if index == 2:
+                root_comparison = True
+            break
+        else:
+            common_elmt = elmt0
+        longest_prefix.append(elmt0)
+    file_name_length = len(path_components0[len(path_components0) - 1])
+    path_0 = os.path.join(*path_components0)[:-file_name_length - 1]
+    if len(longest_prefix) > 0:
+        longest_path_prefix = os.path.join(*longest_prefix)
+        longest_prefix_length = len(longest_path_prefix) + 1
+        if path_0[longest_prefix_length:] != '' and not root_comparison:
+            path_0_components = path_components(path_0[longest_prefix_length:])
+            if path_0_components[0] == ''and path_0_components[1] == ''and len(
+                                        path_0[longest_prefix_length:]) > 20:
+                path_0_components.insert(2, common_elmt)
+                path_0 = os.path.join(*path_0_components)
+            else:
+                path_0 = path_0[longest_prefix_length:]
+        elif not root_comparison:
+            path_0 = common_elmt
+        elif sys.platform.startswith('linux') and path_0 == '':
+            path_0 = '/'
+    return path_0
+
+def get_file_title(files_path_list, filename):
+    """Get tab title without ambiguation."""
+    fname = os.path.basename(filename)
+    same_name_files = get_same_name_files(files_path_list, fname)
+    if len(same_name_files) > 1:
+        compare_path = shortest_path(same_name_files)
+        if compare_path == filename:
+            same_name_files.remove(path_components(filename))
+            compare_path = shortest_path(same_name_files)    
+        diff_path = differentiate_prefix(path_components(filename),
+                                             path_components(compare_path))
+        diff_path_length = len(diff_path)
+        path_component = path_components(diff_path)
+        if (diff_path_length > 20 and len(path_component) > 2):
+            if path_component[0] != '/' and path_component[0] != '':
+                path_component = [path_component[0], '...', 
+                                          path_component[-1]]
+            else:
+                path_component = [path_component[2], '...', 
+                                          path_component[-1]]
+            diff_path = os.path.join(*path_component)
+        fname = fname + " - " + diff_path    
+    return fname
+
+def get_same_name_files(files_path_list, filename):
+    """Get a list of the path components of the files with the same name."""
+    same_name_files = []
+    for fname in files_path_list:
+        if filename == os.path.basename(fname):
+            same_name_files.append(path_components(fname))
+    return same_name_files
+    
+def shortest_path(files_path_list):
+    """Shortest path between files in the list."""
+    if len(files_path_list) > 0:
+        shortest_path = files_path_list[0]
+        shortest_path_length = len(files_path_list[0])
+        for path_elmts in files_path_list:
+            if len(path_elmts) < shortest_path_length:
+                shortest_path_length = len(path_elmts)
+                shortest_path = path_elmts
+        return os.path.join(*shortest_path)
diff --git a/spyder/widgets/editor.py b/spyder/widgets/editor.py
--- a/spyder/widgets/editor.py
+++ b/spyder/widgets/editor.py
@@ -966,9 +966,12 @@ def __modified_readonly_title(self, title, is_modified, is_readonly):
             title = "(%s)" % title
         return title
 
-    def get_tab_text(self, filename, is_modified=None, is_readonly=None):
-        """Return tab title"""
-        return self.__modified_readonly_title(osp.basename(filename),
+    def get_tab_text(self, index, is_modified=None, is_readonly=None):
+        """Return tab title."""
+        files_path_list = [finfo.filename for finfo in self.data]
+        fname = self.data[index].filename
+        fname = sourcecode.get_file_title(files_path_list, fname)
+        return self.__modified_readonly_title(fname,
                                               is_modified, is_readonly)
 
     def get_tab_tip(self, filename, is_modified=None, is_readonly=None):
@@ -1003,8 +1006,8 @@ def add_to_data(self, finfo, set_current):
         self.data.append(finfo)
         self.data.sort(key=self.__get_sorting_func())
         index = self.data.index(finfo)
-        fname, editor = finfo.filename, finfo.editor
-        self.tabs.insertTab(index, editor, self.get_tab_text(fname))
+        editor = finfo.editor
+        self.tabs.insertTab(index, editor, self.get_tab_text(index))
         self.set_stack_title(index, False)
         if set_current:
             self.set_stack_index(index)
@@ -1020,7 +1023,8 @@ def __repopulate_stack(self):
                 is_modified = True
             else:
                 is_modified = None
-            tab_text = self.get_tab_text(finfo.filename, is_modified)
+            index = self.data.index(finfo)
+            tab_text = self.get_tab_text(index, is_modified)
             tab_tip = self.get_tab_tip(finfo.filename)
             index = self.tabs.addTab(finfo.editor, tab_text)
             self.tabs.setTabToolTip(index, tab_tip)
@@ -1054,7 +1058,7 @@ def set_stack_title(self, index, is_modified):
         fname = finfo.filename
         is_modified = (is_modified or finfo.newly_created) and not finfo.default
         is_readonly = finfo.editor.isReadOnly()
-        tab_text = self.get_tab_text(fname, is_modified, is_readonly)
+        tab_text = self.get_tab_text(index, is_modified, is_readonly)
         tab_tip = self.get_tab_tip(fname, is_modified, is_readonly)
         self.tabs.setTabText(index, tab_text)
         self.tabs.setTabToolTip(index, tab_tip)
@@ -1198,7 +1202,7 @@ def close_file(self, index=None, force=False):
         if self.get_stack_count() == 0 and self.create_new_file_if_empty:
             self.sig_new_file[()].emit()
             return False
-
+        self.__modify_stack_title()
         return is_ok
 
     def close_all_files(self):
@@ -1599,6 +1603,11 @@ def __check_file_status(self, index):
         # Finally, resetting temporary flag:
         self.__file_status_flag = False
 
+    def __modify_stack_title(self):
+        for index, finfo in enumerate(self.data):
+            state = finfo.editor.document().isModified()
+            self.set_stack_title(index, state)
+
     def refresh(self, index=None):
         """Refresh tabwidget"""
         if index is None:
@@ -1614,6 +1623,7 @@ def refresh(self, index=None):
             self.__refresh_statusbar(index)
             self.__refresh_readonly(index)
             self.__check_file_status(index)
+            self.__modify_stack_title()
             self.update_plugin_title.emit()
         else:
             editor = None
diff --git a/spyder/widgets/fileswitcher.py b/spyder/widgets/fileswitcher.py
--- a/spyder/widgets/fileswitcher.py
+++ b/spyder/widgets/fileswitcher.py
@@ -296,7 +296,8 @@ def paths(self):
 
     @property
     def filenames(self):
-        return [self.tabs.tabText(index) for index in range(self.tabs.count())]
+        return [os.path.basename(getattr(td, 'filename',
+                                         None)) for td in self.data]
 
     @property
     def current_path(self):


Test patch:
diff --git a/spyder/utils/tests/test_sourcecode.py b/spyder/utils/tests/test_sourcecode.py
new file mode 100644
--- /dev/null
+++ b/spyder/utils/tests/test_sourcecode.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+
+"""Tests for sourcecode.py"""
+
+import os
+import sys
+
+import pytest
+
+from spyder.utils import sourcecode
+
+
+def test_get_primary_at():
+    code = 'import functools\nfunctools.partial'
+    assert sourcecode.get_primary_at(code, len(code)) == 'functools.partial'
+
+
+def test_get_identifiers():
+    code = 'import functools\nfunctools.partial'
+    assert set(sourcecode.get_identifiers(code)) == set(['import', 'functools',
+                                                         'functools.partial'])
+
+
+def test_split_source():
+    code = 'import functools\nfunctools.partial'
+    assert sourcecode.split_source(code) == ['import functools', 'functools.partial']
+    code = code.replace('\n', '\r\n')
+    assert sourcecode.split_source(code) == ['import functools', 'functools.partial']
+
+
+def test_path_components():
+    if sys.platform.startswith('linux'):
+        path_components0 = ['','','documents','test','test.py']        
+    else:
+        path_components0 = ['c:','','documents','test','test.py']        
+    path0 = os.path.join(*path_components0)
+    assert sourcecode.path_components(path0) == path_components0
+
+
+def test_differentiate_prefix():
+    if sys.platform.startswith('linux'):
+        path_components0 = ['','','documents','test','test.py']
+        path_components1 = ['','','documents','projects','test','test.py']
+    else:
+        path_components0 = ['c:','','documents','test','test.py']
+        path_components1 = ['c:','','documents','projects','test','test.py']
+    diff_path0 = os.path.join(*['test'])
+    diff_path1 = os.path.join(*['projects','test'])
+    assert sourcecode.differentiate_prefix(
+                        path_components0, path_components1) ==  diff_path0
+    assert sourcecode.differentiate_prefix(
+                        path_components1, path_components0) ==  diff_path1
+
+def test_get_same_name_files():
+    files_path_list = []
+    if sys.platform.startswith('linux'):
+        fname0 = os.path.join(*['','','documents','test','test.py'])
+        files_path_list.append(fname0)
+        fname1 = os.path.join(*['','','documents','projects','test','test.py'])
+        files_path_list.append(fname1)
+        same_name_files = [['','','documents','test','test.py'],
+                           ['','','documents','projects','test','test.py']]
+    else:
+        fname0 = os.path.join(*['c:','','documents','test','test.py'])
+        files_path_list.append(fname0)
+        fname1 = os.path.join(*['c:','','documents','projects','test','test.py'])
+        files_path_list.append(fname1)
+        same_name_files = [['c:','','documents','test','test.py'],
+                           ['c:','','documents','projects','test','test.py']]
+        assert sourcecode.get_same_name_files(files_path_list
+                                              ,'test.py') == same_name_files
+
+def test_shortest_path():
+    if sys.platform.startswith('linux'):
+        files_path_list =[['','','documents','test','test.py'],
+                          ['','','documents','projects','test','test.py']]
+        shortest_path = os.path.join(*['','','documents','test','test.py'])
+    else:
+        files_path_list =[['c:','','documents','test','test.py'],
+                          ['c:','','documents','projects','test','test.py']]
+        shortest_path = os.path.join(*['c:','','documents','test','test.py'])
+    assert sourcecode.shortest_path(files_path_list) == shortest_path
+
+def test_get_file_title():
+    files_path_list = []
+    if sys.platform.startswith('linux'):
+        fname0 = os.path.join(*['','','documents','test','test.py'])
+        files_path_list.append(fname0)
+        fname1 = os.path.join(*['','','documents','projects','test','test.py'])
+        files_path_list.append(fname1)
+    else:
+        fname0 = os.path.join(*['c:','','documents','test','test.py'])
+        files_path_list.append(fname0)
+        fname1 = os.path.join(*['c:','','documents','projects','test','test.py'])
+        files_path_list.append(fname1)
+    title0 = 'test.py - ' + os.path.join(*['test'])
+    title1 = 'test.py - ' + os.path.join(*['projects','test'])
+    assert sourcecode.get_file_title(files_path_list, fname0) == title0
+    assert sourcecode.get_file_title(files_path_list, fname1) == title1
+
+if __name__ == '__main__':
+    pytest.main()
+