Files
ChocolateyPackages/EthanBrown.SublimeText2.EditorPackages/tools/PackageCache/TrailingSpaces/trailing_spaces.py

454 lines
16 KiB
Python

'''
Provides both a trailing spaces highlighter and a deletion command.
See README.md for details.
@author: Jean-Denis Vauguet <jd@vauguet.fr>, Oktay Acikalin <ok@ryotic.de>
@license: MIT (http://www.opensource.org/licenses/mit-license.php)
@since: 2011-02-25
'''
import sublime
import sublime_plugin
import difflib
import codecs
DEFAULT_MAX_FILE_SIZE = 1048576
DEFAULT_IS_ENABLED = True
DEFAULT_MODIFIED_LINES_ONLY = False
# Global settings object and flags.
# Flags duplicate some of the (core) JSON settings, in case the settings file has
# been corrupted or is empty (ST2 really dislikes that!)
ts_settings_filename = "trailing_spaces.sublime-settings"
ts_settings = None
trailing_spaces_live_matching = DEFAULT_IS_ENABLED
trim_modified_lines_only = DEFAULT_MODIFIED_LINES_ONLY
startup_queue = []
on_disk = None
# Private: Loads settings and sets whether the plugin (live matching) is enabled.
#
# Returns nothing.
def plugin_loaded():
global ts_settings_filename, ts_settings, trailing_spaces_live_matching
global current_highlighting_scope, trim_modified_lines_only, startup_queue
global DEFAULT_COLOR_SCOPE_NAME, on_disk
ts_settings = sublime.load_settings(ts_settings_filename)
trailing_spaces_live_matching = bool(ts_settings.get("trailing_spaces_enabled",
DEFAULT_IS_ENABLED))
current_highlighting_scope = ts_settings.get("trailing_spaces_highlight_color",
"invalid")
DEFAULT_COLOR_SCOPE_NAME = current_highlighting_scope
trim_modified_lines_only = bool(ts_settings.get("trailing_spaces_modified_lines_only",
DEFAULT_MODIFIED_LINES_ONLY))
if trailing_spaces_live_matching:
for view in startup_queue:
match_trailing_spaces(view)
else:
current_highlighting_scope = ""
if ts_settings.get("trailing_spaces_highlight_color") != current_highlighting_scope:
persist_settings()
# Private: Updates user's settings with in-memory values.
#
# Allows for persistent settings from the menu.
#
# Returns nothing.
def persist_settings():
sublime.save_settings(ts_settings_filename)
# Private: Determine if the view is a "Find results" view.
#
# view - the view, you know
#
# Returns True or False.
def is_find_results(view):
return view.settings().get('syntax') and "Find Results" in view.settings().get('syntax')
# Private: Get the regions matching trailing spaces.
#
# As the core regexp matches lines, the regions are, well, "per lines".
#
# view - the view, you know
#
# Returns both the list of regions which map to trailing spaces and the list of
# regions which are to be highlighted, as a list [matched, highlightable].
def find_trailing_spaces(view):
sel = view.sel()[0]
line = view.line(sel.b)
include_empty_lines = bool(ts_settings.get("trailing_spaces_include_empty_lines",
DEFAULT_IS_ENABLED))
include_current_line = bool(ts_settings.get("trailing_spaces_include_current_line",
DEFAULT_IS_ENABLED))
regexp = ts_settings.get("trailing_spaces_regexp") + "$"
no_empty_lines_regexp = "(?<=\S)%s$" % regexp
offending_lines = view.find_all(regexp if include_empty_lines else no_empty_lines_regexp)
if include_current_line:
return [offending_lines, offending_lines]
else:
current_offender = view.find(regexp if include_empty_lines else no_empty_lines_regexp, line.a)
removal = False if current_offender == None else line.intersects(current_offender)
highlightable = [i for i in offending_lines if i != current_offender] if removal else offending_lines
return [offending_lines, highlightable]
# Private: Find the fraking trailing spaces in the view and flags them as such!
#
# It will refresh highlighted regions as well. Does not execute if the
# document's size exceeds the file_max_size setting, or if the fired in a view
# which is not a legacy document (helper/build views and so on).
#
# view - the view, you know
#
# Returns nothing.
def match_trailing_spaces(view):
if ts_settings is None:
startup_queue.append(view)
return
# Silently pass if file is too big.
if max_size_exceeded(view):
return
if not is_find_results(view):
(matched, highlightable) = find_trailing_spaces(view)
add_trailing_spaces_regions(view, matched)
highlight_trailing_spaces_regions(view, highlightable)
# Private: Checks whether the document is bigger than the max_size setting.
#
# view - the view, you know
#
# Returns True or False.
def max_size_exceeded(view):
return view.size() > ts_settings.get('trailing_spaces_file_max_size',
DEFAULT_MAX_FILE_SIZE)
# Private: Marks specified regions as trailing spaces.
#
# view - the view, you know
# regions - regions qualified as trailing spaces
#
# Returns nothing.
def add_trailing_spaces_regions(view, regions):
view.erase_regions('TrailingSpacesMatchedRegions')
view.add_regions('TrailingSpacesMatchedRegions',
regions,
"",
"",
sublime.HIDE_ON_MINIMAP)
# Private: Highlights specified regions as trailing spaces.
#
# It will use the scope enforced by the state of the toggable highlighting.
#
# view - the view, you know
# regions - regions qualified as trailing spaces
#
# Returns nothing.
def highlight_trailing_spaces_regions(view, regions):
view.erase_regions("TrailingSpacesHighlightedRegions")
view.add_regions('TrailingSpacesHighlightedRegions',
regions,
current_highlighting_scope or "",
"",
sublime.HIDE_ON_MINIMAP)
# Private: Toggles highlighting of all trailing spaces in the view.
#
# It has no effect is the plugin is disabled.
#
# view - the view, you know
#
# Returns True (highlighting was turned on) or False (turned off).
def toggle_highlighting(view):
global current_highlighting_scope
# If the scope is that of an invisible, there is nothing to toggle.
if DEFAULT_COLOR_SCOPE_NAME == "":
return "disabled!"
# If performing live, highlighted trailing regions must be updated
# internally.
if not trailing_spaces_live_matching:
(matched, highlightable) = find_trailing_spaces(view)
highlight_trailing_spaces_regions(view, highlightable)
scope = DEFAULT_COLOR_SCOPE_NAME if current_highlighting_scope == "" else ""
current_highlighting_scope = scope
highlight_trailing_spaces_regions(view, view.get_regions('TrailingSpacesHighlightedRegions'))
return "off" if current_highlighting_scope == "" else "on"
# Clear all the highlighted regions in all views.
#
# FIXME: this is not used! Delete?
#
# window - the window, you know
#
# Returns nothing.
def clear_trailing_spaces_highlight(window):
for view in window.views():
view.erase_regions('TrailingSpacesMatchedRegions')
# Find edited lines since last save, as line numbers, based on diff.
#
# It uses a Differ object to compute the diff between the file as red on the
# disk, and the current buffer (which may differ from the disk's state). See
# http://docs.python.org/2/library/difflib.html for details about diff codes.
#
# It relies on a full diff, so it may be expensive computation for very large
# files (diff generation + looping through all lines).
#
# old - a buffer of lines, as in "old version"
# new - a buffer of lines, as in "new version"
#
# Returns the list of edited line numbers.
def modified_lines_as_numbers(old, new):
d = difflib.Differ()
diffs = d.compare(old, new)
# Pretty Naive Algorithm (tm):
# - split off the "Differ code", to check whether:
# - the line is in either in both files or just b: increment the line number
# - the line is only in b: it qualifies as an edited line!
# Starting from -1 as ST2 is internally 0-based for lines.
lineNum = -1
edited_lines = []
for line in diffs:
code = line[:2]
# those lines with "? " are not real! watch out!
if code in (" ", "+ "):
lineNum += 1
if code == "+ ":
edited_lines.append(lineNum)
return False if not edited_lines else edited_lines
# Private: Find the dirty lines.
#
# view - the view, you know
#
# Returns the list of regions matching dirty lines.
def get_modified_lines(view):
try:
on_disk
on_buffer = view.substr(sublime.Region(0, view.size())).splitlines()
except UnicodeDecodeError:
sublime.status_message("File format incompatible with this feature (UTF-8 files only)")
return
lines = []
line_numbers = modified_lines_as_numbers(on_disk, on_buffer)
if line_numbers:
lines = [view.full_line(view.text_point(number,0)) for number in line_numbers]
return lines
# Private: Finds the trailing spaces regions to be deleted.
#
# It abides by the user settings: while in mode "Only Modified Lines", it returns
# the subset of trailing spaces regions which are within dirty lines; otherwise, it
# returns all trailing spaces regions for the document.
#
# view - the view, you know
#
# Returns a list of regions to be deleted.
def find_regions_to_delete(view):
# If the plugin has been running in the background, regions have been matched.
# Otherwise, we must find trailing spaces right now!
if trailing_spaces_live_matching:
regions = view.get_regions('TrailingSpacesMatchedRegions')
else:
(regions, highlightable) = find_trailing_spaces(view)
# Filtering is required in case triming is restricted to dirty regions only.
if trim_modified_lines_only:
modified_lines = get_modified_lines(view)
# If there are no dirty lines, don't do nothing.
if not modified_lines:
return
# Super-private: filters trailing spaces regions to dirty lines only.
#
# As one cannot perform a smart find_all within arbitrary boundaries, we must do some
# extra work:
# - we want to loop through the modified lines set, not the whole trailing regions
# - but we need a way to match modified lines with trailings to those very regions
#
# Hence the reversed dict on regions: keys are the text_point of the begining of
# each region, values are the region's actual boundaries. As a Region is unhashable,
# trailing regions are being recreated later on from those two values.
#
# We loop then loop through the modified lines: for each line, we get its begining
# text_point, and check whether it matches a line with trailing spaces in the
# reversed dict. If so, this is a match (a modified line with trailing spaces), so
# we can re-create and store a Region for the relevant trailing spaces boundaries.
#
# Returns the filtered list of trailing spaces regions for the modified lines set.
def only_those_with_trailing_spaces():
regions_by_begin = {}
matches = []
for region in regions:
begin = view.line(region).begin()
regions_by_begin[begin] = (region.begin(), region.end())
for line in modified_lines:
text_point = line.begin()
if text_point in regions_by_begin:
matches.append(sublime.Region(regions_by_begin[text_point][0], regions_by_begin[text_point][1]))
return matches
regions = only_those_with_trailing_spaces()
return regions
# Private: Deletes the trailing spaces regions.
#
# view - the view, you know
# edit - the Edit object spawned by the deletion command
#
# Returns the number of deleted regions.
def delete_trailing_regions(view, edit):
regions = find_regions_to_delete(view)
if regions:
# Trick: reversing the regions takes care of the growing offset while
# deleting the successive regions.
regions.reverse()
for r in regions:
view.erase(edit, r)
return len(regions)
else:
return 0
# Public: Toggles the highlighting on or off.
class ToggleTrailingSpacesCommand(sublime_plugin.WindowCommand):
def run(self):
view = self.window.active_view()
if max_size_exceeded(view):
sublime.status_message("File is too big, trailing spaces handling disabled.")
return
state = toggle_highlighting(view)
ts_settings.set("trailing_spaces_highlight_color", current_highlighting_scope)
persist_settings()
sublime.status_message('Highlighting of trailing spaces is %s' % state)
def is_checked(self):
return current_highlighting_scope != ""
# Public: Toggles "Modified Lines Only" mode on or off.
class ToggleTrailingSpacesModifiedLinesOnlyCommand(sublime_plugin.WindowCommand):
def run(self):
global trim_modified_lines_only
was_on = ts_settings.get("trailing_spaces_modified_lines_only")
ts_settings.set("trailing_spaces_modified_lines_only", not was_on)
persist_settings()
# TODO: use ts_settings.add_on_change() when it lands in ST3
trim_modified_lines_only = ts_settings.get('trailing_spaces_modified_lines_only')
message = "Let's trim trailing spaces everywhere" if was_on \
else "Let's trim trailing spaces only on modified lines"
sublime.status_message(message)
def is_checked(self):
return ts_settings.get("trailing_spaces_modified_lines_only")
# Public: Matches and highlights trailing spaces on key events, according to the
# current settings.
class TrailingSpacesListener(sublime_plugin.EventListener):
def on_modified(self, view):
if trailing_spaces_live_matching:
match_trailing_spaces(view)
def on_activated(self, view):
if trailing_spaces_live_matching:
match_trailing_spaces(view)
def on_selection_modified(self, view):
if trailing_spaces_live_matching:
match_trailing_spaces(view)
def on_activated(self, view):
self.freeze_last_version(view)
if trailing_spaces_live_matching:
match_trailing_spaces(view)
def on_pre_save(self, view):
self.freeze_last_version(view)
if ts_settings.get("trailing_spaces_trim_on_save"):
view.run_command("delete_trailing_spaces")
# Toggling messes with what is red from the disk, and it breaks the diff
# used when modified_lines_only is true. Honestly, I don't know why (yet).
# Anyway, let's cache the persisted version of the document's buffer for
# later use on specific event, so that we always have a decent version of
# "what's on the disk" to work with.
def freeze_last_version(self, view):
global on_disk
file_name = view.file_name()
# For some reasons, the on_activated hook gets fired on a ghost document
# from time to time.
if file_name:
on_disk = codecs.open(file_name, "r", "utf-8").read().splitlines()
# Public: Deletes the trailing spaces.
class DeleteTrailingSpacesCommand(sublime_plugin.TextCommand):
def run(self, edit):
if max_size_exceeded(self.view):
sublime.status_message("File is too big, trailing spaces handling disabled.")
return
deleted = delete_trailing_regions(self.view, edit)
if deleted:
if ts_settings.get("trailing_spaces_save_after_trim") \
and not ts_settings.get("trailing_spaces_trim_on_save"):
sublime.set_timeout(lambda: self.save(self.view), 10)
msg_parts = {"nbRegions": deleted,
"plural": 's' if deleted > 1 else ''}
message = "Deleted %(nbRegions)s trailing spaces region%(plural)s" % msg_parts
else:
message = "No trailing spaces to delete!"
sublime.status_message(message)
def save(self, view):
if view.file_name() is None:
view.run_command('prompt_save_as')
else:
view.run_command('save')
# ST3 features a plugin_loaded hook which is called when ST's API is ready.
#
# We must therefore call our init callback manually on ST2. It must be the last
# thing in this plugin (thanks, beloved contributors!).
if not int(sublime.version()) > 3000:
plugin_loaded()