294 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			294 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import sublime
 | |
| import sublime_plugin
 | |
| import re
 | |
| from itertools import izip_longest
 | |
| from pprint import pprint
 | |
| 
 | |
| REGEX_ESCAPE_CHARS = '\\+*()[]{}^$?|:].,'
 | |
| 
 | |
| # not a fan of using globals like this, but not sure if there's a better way with the plugin
 | |
| # API that ST2 provides.  Tried attaching as fields to active_view, but didn't persiste, I'm guessing
 | |
| # it's just a representation of something that gets regenerated on demand so dynamic fields are transient
 | |
| JUMP_GROUP_GENERATOR = None
 | |
| CURRENT_JUMP_GROUP = None
 | |
| EASY_MOTION_EDIT = None
 | |
| SELECT_TEXT = False
 | |
| COMMAND_MODE_WAS = False
 | |
| JUMP_TARGET_SCOPE = 'string'
 | |
| 
 | |
| 
 | |
| class JumpGroupGenerator:
 | |
|     '''
 | |
|        given a list of region jump targets matching the given character, can emit a series of
 | |
|        JumpGroup dictionaries going forwards with next and backwards with previous
 | |
|     '''
 | |
|     def __init__(self, view, character, placeholder_chars, case_sensitive):
 | |
|         self.view = view
 | |
|         self.case_sensitive = case_sensitive
 | |
|         self.placeholder_chars = placeholder_chars
 | |
|         self.all_jump_targets = self.find_all_jump_targets_in_visible_region(character)
 | |
|         self.interleaved_jump_targets = self.interleave_jump_targets_from_cursor()
 | |
|         self.jump_target_index = 0
 | |
|         self.jump_target_groups = self.create_jump_target_groups()
 | |
|         self.jump_target_group_index = -1
 | |
| 
 | |
|     def determine_re_flags(self, character):
 | |
|         if character == 'enter':
 | |
|             return '(?m)'
 | |
|         elif self.case_sensitive:
 | |
|             return '(?i)'
 | |
|         else:
 | |
|             return ''
 | |
| 
 | |
|     def interleave_jump_targets_from_cursor(self):
 | |
|         sel = self.view.sel()[0]  # multi select not supported, doesn't really make sense
 | |
|         sel_begin = sel.begin()
 | |
|         sel_end = sel.end()
 | |
|         before = []
 | |
|         after = []
 | |
| 
 | |
|         # split them into two lists radiating out from the cursor position
 | |
|         for target in self.all_jump_targets:
 | |
|             if target.begin() < sel_begin:
 | |
|                 # add to beginning of list so closest targets to cursor are first
 | |
|                 before.insert(0, target)
 | |
|             elif target.begin() > sel_end:
 | |
|                 after.append(target)
 | |
| 
 | |
|         # now interleave the two lists together into one list
 | |
|         return [target for targets in izip_longest(before, after) for target in targets if target is not None]
 | |
| 
 | |
|     def create_jump_target_groups(self):
 | |
|         jump_target_groups = []
 | |
| 
 | |
|         while self.has_next_jump_target():
 | |
|             jump_group = dict()
 | |
| 
 | |
|             for placeholder_char in self.placeholder_chars:
 | |
|                 if self.has_next_jump_target():
 | |
|                     jump_group[placeholder_char] = self.interleaved_jump_targets[self.jump_target_index]
 | |
|                     self.jump_target_index += 1
 | |
|                 else:
 | |
|                     break
 | |
| 
 | |
|             jump_target_groups.append(jump_group)
 | |
| 
 | |
|         return jump_target_groups
 | |
| 
 | |
|     def has_next_jump_target(self):
 | |
|         return self.jump_target_index < len(self.interleaved_jump_targets)
 | |
| 
 | |
|     def __len__(self):
 | |
|         return len(self.jump_target_groups)
 | |
| 
 | |
|     def next(self):
 | |
|         self.jump_target_group_index += 1
 | |
| 
 | |
|         if self.jump_target_group_index >= len(self.jump_target_groups) or self.jump_target_group_index < 0:
 | |
|             self.jump_target_group_index = 0
 | |
| 
 | |
|         return self.jump_target_groups[self.jump_target_group_index]
 | |
| 
 | |
|     def previous(self):
 | |
|         self.jump_target_group_index -= 1
 | |
| 
 | |
|         if self.jump_target_group_index < 0 or self.jump_target_group_index >= len(self.jump_target_groups):
 | |
|             self.jump_target_group_index = len(self.jump_target_groups) - 1
 | |
| 
 | |
|         return self.jump_target_groups[self.jump_target_group_index]
 | |
| 
 | |
|     def find_all_jump_targets_in_visible_region(self, character):
 | |
|         visible_region_begin = self.visible_region_begin()
 | |
|         visible_text = self.visible_text()
 | |
|         folded_regions = self.get_folded_regions(self.view)
 | |
