Skip to content

Instantly share code, notes, and snippets.

@mattst
Created June 22, 2018 15:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattst/344d71dfde217828377105d9aaea439b to your computer and use it in GitHub Desktop.
Save mattst/344d71dfde217828377105d9aaea439b to your computer and use it in GitHub Desktop.
JumpToCharSeqFixed.py
#
# 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