diff --git a/Default.sublime-keymap b/Default.sublime-keymap new file mode 100755 index 0000000..292f219 --- /dev/null +++ b/Default.sublime-keymap @@ -0,0 +1,6 @@ +[ + { "keys": ["alt+up"], "command": "jump_prev_indent" }, + { "keys": ["alt+shift+up"], "command": "jump_prev_indent", "args": { "extend_selection": true} }, + { "keys": ["alt+down"], "command": "jump_next_indent" }, + { "keys": ["alt+shift+down"], "command": "jump_next_indent", "args": { "extend_selection": true} } +] diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/file_scanner.py b/file_scanner.py new file mode 100644 index 0000000..5eacd85 --- /dev/null +++ b/file_scanner.py @@ -0,0 +1,115 @@ +import sublime, re +from .view_helper import ViewHelper + +class FileScanner: + def __init__(self, view): + self.view = view + self.view_helper = ViewHelper(view) + + def scan_from_beginning(self, direction = 'forward'): + search_start = self.next_line(self.view_helper.initial_selection().begin()) + return self.search(self.search_str(), search_start) - 1 + + def scan_from_end(self, direction = 'forward'): + view_helper = self.view_helper + search_start = view_helper.find_bol(view_helper.initial_selection().begin()) + search_end = self.previous_line(view_helper.initial_selection().end()) + return self.reverse_search(self.search_str(), search_start, search_end) - 1 + + def scan(self, direction = 'forward'): + if direction == 'forward': + return self.search(self.search_str(), self.next_line()) - 1 + else: + return self.reverse_search(self.search_str(), 0, self.previous_line()) - 1 + + def search_str(self): + if re.match(r"^\s*$", self.str_to_left()) and re.match(r"^\s+\S+", self.str_to_right()): + search_str = "^ {0," + str(len(self.str_to_left())) + "}\S+" + else: + search_str = "^" + self.leading_spaces() + "\S" + return search_str + + def str_to_left(self): + view = self.view + sel_pt = self.view_helper.initial_cursor_position() + left_pt = view.line(sel_pt).a + left_region = sublime.Region(left_pt, sel_pt) + return view.substr(left_region) + + def str_to_right(self): + view = self.view + sel_pt = self.view_helper.initial_cursor_position() + right_pt = view.line(sel_pt).b + right_region = sublime.Region(sel_pt, right_pt) + return view.substr(right_region) + + def leading_spaces(self): + spaces = re.match(r"(\s*)", self.current_line()) + return spaces.group(0) + + def current_line(self): + view = self.view + line_region = view.line(self.view_helper.initial_cursor_position()) + return view.substr(line_region) + + def previous_line(self, position = None): + position = position or self.view_helper.initial_cursor_position() + return self.view.line(position).a - 1 + + def next_line(self, position = None): + position = position or self.view_helper.initial_cursor_position() + view = self.view + return view.rowcol(view.line(position).b + 1)[0] + + def search(self, pattern, start_line = None, flags = 0): + view = self.view + + if start_line: + start = view.text_point(start_line, 0) + else: + start = self.view_helper.initial_cursor_position().begin() + reg = view.find(pattern, start, flags) + + if not reg is None: + row = (view.rowcol(reg.begin())[0] + 1) + else: + row = self.calculate_relative_ref('.', start_line = start_line) + return row + + def reverse_search(self, pattern, start = 0, end = -1, flags = 0): + view = self.view + + if end == -1: + end = view.size() + + end = self.view_helper.find_eol(view.line(end).a) + match = self.find_last_match(pattern, start, end) + return view.rowcol(match.begin())[0] + 1 + + def search_in_range(self, pattern, start, end, flags = 0): + match = self.view.find(pattern, start, flags) + if match and ((match.begin() >= start) and (match.end() <= end)): + return True + + def find_last_match(self, pattern, start, end, flags = 0): + """Find last occurrence of `pattern` between `start`, `end`. + """ + view = self.view + match = view.find(pattern, start, flags) + new_match = None + while match: + new_match = view.find(pattern, match.end(), flags) + if new_match and new_match.end() <= end: + match = new_match + else: + return match + + def calculate_relative_ref(self, where, start_line = None): + view = self.view + + if where == '$': + return view.rowcol(view.size())[0] + 1 + if where == '.': + if start_line: + return view.rowcol(view.text_point(start_line, 0))[0] + 1 + return view.rowcol(view.sel()[0].begin())[0] + 1 diff --git a/jump_along_indent.py b/jump_along_indent.py new file mode 100644 index 0000000..2a097c1 --- /dev/null +++ b/jump_along_indent.py @@ -0,0 +1,79 @@ +import sublime, sublime_plugin, re, sys +from .file_scanner import FileScanner +from .view_helper import ViewHelper + +def cursor_position(view): + return view.sel()[0] + +def build_selection(view, new_region, target): + view.sel().clear() + view.sel().add(new_region) + view.show(target) + +class JumpNextIndentCommand(sublime_plugin.TextCommand): + def run(self, edit, extend_selection = False): + view = self.view + self.view_helper = ViewHelper(view) + self.scanner = FileScanner(view) + cursor_at_top_of_selection = self.view_helper.cursor_at_top_of_selection() + + if not extend_selection: + self.jump_downward() + elif cursor_at_top_of_selection: + self.deselect_downward() + else: + self.select_downward() + + def jump_downward(self): + target = self.target_point() + new_region = sublime.Region(target) + build_selection(self.view, new_region, target) + + def select_downward(self): + target = self.target_point() + new_region = sublime.Region(self.view_helper.initial_selection().begin(), target) + build_selection(self.view, new_region, target) + + def deselect_downward(self): + matched_point = self.scanner.scan_from_beginning() + target = self.target_point(matched_point) + new_region = sublime.Region(self.view_helper.initial_selection().end(), target) + build_selection(self.view, new_region, target) + + def target_point(self, matched_point = None): + matched_point = matched_point or self.scanner.scan() + return self.view.text_point(matched_point, self.view_helper.target_column(matched_point)) + +class JumpPrevIndentCommand(sublime_plugin.TextCommand): + def run(self, edit, extend_selection = False): + view = self.view + self.view_helper = ViewHelper(view) + self.scanner = FileScanner(view) + cursor_at_bottom_of_selection = self.view_helper.cursor_at_bottom_of_selection() + + if not extend_selection: + self.jump_upward() + elif cursor_at_bottom_of_selection: + self.deselect_upward() + else: + self.select_upward() + + def jump_upward(self): + target = self.target_point() + new_region = sublime.Region(target) + build_selection(self.view, new_region, target) + + def select_upward(self): + target = self.target_point() + new_region = sublime.Region(self.view_helper.initial_selection().end(), target) + build_selection(self.view, new_region, target) + + def deselect_upward(self): + matched_point = self.scanner.scan_from_end(direction = 'backward') + target = self.target_point(matched_point) + new_region = sublime.Region(self.view_helper.initial_selection().begin(), target) + build_selection(self.view, new_region, target) + + def target_point(self, matched_point = None): + matched_point = matched_point or self.scanner.scan(direction = 'backward') + return self.view.text_point(matched_point, self.view_helper.target_column(matched_point)) diff --git a/view_helper.py b/view_helper.py new file mode 100644 index 0000000..67a2801 --- /dev/null +++ b/view_helper.py @@ -0,0 +1,32 @@ +class ViewHelper: + def __init__(self, view): + self.view = view + + def initial_cursor_position(self): + return self.initial_selection().b + + def initial_selection(self): + return self.view.sel()[0] + + def initial_column(self): + return self.view.rowcol(self.initial_cursor_position())[1] + + def cursor_at_top_of_selection(self): + return self.initial_selection().a > self.initial_selection().b + + def cursor_at_bottom_of_selection(self): + return self.initial_selection().b > self.initial_selection().a + + def target_column(self, target): + end_of_line = self.find_eol(target) + + if self.initial_column() > end_of_line: + return end_of_line + else: + return self.initial_column() + + def find_eol(self, point): + return self.view.line(point).end() + + def find_bol(self, point): + return self.view.line(point).begin()