|         matching_regions = []
 | |
|         target_regexp = self.target_regexp(character)
 | |
| 
 | |
|         for char_at in (match.start() for match in re.finditer(target_regexp, visible_text)):
 | |
|             char_point = char_at + visible_region_begin
 | |
|             char_region = sublime.Region(char_point, char_point + 1)
 | |
|             if not self.region_list_contains_region(folded_regions, char_region):
 | |
|                 matching_regions.append(char_region)
 | |
| 
 | |
|         return matching_regions
 | |
| 
 | |
|     def region_list_contains_region(self, region_list, region):
 | |
| 
 | |
|         for element_region in region_list:
 | |
|             if element_region.contains(region):
 | |
|                 return True
 | |
|         return False
 | |
| 
 | |
|     def visible_region_begin(self):
 | |
|         return self.view.visible_region().begin()
 | |
| 
 | |
|     def visible_text(self):
 | |
|         visible_region = self.view.visible_region()
 | |
|         return self.view.substr(visible_region)
 | |
| 
 | |
|     def target_regexp(self, character):
 | |
|         re_flags = self.determine_re_flags(character)
 | |
|         if (REGEX_ESCAPE_CHARS.find(character) >= 0):
 | |
|             return re_flags + '\\' + character
 | |
|         elif character == "enter":
 | |
|             return re_flags + "(?=^).|.(?=$)"
 | |
|         else:
 | |
|             return re_flags + character
 | |
| 
 | |
|     def get_folded_regions(self, view):
 | |
|         '''
 | |
|         No way in the API to get the folded regions without unfolding them first
 | |
|         seems to be quick enough that you can't actually see them fold/unfold
 | |
|         '''
 | |
|         folded_regions = view.unfold(view.visible_region())
 | |
|         view.fold(folded_regions)
 | |
|         return folded_regions
 | |
| 
 | |
| 
 | |
| class EasyMotionCommand(sublime_plugin.WindowCommand):
 | |
|     winning_selection = None
 | |
| 
 | |
|     def run(self, character=None, select_text=False):
 | |
|         global JUMP_GROUP_GENERATOR, SELECT_TEXT, JUMP_TARGET_SCOPE
 | |
|         sublime.status_message("EasyMotion: Jump to " + character)
 | |
| 
 | |
|         SELECT_TEXT = select_text
 | |
| 
 | |
|         active_view = self.window.active_view()
 | |
| 
 | |
|         settings = sublime.load_settings("EasyMotion.sublime-settings")
 | |
|         placeholder_chars = settings.get('placeholder_chars', 'abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ')
 | |
|         JUMP_TARGET_SCOPE = settings.get('jump_target_scope', 'string')
 | |
|         case_sensitive = settings.get('case_sensitive', True)
 | |
| 
 | |
|         JUMP_GROUP_GENERATOR = JumpGroupGenerator(active_view, character, placeholder_chars, case_sensitive)
 | |
| 
 | |
|         if len(JUMP_GROUP_GENERATOR) > 0:
 | |
|             self.activate_mode(active_view)
 | |
|             self.window.run_command("show_jump_group")
 | |
|         else:
 | |
|             sublime.status_message("EasyMotion: unable to find any instances of " + character + " in visible region")
 | |
| 
 | |
|     def activate_mode(self, active_view):
 | |
|         global COMMAND_MODE_WAS
 | |
|         active_view.settings().set('easy_motion_mode', True)
 | |
|         # yes, this feels a little dirty to mess with the Vintage plugin, but there
 | |
|         # doesn't appear to be any other way to tell it to not intercept keys, so turn it
 | |
|         # off (if it's on) while we're running EasyMotion
 | |
|         COMMAND_MODE_WAS = active_view.settings().get('command_mode')
 | |
|         if (COMMAND_MODE_WAS):
 | |
|             active_view.settings().set('command_mode', False)
 | |
| 
 | |
| 
 | |
| class ShowJumpGroup(sublime_plugin.WindowCommand):
 | |
|     active_view = None
 | |
| 
 | |
|     def run(self, next=True):
 | |
|         self.active_view = self.window.active_view()
 | |
| 
 | |
|         self.show_jump_group(next)
 | |
| 
 | |
|     def show_jump_group(self, next=True):
 | |
|         global JUMP_GROUP_GENERATOR, CURRENT_JUMP_GROUP
 | |
| 
 | |
|         if next:
 | |
|             CURRENT_JUMP_GROUP = JUMP_GROUP_GENERATOR.next()
 | |
|         else:
 | |
|             CURRENT_JUMP_GROUP = JUMP_GROUP_GENERATOR.previous()
 | |
| 
 | |
|         self.activate_current_jump_group()
 | |
| 
 | |
|     def activate_current_jump_group(self):
 | |
|         global CURRENT_JUMP_GROUP, EASY_MOTION_EDIT, JUMP_TARGET_SCOPE
 | |
|         '''
 | |
|             Start up an edit object if we don't have one already, then create all of the jump targets
 | |
|         '''
 | |
|         if (EASY_MOTION_EDIT is not None):
 | |
|             # normally would call deactivate_current_jump_group here, but apparent ST2 bug prevents it from calling undo correctly
 | |
|             # instead just decorate the new character and keep the same edit object so all changes get undone properly
 | |
|             self.active_view.erase_regions("jump_match_regions")
 | |
|         else:
 | |
|             EASY_MOTION_EDIT = self.active_view.begin_edit()
 | |
| 
 | |
|         for placeholder_char in CURRENT_JUMP_GROUP.keys():
 | |
|             self.active_view.replace(EASY_MOTION_EDIT, CURRENT_JUMP_GROUP[placeholder_char], placeholder_char)
 | |
| 
 | |
|         self.active_view.add_regions("jump_match_regions", CURRENT_JUMP_GROUP.values(), JUMP_TARGET_SCOPE, "dot")
 | |
| 
 | |
| 
 | |
| class JumpTo(sublime_plugin.WindowCommand):
 | |
|     def run(self, character=None):
 | |
|         global COMMAND_MODE_WAS
 | |
| 
 | |
|         self.active_view = self.window.active_view()
 | |
|         self.winning_selection = self.winning_selection_from(character)
 | |
|         self.finish_easy_motion()
 | |
|         self.active_view.settings().set('easy_motion_mode', False)
 | |
|         if (COMMAND_MODE_WAS):
 | |
|             self.active_view.settings().set('command_mode', True)
 | |
| 
 | |
|     def winning_selection_from(self, selection):
 | |
|         global CURRENT_JUMP_GROUP, SELECT_TEXT
 | |
|         winning_region = None
 | |
|         if selection in CURRENT_JUMP_GROUP:
 | |
|             winning_region = CURRENT_JUMP_GROUP[selection]
 | |
| 
 | |
|         if winning_region is not None:
 | |
|             if SELECT_TEXT:
 | |
|                 for current_selection in self.active_view.sel():
 | |
|                     if winning_region.begin() < current_selection.begin():
 | |
|                         return sublime.Region(current_selection.end(), winning_region.begin())
 | |
|                     else:
 | |
|                         return sublime.Region(current_selection.begin(), winning_region.end())
 | |
|             else:
 | |
|                 return sublime.Region(winning_region.begin(), winning_region.begin())
 | |
| 
 | |
|     def finish_easy_motion(self):
 | |
|         '''
 | |
|         We need to clean up after ourselves by restoring the view to it's original state, if the user did
 | |
|         press a jump target that we've got saved, jump to it as the last action
 | |
|         '''
 | |
|         self.deactivate_current_jump_group()
 | |
|         self.jump_to_winning_selection()
 | |
| 
 | |
|     def deactivate_current_jump_group(self):
 | |
|         '''
 | |
|             Close out the edit that we've been messing with and then undo it right away to return the buffer to
 | |
|             the pristine state that we found it in.  Other methods ended up leaving the window in a dirty save state
 | |
|             and this seems to be the cleanest way to get back to the original state
 | |
|         '''
 | |
|         global EASY_MOTION_EDIT
 | |
|         if (EASY_MOTION_EDIT is not None):
 | |
|             self.active_view.end_edit(EASY_MOTION_EDIT)
 | |
|             self.window.run_command("undo")
 | |
|             EASY_MOTION_EDIT = None
 | |
| 
 | |
|         self.active_view.erase_regions("jump_match_regions")
 | |
| 
 | |
|     def jump_to_winning_selection(self):
 | |
|         if self.winning_selection is not None:
 | |
|             self.active_view.run_command("jump_to_winning_selection", {"begin": self.winning_selection.begin(), "end": self.winning_selection.end()})
 | |
| 
 | |
| 
 | |
| class DeactivateJumpTargets(sublime_plugin.WindowCommand):
 | |
|     def run(self):
 | |
|         pprint("DeactivateJumpTargets called")
 | |
|         global EASY_MOTION_EDIT
 | |
| 
 | |
|         active_view = self.window.active_view()
 | |
|         if (EASY_MOTION_EDIT is not None):
 | |
|             active_view.end_edit(EASY_MOTION_EDIT)
 | |
|             self.window.run_command("undo")
 | |
|             EASY_MOTION_EDIT = None
 | |
| 
 | |
|         active_view.erase_regions("jump_match_regions")
 | |
| 
 | |
| 
 | |
| class JumpToWinningSelection(sublime_plugin.TextCommand):
 | |
|     def run(self, edit, begin, end):
 | |
|         winning_region = sublime.Region(long(begin), long(end))
 | |
|         sel = self.view.sel()
 | |
|         sel.clear()
 | |
|         sel.add(winning_region)
 | |
|         self.view.show(winning_region)
 |