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