1212import sublime_plugin
1313import difflib
1414import codecs
15+ import re
1516
1617from os .path import isfile
1718
1819DEFAULT_MAX_FILE_SIZE = 1048576
1920DEFAULT_IS_ENABLED = True
21+ DEFAULT_UPDATE_INTERVAL = 250
2022DEFAULT_MODIFIED_LINES_ONLY = False
2123
2224# Global settings object and flags.
2527ts_settings_filename = "trailing_spaces.sublime-settings"
2628ts_settings = None
2729trailing_spaces_live_matching = DEFAULT_IS_ENABLED
30+ trailing_spaces_update_interval = DEFAULT_UPDATE_INTERVAL
2831trim_modified_lines_only = DEFAULT_MODIFIED_LINES_ONLY
2932trailing_spaces_syntax_ignore = []
3033startup_queue = []
3134on_disk = None
3235
36+ # dictionary of currently active view ids and last visible regions
37+ active_views = {}
38+
3339
3440# Private: Loads settings and sets whether the plugin (live matching) is enabled.
3541#
3642# Returns nothing.
3743def plugin_loaded ():
3844 global ts_settings_filename , ts_settings , trailing_spaces_live_matching
45+ global trailing_spaces_update_interval
3946 global current_highlighting_scope , trim_modified_lines_only , startup_queue
4047 global DEFAULT_COLOR_SCOPE_NAME , trailing_spaces_syntax_ignore
4148
4249 ts_settings = sublime .load_settings (ts_settings_filename )
4350 trailing_spaces_live_matching = bool (ts_settings .get ("trailing_spaces_enabled" ,
4451 DEFAULT_IS_ENABLED ))
52+ trailing_spaces_update_interval = int (ts_settings .get ("trailing_spaces_update_interval" ,
53+ DEFAULT_UPDATE_INTERVAL ))
4554 current_highlighting_scope = ts_settings .get ("trailing_spaces_highlight_color" ,
4655 "region.redish" )
4756 DEFAULT_COLOR_SCOPE_NAME = current_highlighting_scope
@@ -58,6 +67,14 @@ def plugin_loaded():
5867 persist_settings ()
5968
6069
70+ # Private: Makes sure all timers are stopped.
71+ #
72+ # Returns nothing.
73+ def plugin_unloaded ():
74+ # clear all active views to kill all timeouts
75+ active_views .clear ()
76+
77+
6178# Private: Updates user's settings with in-memory values.
6279#
6380# Allows for persistent settings from the menu.
@@ -67,6 +84,22 @@ def persist_settings():
6784 sublime .save_settings (ts_settings_filename )
6885
6986
87+ # Private: Returns all regions within region that match regex.
88+ #
89+ # view - the view, you know
90+ # region - the region to search
91+ # regex - the regex pattern to search for
92+ #
93+ # Returns all matching regions within region.
94+ def view_find_all_in_region (view , region , regex ):
95+ # find all matches in the region's text
96+ text = view .substr (region )
97+ matches = re .finditer (regex , text , re .MULTILINE )
98+
99+ # return the found positions translated to the region's starting position
100+ return [sublime .Region (m .start () + region .begin (), m .end () + region .begin ()) for m in matches ]
101+
102+
70103# Private: Get the regions matching trailing spaces.
71104#
72105# As the core regexp matches lines, the regions are, well, "per lines".
@@ -81,9 +114,12 @@ def find_trailing_spaces(view):
81114 include_current_line = bool (ts_settings .get ("trailing_spaces_include_current_line" ,
82115 DEFAULT_IS_ENABLED ))
83116 regexp = ts_settings .get ("trailing_spaces_regexp" ) + "$"
84- no_empty_lines_regexp = "(?<=\\ S)%s$" % regexp
85117
86- offending_lines = view .find_all (regexp if include_empty_lines else no_empty_lines_regexp )
118+ if not include_empty_lines :
119+ regexp = "(?<=\\ S)%s$" % regexp
120+
121+ # find all matches in the currently visible region
122+ offending_lines = view_find_all_in_region (view , view .visible_region (), regexp )
87123 ignored_scopes = "," .join (ts_settings .get ("trailing_spaces_scope_ignore" , []))
88124 filtered_lines = []
89125 for region in offending_lines :
@@ -97,9 +133,10 @@ def find_trailing_spaces(view):
97133 if include_current_line or not line :
98134 return [filtered_lines , filtered_lines ]
99135 else :
100- current_offender = view .find (regexp if include_empty_lines else no_empty_lines_regexp , line .a )
101- removal = False if current_offender is None else line .intersects (current_offender )
102- highlightable = [i for i in filtered_lines if i != current_offender ] if removal else filtered_lines
136+ # find all matches in the current line and exclude them from highlighting
137+ current_offenders = view_find_all_in_region (view , line , regexp )
138+ highlightable = [r for r in filtered_lines if r not in current_offenders ]
139+
103140 return [filtered_lines , highlightable ]
104141
105142
@@ -422,6 +459,12 @@ def on_activated(self, view):
422459 if trailing_spaces_live_matching :
423460 match_trailing_spaces (view )
424461
462+ # continuously watch view for changes to the visible region
463+ if not view .id () in active_views :
464+ # track
465+ active_views [view .id ()] = view .visible_region ()
466+ self .update_on_region_change (view )
467+
425468 def on_pre_save (self , view ):
426469 global trim_modified_lines_only
427470 if trim_modified_lines_only :
@@ -430,6 +473,18 @@ def on_pre_save(self, view):
430473 if ts_settings .get ("trailing_spaces_trim_on_save" ):
431474 view .run_command ("delete_trailing_spaces" )
432475
476+ def update_on_region_change (self , view ):
477+ # compare the currently visible region to the previous (if any) and
478+ # update if there were changes
479+ if view .visible_region () != active_views .get (view .id (), view .visible_region ()):
480+ match_trailing_spaces (view )
481+ active_views [view .id ()] = view .visible_region ()
482+
483+ # continue only if the view is still active
484+ if trailing_spaces_live_matching and view .id () in active_views :
485+ sublime .set_timeout_async (lambda : self .update_on_region_change (view ),
486+ trailing_spaces_update_interval )
487+
433488 # Toggling messes with what is red from the disk, and it breaks the diff
434489 # used when modified_lines_only is true. Honestly, I don't know why (yet).
435490 # Anyway, let's cache the persisted version of the document's buffer for
0 commit comments