Created
June 22, 2018 15:22
-
-
Save mattst/344d71dfde217828377105d9aaea439b to your computer and use it in GitHub Desktop.
JumpToCharSeqFixed.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Name: JumpToCharSeq (Jump To Character Sequence) | |
# Author: mattst@i-dig.info | |
# Requires: Sublime Text v3 | |
# | |
# ST Command: jump_to_char_seq | |
# Optional Arg: direction --> "forwards" (default), "backwards" | |
# Optional Arg: case_sensitive --> boolean (default is false) | |
# | |
# { "keys": ["ctrl+k", "ctrl+left"], "command": "jump_to_char_seq", | |
# "args": {"direction": "backwards", "case_sensitive": true} }, | |
import sublime | |
import sublime_plugin | |
NOT_FOUND = -1 | |
FORWARDS = 101 | |
BACKWARDS = 102 | |
SENSITIVE = 103 | |
INSENSITIVE = 104 | |
REGIONS_KEY = "jump_to_char_seq_regions_key" | |
ADD_SELECTIONS_CMD = "jump_to_char_seq_add_selections" | |
class JumpToCharSeqCommand(sublime_plugin.TextCommand): | |
""" | |
The JumpToCharSeqCommand class is a Sublime Text plugin which moves all | |
selections to the next instance of a user entered character sequence to | |
occur on the same line. It can move either forwards or backwards from the | |
starting position and perform a case sensitive or insensitive character | |
search depending on the command's args. It is particularly useful when | |
simultaneously editing multiple lines which are similar but not identical. | |
""" | |
def run(self, edit, **kwargs): | |
self.search_direction = self.get_search_direction(**kwargs) | |
self.case_sensitivity = self.get_case_sensitivity(**kwargs) | |
self.original_sels = list(self.view.sel()) | |
if len(self.original_sels) < 1: | |
return | |
# Clear and redraw the selections, nicely styled so they are | |
# shown clearly to the user while the search term is entered. | |
self.view.sel().clear() | |
self.view.add_regions(REGIONS_KEY, self.original_sels, "None", "dot", | |
sublime.DRAW_NO_FILL | sublime.DRAW_EMPTY_AS_OVERWRITE) | |
self.show_input_panel() | |
def get_search_direction(self, **kwargs): | |
""" Returns the search direction arg or the forwards default. """ | |
direction = kwargs.get("direction") | |
if isinstance(direction, str): | |
if direction == "forwards": | |
return FORWARDS | |
elif direction == "backwards": | |
return BACKWARDS | |
return FORWARDS | |
def get_case_sensitivity(self, **kwargs): | |
""" Returns the case sensitivity arg or the insensitive default. """ | |
case_sensitive = kwargs.get("case_sensitive") | |
if isinstance(case_sensitive, bool): | |
if case_sensitive: | |
return SENSITIVE | |
else: | |
return INSENSITIVE | |
return INSENSITIVE | |
def show_input_panel(self): | |
""" Display the input panel for search term entry. """ | |
if self.search_direction == FORWARDS: | |
input_panel_msg = "Enter Forwards Jump Search Term" | |
else: | |
input_panel_msg = "Enter Backwards Jump Search Term" | |
if self.case_sensitivity == INSENSITIVE: | |
input_panel_msg += " (Case Insensitive):" | |
else: | |
input_panel_msg += " (Case Sensitive):" | |
self.view.window().show_input_panel(input_panel_msg, "", | |
self.on_done, None, self.on_cancel) | |
def on_done(self, search_term): | |
""" Called when the user accepts the text in the input panel. """ | |
if len(search_term) > 0: | |
self.move_selections(search_term) | |
else: | |
self.on_cancel() | |
def on_cancel(self): | |
""" Called when the user cancels the input panel. """ | |
# Restore the original selections. | |
self.view.run_command(ADD_SELECTIONS_CMD) | |
def move_selections(self, search_term): | |
""" | |
Controls moving each selection to the first instance of search_term | |
that is found on the same line in a forwards or backwards direction. | |
""" | |
new_sels = [] | |
for sel in self.original_sels: | |
# If the search does not find a match then sel is returned by | |
# do_search_on_line(), so new_sels is always fully populated. | |
new_sels.append(self.do_search_on_line(sel, search_term)) | |
# Drawing new_sels in the same way as original_sels are drawn in | |
# run() is done so that they remain neatly displayed if the user | |
# performs a soft undo to roll back what this plugin has done. | |
self.view.add_regions(REGIONS_KEY, new_sels, "None", "dot", | |
sublime.DRAW_NO_FILL | sublime.DRAW_EMPTY_AS_OVERWRITE) | |
# An edit object is needed to reliably add selections, | |
# this class's edit object expired when run() returned. | |
self.view.run_command(ADD_SELECTIONS_CMD) | |
def do_search_on_line(self, sel, search_term): | |
""" | |
Performs a search for search_term on the line that sel is on, returning | |
the region it is found at or sel unchanged if search_term is not found. | |
""" | |
# Ignore selections which span multiple lines. | |
if self.view.line(sel.begin()) != self.view.line(sel.end()): | |
return sel | |
sel_line = self.view.line(sel.begin()) | |
sel_line_str = self.view.substr(sel_line) | |
if self.case_sensitivity == INSENSITIVE: | |
sel_line_str = sel_line_str.lower() | |
search_term = search_term.lower() | |
search_term_pos = self.get_search_term_pos(sel, sel_line, | |
sel_line_str, search_term) | |
if search_term_pos == NOT_FOUND: | |
return sel | |
search_term_begin = sel_line.begin() + search_term_pos | |
search_term_end = search_term_begin + len(search_term) | |
return sublime.Region(search_term_begin, search_term_end) | |
def get_search_term_pos(self, sel, sel_line, sel_line_str, search_term): | |
""" Returns the first index of search_term found in sel_line_str. """ | |
# sel.begin() will always be >= sel_line.begin(). | |
sel_offset = sel.begin() - sel_line.begin() | |
if self.search_direction == FORWARDS: | |
begin_pos = sel_offset + sel.size() | |
end_pos = len(sel_line_str) | |
return sel_line_str.find(search_term, begin_pos, end_pos) | |
elif self.search_direction == BACKWARDS: | |
begin_pos = 0 | |
end_pos = sel_offset | |
return sel_line_str.rfind(search_term, begin_pos, end_pos) | |
# Fail safe; should never happen. | |
return NOT_FOUND | |
class JumpToCharSeqAddSelectionsCommand(sublime_plugin.TextCommand): | |
""" | |
Reliably adds the selections referenced by REGIONS_KEY, these will always | |
be either the new selections or the original ones. | |
The JumpToCharSeqCommand class's edit object expires when its run() method | |
returns. Since an edit object is required to reliably add selections, this | |
class provides that functionality. | |
""" | |
def run(self, edit): | |
sels = self.view.get_regions(REGIONS_KEY) | |
self.view.erase_regions(REGIONS_KEY) | |
self.view.sel().add_all(sels) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment