diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f8627..d243ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,27 @@ File History Changelog ====================== +v1.6.0 (2014-09-19) +------------------- + +- Adapt to changes made to the `FileHistory.sublime-settings` file (#25) +- Don't update the settings file when values are missing, just silently use the + defaults (#25) +- Support `path_exclude_patterns` setting to exclude files from being tracked + in the history (basically a filename pattern blacklist) (#22) +- Support `path_reinclude_patterns` setting to re-include files that were + excluded before (basically a filename pattern whitelist) (#22) +- Deleting from quickpanel should reopen with an updated list and the next + entry selected (#24) +- Catch exception when loading history file fails (#27) +- Support for storing daily backups of the history file. See `max_backup_count` + setting (#27) +- Bug fix: Cannot open multiple files within the same palette if the file is + opened in a different group (#23) +- Buf fix: Panel didn't show on ST2 +- Some refactoring + + v1.5.2 (2014-07-22) ------------------- @@ -74,12 +95,14 @@ v1.4.1 (2014-01-10) - Updated the version number in messages.json + v1.4.0 (2014-01-10) ------------------- - Fixed some issues in the README and added a settings section - Updated the version number to reflect significance of the added functionality + v1.3.5 (2014-01-09) ------------------- diff --git a/FileHistory.sublime-settings b/FileHistory.sublime-settings index c833e17..6005f82 100644 --- a/FileHistory.sublime-settings +++ b/FileHistory.sublime-settings @@ -1,6 +1,5 @@ { // Path to store the history entries in (relative to the sublime packages path) - // Default value is: "User/FileHistory.json" "history_file": "User/FileHistory.json", // Maximum number of history entries we should keep (older entries truncated) @@ -13,7 +12,8 @@ "use_saved_position": true, // Which position to open a file at when the saved index in no longer valid - // "next", "first", "last" + // + // Options: "next", "first", "last" "new_tab_position": "next", // Should we show a preview of the history entries? @@ -25,25 +25,27 @@ // If a cleanup of the history should be run on startup "cleanup_on_startup": true, - // Should the history be reset on startup + // Should the history be reset on startup? + // // BE CAREFUL, this will DELETE ALL of your history entries "delete_all_on_startup": false, - - // Should a monospace be used in the quick panel? + + // Should a monospace font be used in the quick panel? "monospace_font": false, - + // Should the last accessed timestamp be shown in the quick panel? "display_timestamps": true, - // Format the timestamp should be added to the history entry - "timestamp_format": "%Y-%m-%d @ %H:%M:%S", - // How to display the timestamp: - // "relative": how long since the access, e.g. 2 days, 5 hours, 7 seconds + // "relative": how long since the access, e.g. '2 days, 5 hours' // "absolute": the date and time of the last access - // Please note that the "relative" option can cause a delay in the quick panel popping up + // + // Please note that the "relative" option can cause a delay in the quick panel popping up. "timestamp_display_type": "relative", + // Format of the absolute timestamp that should be added to the history entry + "timestamp_format": "%Y-%m-%d @ %H:%M:%S", + // Which timestamp to display? // "history_access" - last opened/closed timestamp // "filesystem" - the file's last modified timestamp @@ -52,6 +54,26 @@ // Should the history file be nicely formatted? "prettify_history": false, - // Print out the debug text? + // List of path regexs to exclude from the history tracking + // Can be extended in project settings (in a "file_histoy" dict). + // + // Note: You must use forward slashes for the path separator (regardless of platform) + // and escape backslashes properly. + // e.g. ["/temp/", "C:/Program Files/Internet Explorer/", "\\.bak$"] + "path_exclude_patterns": [], + + // List of path regexs that will re-include files that were excluded before + // Can be extended in project settings (in a "file_histoy" dict). + // + // Note: You must use forward slashes for the path separator (regardless of platform) + // and escape backslashes properly. + // e.g. ["/temp/", "C:/Program Files/Internet Explorer/", "\\.my\\.bak$"] + "path_reinclude_patterns": [], + + // The number of daily backups to keep (backup is saved the first time the history is modified) + // To turn off backups, change this setting to 0 (zero). + "max_backup_count": 3, + + // Print out debug text? "debug": false } diff --git a/README.md b/README.md index 50b4cba..3d12fb3 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ # SublimeText - File History # -**Sublime Text 2 and 3** plugin to provide access to the history of accessed files - project-wise or globally. The most recently closed file can be instantly re-opened with a keyboard shortcut or the user can search through the entire file history within the quick panel (including file preview and the ability to open multiple files). +**Sublime Text 2 and 3** plugin to provide access to the history of accessed files - project-wise or globally. The most recently closed file can be instantly re-opened with a keyboard shortcut or the user can search through the entire file history within the quick panel (including file preview and the ability to open multiple files). + +![Example Image][img2] + ## Features ## -Keeps a history of the files that you have accessed in SublimeText (on both a per-project and global level). The most recently closed file can be instantly re-opened with a keyboard shortcut or the user can search through the entire file history in the quick panel. +Keeps a history of the files that you have accessed in SublimeText (on both a per-project and global level). The most recently closed file can be instantly re-opened with a keyboard shortcut or the user can search through the entire file history in the quick panel. Overview of features: -* [Settings][settings] file to customize the functionality. -* When re-opening a file from the history, choose the position to open it in: the ```first``` tab, the ```last``` tab, the ```next``` tab or in the position that it was when it was closed + +* [FileHistory.sublime-settings][] file to customize functionality +* When re-opening a file from the history, choose the position to open it in: the `first` tab, the `last` tab, the `next` tab or in the position that it was when it was closed * Display a preview of the file while looking through the file history in the quick panel (only Sublime Text 3) * Choose target location where the file history should be saved -* Optionally remove any non-existent files while looking through the file history (when previewed or opened) -* Optionally clean up the history on start-up -* Optionally display the quick panel entries with a monospaced font -* Open multiple history entries from the quick panel with the ```right``` key - -Originally obtained from a [gist][gist] by Josh Bjornson. +* Optionally remove any non-existent files while looking through the file history (when previewed or opened) or on start-up +* Open multiple history entries from the quick panel with the Right key +* Delete history entries from the quick panel with Ctrl+Del +* Path exclude and re-include patterns (regex) that can be extended in project settings ## Installation ## @@ -32,7 +34,7 @@ When you opened a panel you can use the right key to open the file an For default keymap definitions, see [Default.sublime-keymap][keymap] ([OSX][keymap-osx]). -For the available and default settings, see [Settings](#settings). +For the available and default settings, see [FileHistory.sublime-settings][]. ### Images ### @@ -42,6 +44,28 @@ For the available and default settings, see [Settings](#settings). *The popup for the global history with text* ![example1][img2] +### Project Settings ### + +You can **extend** the `path_exclude_patterns` and `path_reinclude_patterns` lists in your project settings. + +For this, add a `"file_history"` dictionary to your project's settings and then one or both of the settings to that. Example: + +```json +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "file_history": { + "path_exclude_patterns": ["/bin/"], + "path_reinclude_patterns": ["\\.compiled$"] + } + } +} +``` + ### Commands ### **open_recently_closed_file** (Window) @@ -66,55 +90,14 @@ Checks the current project or the whole history for non-existent files and remov Removes all file history data. -### Customization via settings file ### - -**Important**: At the moment you need to restart Sublime Text after editing the [settings][settings] file (because the settings are cached by the Sublime Text API). - -The following functionality can be customized in the [settings][settings] file: - -* `history_file` - Path to store the history entries in (relative to the sublime packages path) - - default value is `"User/FileHistory.json"` -* `global_max_entries` - Maximum number of history entries we should keep (older entries truncated) - - default value is `100` -* `project_max_entries` - Maximum number of history entries we should keep (older entries truncated) - - default value is `50` -* `use_saved_position` - If we should try to use the saved position of the file - - default value is `true` -* `new_tab_position` - Which position to open a file at when the saved index is no longer valid (or `use_saved_position` is set to `false`) - - default value is `"next"` - - available options are `"next"`, `"first"` and `"last"` -* `show_file_preview` - Should we show a preview of the history entries? - - default value is `true` *(not available on ST2)* -* `remove_non_existent_files_on_preview` - Remove any non-existent files from the history (when previewed or opened) - - default value is `false` -* `cleanup_on_startup` - Remove any non-existent files on startup - - default value is `true` -* `monospace_font` - Should a monospace be used in the quick panel? - - default value is `false` -* `timestamp_show` - Should the last access's timestamp be shown in the quick panel? - - default value is `true` -* `timestamp_relative` - Show a relative time value instead of absolute - - default value is `true` -* `timestamp_format` - The format of the timestamp - - default value is `%Y-%m-%d @ %H:%M:%S` -* `timestamp_mode` - Which timestamp to display? ("history_access" - last opened/closed timestamp, "filesystem" - the file's last modified timestamp) - - default value is `filesystem` -* `prettify_history` - Should the file history be saved as nicely formatted json? - - default value is `false` -* `debug` - Print out the debug text to the console? - - default value is `false` - - -[gist]: https://gist.github.com/1133602 + [github]: https://github.com/FichteFoll/sublimetext-filehistory "Github.com: FichteFoll/sublime-filehistory" -[zipball]: https://github.com/FichteFoll/sublimetext-filehistory/zipball/master [pck-ctrl]: http://wbond.net/sublime_packages/package_control "Sublime Package Control by wbond" -[settings]: FileHistory.sublime-settings "FileHistory.sublime-settings" +[FileHistory.sublime-settings]: FileHistory.sublime-settings [keymap]: Default.sublime-keymap "Default.sublime-keymap" [keymap-osx]: Default%20%28OSX%29.sublime-keymap "Default (OSX).sublime-keymap" [img1]: http://i.imgur.com/B5ViHHv.png [img2]: http://i.imgur.com/y40CEFo.png - diff --git a/file_history.py b/file_history.py index e299ee4..0d36908 100644 --- a/file_history.py +++ b/file_history.py @@ -1,23 +1,30 @@ -import sublime -import sublime_plugin import os import hashlib import json import time import datetime +import re +import shutil +import glob +from textwrap import dedent -is_ST2 = int(sublime.version()) < 3000 +import sublime +import sublime_plugin +is_ST2 = int(sublime.version()) < 3000 -def plugin_loaded(): - # Force the FileHistory singleton to be instantiated so the startup tasks will be executed - # Depending on the "cleanup_on_startup" setting, the history may be cleaned at startup - FileHistory.instance() +invoke_async = sublime.set_timeout if is_ST2 else sublime.set_timeout_async class FileHistory(object): _instance = None + SETTINGS_CALLBACK_KEY = 'FileHistory-reload' + PRINT_DEBUG = False + SETTINGS_FILE = 'FileHistory.sublime-settings' + INDENT_SIZE = 2 + DEFAULT_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S' + @classmethod def instance(cls): """Basic singleton implementation""" @@ -27,62 +34,64 @@ def instance(cls): def __init__(self): """Class to manage the file-access history""" - self.SETTINGS_FILE = 'FileHistory.sublime-settings' - self.PRINT_DEBUG = False self.__load_settings() self.__load_history() self.__clear_context() - self.invoke_async = sublime.set_timeout if is_ST2 else sublime.set_timeout_async - if self.DELETE_ALL_ON_STARTUP: - self.invoke_async(lambda: self.delete_all_history(), 0) + invoke_async(lambda: self.delete_all_history(), 0) elif self.CLEANUP_ON_STARTUP: - self.invoke_async(lambda: self.clean_history(False), 0) - + invoke_async(lambda: self.clean_history(False), 0) def __load_settings(self): - default_date_format = '%Y-%m-%d %H:%M:%S' - """Load the plugin settings from FileHistory.sublime-settings""" - app_settings = sublime.load_settings(self.SETTINGS_FILE) - settings_exist = app_settings.has('history_file') - - # TODO these settings may change during execution but are not re-fetched when that happens - # We either need to set this as a `settings.set_on_change` callback (will be called for any - # modification) or wrap settings differently. - - self.PRINT_DEBUG = self.__ensure_setting(app_settings, 'debug', False) - self.GLOBAL_MAX_ENTRIES = self.__ensure_setting(app_settings, 'global_max_entries', 100) - self.PROJECT_MAX_ENTRIES = self.__ensure_setting(app_settings, 'project_max_entries', 50) - self.USE_SAVED_POSITION = self.__ensure_setting(app_settings, 'use_saved_position', True) - self.NEW_TAB_POSITION = self.__ensure_setting(app_settings, 'new_tab_position', 'next') - self.REMOVE_NON_EXISTENT_FILES = self.__ensure_setting(app_settings, 'remove_non_existent_files_on_preview', True) - self.CLEANUP_ON_STARTUP = self.__ensure_setting(app_settings, 'cleanup_on_startup', True) - self.DELETE_ALL_ON_STARTUP = self.__ensure_setting(app_settings, 'delete_all_on_startup', False) - history_path = self.__ensure_setting(app_settings, 'history_file', os.path.join('User', 'FileHistory.json')) + + self.app_settings = sublime.load_settings(self.SETTINGS_FILE) + self.__refresh_settings() + + # The settings may change during execution so we need to listen for changes + self.app_settings.add_on_change(self.SETTINGS_CALLBACK_KEY, self.__refresh_settings) + + def __refresh_settings(self): + print('[FileHistory] Reloading the settings file "%s".' % (self.SETTINGS_FILE)) + + self.PRINT_DEBUG = self.__ensure_setting('debug', False) + + self.GLOBAL_MAX_ENTRIES = self.__ensure_setting('global_max_entries', 100) + self.PROJECT_MAX_ENTRIES = self.__ensure_setting('project_max_entries', 50) + self.USE_SAVED_POSITION = self.__ensure_setting('use_saved_position', True) + self.NEW_TAB_POSITION = self.__ensure_setting('new_tab_position', 'next') + + self.REMOVE_NON_EXISTENT_FILES = self.__ensure_setting('remove_non_existent_files_on_preview', True) + self.CLEANUP_ON_STARTUP = self.__ensure_setting('cleanup_on_startup', True) + self.DELETE_ALL_ON_STARTUP = self.__ensure_setting('delete_all_on_startup', False) + history_path = self.__ensure_setting('history_file', os.path.join('User', 'FileHistory.json')) + self.HISTORY_FILE = os.path.normpath(os.path.join(sublime.packages_path(), history_path)) - self.USE_MONOSPACE = self.__ensure_setting(app_settings, 'monospace_font', False) - self.TIMESTAMP_SHOW = self.__ensure_setting(app_settings, 'timestamp_show', True) - self.TIMESTAMP_FORMAT = self.__ensure_setting(app_settings, 'timestamp_format', default_date_format) - self.TIMESTAMP_MODE = self.__ensure_setting(app_settings, 'timestamp_mode', 'history_access') - self.TIMESTAMP_RELATIVE = self.__ensure_setting(app_settings, 'timestamp_relative', True) - self.PRETTIFY_HISTORY = self.__ensure_setting(app_settings, 'prettify_history', False) - self.INDENT_SIZE = 4 + + self.USE_MONOSPACE = self.__ensure_setting('monospace_font', False) + + self.TIMESTAMP_SHOW = self.__ensure_setting('timestamp_show', True) + self.TIMESTAMP_FORMAT = self.__ensure_setting('timestamp_format', self.DEFAULT_TIMESTAMP_FORMAT) + self.TIMESTAMP_MODE = self.__ensure_setting('timestamp_mode', 'history_access') + self.TIMESTAMP_RELATIVE = self.__ensure_setting('timestamp_relative', True) + + self.PRETTIFY_HISTORY = self.__ensure_setting('prettify_history', False) + + self.PATH_EXCLUDE_PATTERNS = self.__ensure_setting('path_exclude_patterns', []) + self.PATH_REINCLUDE_PATTERNS = self.__ensure_setting('path_reinclude_patterns', []) + + self.MAX_BACKUP_COUNT = self.__ensure_setting('max_backup_count', 3) # Test if the specified format string is valid try: time.strftime(self.TIMESTAMP_FORMAT) except ValueError: print('[FileHistory] Invalid timstamp_format string. Falling back to default.') - self.TIMESTAMP_FORMAT = default_date_format + self.TIMESTAMP_FORMAT = self.DEFAULT_TIMESTAMP_FORMAT # Ignore the file preview setting for ST2 - self.SHOW_FILE_PREVIEW = False if is_ST2 else self.__ensure_setting(app_settings, 'show_file_preview', True) - - if not settings_exist: - print('[FileHistory] Unable to find the settings file "%s". A default settings file has been created for you.' % (self.SETTINGS_FILE)) - sublime.save_settings(self.SETTINGS_FILE) + self.SHOW_FILE_PREVIEW = False if is_ST2 else self.__ensure_setting('show_file_preview', True) def get_timestamp(self, filename=None): if filename and os.path.exists(filename): @@ -101,17 +110,14 @@ def get_history_timestamp(self, history_entry): timestamp = self.get_timestamp(filepath) return (action, timestamp) - def __ensure_setting(self, settings, key, default_value): + def __ensure_setting(self, key, default_value): value = default_value - if settings.has(key): - value = settings.get(key) - self.debug('FileHistory setting "%s" = "%s"' % (key, value)) + if self.app_settings.has(key): + value = self.app_settings.get(key) + self.debug('Setting "%s" = "%s"' % (key, value)) else: - self.debug('FileHistory setting "%s" not found. Using the default value of "%s"' % (key, default_value)) - # TOCHECK I am not sure we should do this. It makes modifying default behaviour a pain because we force - # all users onto a custom configuration. Furthermore, I don't have documentation comments in the user - # file because it's rewritten all the time. - settings.set(key, default_value) + # no need to persist this setting - just use the default + self.debug('Setting "%s" not found. Using the default value of "%s"' % (key, default_value)) return value def debug(self, text): @@ -145,12 +151,23 @@ def get_current_project_key(self): def __load_history(self): self.history = {} - self.debug('Loading the history from file ' + self.HISTORY_FILE) if not os.path.exists(self.HISTORY_FILE): + self.debug("History file '%s' doesn't exist" % self.HISTORY_FILE) return - with open(self.HISTORY_FILE, 'r') as f: - updated_history = json.load(f) + self.debug('Loading the history from file ' + self.HISTORY_FILE) + try: + with open(self.HISTORY_FILE, 'r') as f: + updated_history = json.load(f) + except Exception as e: + updated_history = {} + sublime.error_message( + dedent("""\ + File History could not read your history file at '%s'. + + %s: %s""") + % (self.HISTORY_FILE, e.__class__.__name__, e) + ) self.history = updated_history @@ -162,6 +179,28 @@ def __save_history(self): json.dump(self.history, f, indent=history_indentation) f.flush() + invoke_async(lambda: self.__manage_backups(), 0) + + def __manage_backups(self): + # Only keep backups if the user wants them + if self.MAX_BACKUP_COUNT <= 0: + return + + # Make sure there is a backup of the history for today + (root, ext) = os.path.splitext(self.HISTORY_FILE) + datestamp = time.strftime('%Y%m%d') + backup = '%s_%s%s' % (root, datestamp, ext) + if not os.path.exists(backup): + self.debug('Backing up the history file for %s' % datestamp) + shutil.copy(self.HISTORY_FILE, backup) + + # Limit the number of backup files to keep + listing = sorted(glob.glob('%s_*%s' % (root, ext)), reverse=True) + if len(listing) > self.MAX_BACKUP_COUNT: + for discard_file in listing[self.MAX_BACKUP_COUNT:]: + self.debug('Discarding old backup %s' % discard_file) + os.remove(discard_file) + def delete_all_history(self): self.history = {} self.__save_history() @@ -169,6 +208,7 @@ def delete_all_history(self): def get_history(self, current_project_only=True): """Return the requested history (global or project-specific): closed files followed by opened files""" # Make sure the history is loaded + # TODO: If we have loaded history previously we should cache it and not access the file system again if len(self.history) == 0: self.__load_history() @@ -193,6 +233,29 @@ def __ensure_project(self, project_name): self.history[project_name]['opened'] = [] self.history[project_name]['closed'] = [] + def is_suppressed(self, view, filename): + override_settings = view.settings().get("file_history", dict()) + exclude_patterns = self.PATH_EXCLUDE_PATTERNS + override_settings.get("path_exclude_patterns", []) + reinclude_patterns = self.PATH_REINCLUDE_PATTERNS + override_settings.get("path_reinclude_patterns", []) + + # Force forward slashes in the filename + filename = os.path.normpath(filename).replace("\\", "/") + + # Search the filename for the pattern and suppress it if it matches + for exclude in exclude_patterns: + if re.search(exclude, filename): + self.debug('[X] Exclusion pattern "%s" blocks history tracking for filename "%s"' + % (exclude, filename)) + # See if none of out reinclude patterns nulifies the exclude + for reinclude in reinclude_patterns: + if re.search(reinclude, filename): + self.debug('[O] Inclusion pattern "%s" re-includes history tracking for filename "%s"' + % (reinclude, filename)) + return False + return True + + return False + def add_view(self, window, view, history_type): # No point adding a transient view to the history if self.is_transient_view(window, view): @@ -202,7 +265,13 @@ def add_view(self, window, view, history_type): filename = view.file_name() if filename is not None: project_name = self.get_current_project_key() - if os.path.exists(filename): + + if self.is_suppressed(view, filename): + # If filename matches 'path_exclude_patterns' then abort the history tracking + # and remove any references to this file from the history + self.__remove(project_name, filename) + self.__remove('global', filename) + elif os.path.exists(filename): # Add to both the project-specific and global histories (group, index) = sublime.active_window().get_view_index(view) self.__add_to_history(project_name, history_type, filename, group, index) @@ -303,6 +372,7 @@ def __clear_context(self): self.current_view = None self.current_history_entry = None + self.current_selected_index = -1 self.project_name = None @@ -342,8 +412,10 @@ def __calculate_view_index(self, window, history_entry): index = 0 return (group, index) - def preview_history(self, window, history_entry): + def preview_history(self, window, selected_index, history_entry): """Preview the file if it exists, otherwise show the previous view (aka the "calling_view")""" + # Save the selected index for a potential reopen when an entry is deleted + self.current_selected_index = selected_index self.current_history_entry = history_entry # track the view even if we won't be previewing it (to support quick-open and remove from history quick keys) @@ -355,8 +427,8 @@ def preview_history(self, window, history_entry): filepath = history_entry['filename'] if os.path.exists(filepath): - # asyncronously open the preview (improves percieved performance) - self.invoke_async(lambda: self.__open_preview(window, filepath), 0) + # asynchronously open the preview (improves perceived performance) + invoke_async(lambda: self.__open_preview(window, filepath), 0) else: # Close the last preview and remove the non-existent file from the history self.__close_preview(window) @@ -382,7 +454,7 @@ def quick_open_preview(self, window): self.__track_calling_view(window) def delete_current_entry(self): - """Delete the history entry for the file that is currently being previewed""" + """Delete the history entry for the file that is currently being previewed""" if not self.current_history_entry: return @@ -403,7 +475,7 @@ def open_history(self, window, history_entry): self.debug('Opened file in group %s, index %s (based on saved group %s, index %s): %s' % (group, index, history_entry['group'], history_entry['index'], history_entry['filename'])) # Add the file we just opened to the history and clear the context - self.add_view(window, new_view, 'opened') + invoke_async(self.add_view(window, new_view, 'opened'), 0) self.__clear_context() def __close_preview(self, window): @@ -456,15 +528,36 @@ class DeleteFileHistoryEntryCommand(sublime_plugin.WindowCommand): def run(self): FileHistory.instance().delete_current_entry() + # Remember if we are showing the global history or the project-specific history + project_flag = not (FileHistory.instance().project_name == 'global') + + # Deleting an entry from the quick panel should reopen it with the entry removed + # TODO recover filter text? (I don't think it is possible to get the quick-panel filter text from the API) + args = {'current_project_only': project_flag, + 'selected_index': FileHistory.instance().current_selected_index} + sublime.active_window().run_command('hide_overlay') + sublime.active_window().run_command('open_recently_closed_file', args=args) + class OpenRecentlyClosedFileCommand(sublime_plugin.WindowCommand): """class to either open the last closed file or show a quick panel with the recent file history (closed files first)""" __is_active = False + def timestamp_from_string(self, timestamp, current_time): + """try with the user-defined timestamp then try the default timestamp. If neither works, then return 0. + This can happen if the user changes the timestamp format setting after there are already entries in the history file""" + history_time = current_time + for format_string in [FileHistory.instance().TIMESTAMP_FORMAT, FileHistory.instance().DEFAULT_TIMESTAMP_FORMAT]: + try: + history_time = datetime.datetime.strptime(timestamp, format_string) + break + except ValueError: + self.debug('The timestamp "%s" does not match the format "%s"' % (timestamp, format_string)) + return history_time + def approximate_age(self, current_time, timestamp, precision=2): - # loosely based on http://codereview.stackexchange.com/questions/37285/efficient-human-readable-timedelta - diff = current_time - datetime.datetime.strptime(timestamp, FileHistory.instance().TIMESTAMP_FORMAT) + diff = current_time - self.timestamp_from_string(timestamp, current_time) def divide(rem, mod): return rem % mod, int(rem // mod) @@ -473,7 +566,11 @@ def subtract(rem, div): n = int(rem // div) return n, rem - n * div - rem = diff.total_seconds() + if is_ST2: + rem = diff.seconds + diff.days * 24 * 60 * 60 + else: + rem = diff.total_seconds() + seconds, rem = divide(rem, 60) minutes, rem = divide(rem, 60) hours, days = divide(rem, 24) @@ -500,7 +597,7 @@ def subtract(rem, div): return ", ".join(magnitudes) - def run(self, show_quick_panel=True, current_project_only=True, selected_file=None): + def run(self, show_quick_panel=True, current_project_only=True, selected_index=-1): self.history_list = FileHistory.instance().get_history(current_project_only) if show_quick_panel: current_time = datetime.datetime.now() @@ -514,7 +611,9 @@ def run(self, show_quick_panel=True, current_project_only=True, selected_file=No if FileHistory.instance().TIMESTAMP_SHOW: (action, timestamp) = FileHistory.instance().get_history_timestamp(node) - if bool(FileHistory.instance().TIMESTAMP_RELATIVE): + if not os.path.exists(filepath): + stamp = ' file no longer exists' + elif bool(FileHistory.instance().TIMESTAMP_RELATIVE): stamp = ' %s ~%s ago' % (action, self.approximate_age(current_time, timestamp)) else: stamp = ' %s on %s' % (action, timestamp) @@ -528,7 +627,9 @@ def run(self, show_quick_panel=True, current_project_only=True, selected_file=No if is_ST2: self.window.show_quick_panel(display_list, self.open_file, font_flag) else: - self.window.show_quick_panel(display_list, self.open_file, font_flag, on_highlight=self.show_preview) + self.window.show_quick_panel(display_list, self.open_file, font_flag, + on_highlight=self.show_preview, + selected_index=selected_index) else: self.open_file(0) @@ -544,16 +645,35 @@ def is_active(cls): def is_valid(self, selected_index): return selected_index >= 0 and selected_index < len(self.history_list) + def get_view_from_another_group(self, selected_index): + open_view = self.window.find_open_file(self.history_list[selected_index]['filename']) + if open_view: + calling_group = FileHistory.instance().calling_view_index[0] + preview_group = self.window.get_view_index(open_view)[0] + if preview_group != calling_group: + return open_view + return None + def show_preview(self, selected_index): # Note: This function will never be called in ST2 if self.is_valid(selected_index): - FileHistory.instance().preview_history(self.window, self.history_list[selected_index]) + # A bug in SublimeText will cause the quick-panel to unexpectedly close trying to show the preview + # for a file that is already open in a different group, so simply don't display the preview for these files + if self.get_view_from_another_group(selected_index): + pass + else: + FileHistory.instance().preview_history(self.window, selected_index, self.history_list[selected_index]) def open_file(self, selected_index): self.__class__.__is_active = False if self.is_valid(selected_index): - FileHistory.instance().open_history(self.window, self.history_list[selected_index]) + # If the file is open in another group then simply give focus to that view, otherwise open the file + open_view = self.get_view_from_another_group(selected_index) + if open_view: + self.window.focus_view(open_view) + else: + FileHistory.instance().open_history(self.window, self.history_list[selected_index]) else: # The user cancelled the action FileHistory.instance().reset(self.window) @@ -575,3 +695,17 @@ def on_query_context(self, view, key, operator, operand, match_all): return v1 != v2 else: return None + + +def plugin_loaded(): + # Force the FileHistory singleton to be instantiated so the startup tasks will be executed + # Depending on the "cleanup_on_startup" setting, the history may be cleaned at startup + FileHistory.instance() + + +def plugin_unloaded(): + # Unregister our on_change callback + FileHistory.instance().app_settings.clear_on_change(FileHistory.SETTINGS_CALLBACK_KEY) + +# ST2 backwards (and don't call it twice in ST3) +unload_handler = plugin_unloaded if is_ST2 else lambda: None diff --git a/messages.json b/messages.json index 757e39e..e3d9e9d 100644 --- a/messages.json +++ b/messages.json @@ -2,5 +2,6 @@ "install": "messages/install.md", "1.5.0": "messages/1.5.0.md", "1.5.1": "messages/1.5.1.md", - "1.5.2": "messages/1.5.2.md" + "1.5.2": "messages/1.5.2.md", + "1.6.0": "messages/1.6.0.md" } diff --git a/messages/1.6.0.md b/messages/1.6.0.md new file mode 100644 index 0000000..3b002cb --- /dev/null +++ b/messages/1.6.0.md @@ -0,0 +1,19 @@ +v1.6.0 (2014-09-19) +------------------- + +- Adapt to changes made to the `FileHistory.sublime-settings` file (#25) +- Don't update the settings file when values are missing, just silently use the + defaults (#25) +- Support `path_exclude_patterns` setting to exclude files from being tracked + in the history (basically a filename pattern blacklist) (#22) +- Support `path_reinclude_patterns` setting to re-include files that were + excluded before (basically a filename pattern whitelist) (#22) +- Deleting from quickpanel should reopen with an updated list and the next + entry selected (#24) +- Catch exception when loading history file fails (#27) +- Support for storing daily backups of the history file. See `max_backup_count` + setting (#27) +- Bug fix: Cannot open multiple files within the same palette if the file is + opened in a different group (#23) +- Buf fix: Panel didn't show on ST2 +- Some refactoring