├── .gitignore ├── .python-version ├── .travis.yml ├── Default.sublime-keymap ├── LICENSE ├── Main.sublime-menu ├── README.md ├── file_scanner.py ├── jump_along_indent.py ├── jump_along_indent.sublime-settings ├── tests ├── helper.py ├── test_jump_next_indent.py ├── test_jump_next_offset_indent.py ├── test_jump_prev_indent.py └── test_jump_prev_offset_indent.py └── view_helper.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | env: 6 | global: 7 | - PACKAGE="sublime_jump_along_indent" 8 | matrix: 9 | - SUBLIME_TEXT_VERSION="3" 10 | 11 | before_install: 12 | - curl -OL https://raw.githubusercontent.com/randy3k/UnitTesting/master/sbin/travis.sh 13 | 14 | install: 15 | - sh travis.sh bootstrap 16 | 17 | before_script: 18 | - export DISPLAY=:99.0 19 | - sh -e /etc/init.d/xvfb start 20 | 21 | script: 22 | - sh travis.sh run_tests 23 | 24 | notifications: 25 | email: false 26 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["alt+up"], "command": "jump_prev_indent" }, 3 | { "keys": ["alt+shift+up"], "command": "jump_prev_indent", "args": { "extend_selection": true} }, 4 | { "keys": ["alt+down"], "command": "jump_next_indent" }, 5 | { "keys": ["alt+shift+down"], "command": "jump_next_indent", "args": { "extend_selection": true} } 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Matthew Wean 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "Jump Along Indent", 16 | "children": 17 | [ 18 | { 19 | "command": "open_file", 20 | "id": "bindings", 21 | "args": { 22 | "file": "${packages}/Jump Along Indent/Default.sublime-keymap" 23 | }, 24 | "caption": "Key Bindings – Default" 25 | }, 26 | { 27 | "command": "open_file", 28 | "args": { 29 | "file": "${packages}/User/Default (Windows).sublime-keymap", 30 | "platform": "Windows" 31 | }, 32 | "caption": "Key Bindings – User" 33 | }, 34 | { 35 | "command": "open_file", 36 | "args": { 37 | "file": "${packages}/User/Default (OSX).sublime-keymap", 38 | "platform": "OSX" 39 | }, 40 | "caption": "Key Bindings – User" 41 | }, 42 | { 43 | "command": "open_file", 44 | "args": { 45 | "file": "${packages}/User/Default (Linux).sublime-keymap", 46 | "platform": "Linux" 47 | }, 48 | "caption": "Key Bindings – User" 49 | }, 50 | { 51 | "command": "open_file", 52 | "args": { 53 | "file": "${packages}/Jump Along Indent/jump_along_indent.sublime-settings" 54 | }, 55 | "caption": "Settings - Default" 56 | }, 57 | { 58 | "command": "open_file", 59 | "args": { 60 | "file": "${packages}/User/jump_along_indent.sublime-settings" 61 | }, 62 | "caption": "Settings - User" 63 | } 64 | ] 65 | } 66 | ] 67 | } 68 | ] 69 | } 70 | ] 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sublime Jump Along Indent # 2 | 3 | [![Sublime Version](http://img.shields.io/badge/sublime_text-3-orange.svg?style=flat)](http://www.sublimetext.com/3) 4 | [![Build Status](http://img.shields.io/travis/mwean/sublime_jump_along_indent/master.svg?style=flat)](https://travis-ci.org/mwean/sublime_jump_along_indent) 5 | [![Release Version](http://img.shields.io/badge/release-v0.4.0-blue.svg?style=flat)](https://github.com/mwean/sublime_jump_along_indent/releases/latest) 6 | [![MIT Licesne](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://github.com/mwean/sublime_jump_along_indent/blob/master/LICENSE) 7 | 8 | ## Description ## 9 | 10 | A Sublime Text 3/4 plugin to move the cursor to next/previous line at the same indentation level as the current line. 11 | 12 | There are two commands: `jump_next_indent` and `jump_prev_indent`. 13 | 14 | ![Before jumping downward](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/pre_jump.png) → ![After jumping downward](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/post_jump.png) 15 | 16 | If the cursor is to the left of an indented line, it will jump to the next line that is at that level or less. 17 | 18 | ![Before jumping downward](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/pre_jump_inset.png) → ![After jumping downward](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/post_jump_inset.png) 19 | 20 | If there are several lines on the same indent level, the cursor will jump to the beginning or end of the block of lines. 21 | 22 | ![Before jumping downward](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/pre_jump_block.png) → ![After jumping downward](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/post_jump_block.png) 23 | 24 | ### Extending selection ### 25 | 26 | With the option `extend_selection: true` you can extend the selection while jumping: 27 | 28 | ![Before selecting downward](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/pre_jump.png) → ![After selecting downward](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/post_select.png) 29 | 30 | ### Jumping to a different indent level ### 31 | 32 | You can also use the `indent_offset` option to jump to a more or less-indented line. For example, with `indent_offset = -1`: 33 | 34 | ![Before jumping up and out](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/pre_jump_out.png) → ![After jumping up and out](https://s3.amazonaws.com/mwean-github/sublime_jump_along_indent/post_jump_out.png) 35 | 36 | ## Installation ## 37 | 38 | ### Using Package Control ### 39 | - Select "Package Control: Install Package" from the Command Palette 40 | - Search for "Jump Along Indent" 41 | 42 | ### Using Git ### 43 | - Clone the repository in your Sublime Text Packages directory: 44 | - `git clone https://github.com/mwean/sublime_jump_along_indent.git /path/to/sublime/packages` 45 | 46 | ### Not Using Git ### 47 | - [Download code archive](https://github.com/mwean/sublime_jump_along_indent/archive/master.zip) 48 | - Unzip and move to Sublime Text packages folder 49 | 50 | ## Usage ## 51 | 52 | The plugin comes with a set of default keybindings: 53 | 54 | - `alt+up`: Jump to previous indented line 55 | - `alt+down`: Jump to next indented line 56 | - `alt+shift+up`: Jump to previous indented line and extend selection 57 | - `alt+shift+down`: Jump to next indented line and extend selection 58 | 59 | ## Credits ## 60 | 61 | Some of the methods in `file_scanner.py` were adapted from the [VintageEx](https://github.com/SublimeText/VintageEx) plugin. 62 | -------------------------------------------------------------------------------- /file_scanner.py: -------------------------------------------------------------------------------- 1 | import sublime, re 2 | from .view_helper import ViewHelper 3 | 4 | class FileScanner: 5 | def __init__(self, view, viewhelper): 6 | self.view = view 7 | self.view_helper = viewhelper 8 | 9 | def scan(self, direction = 'forward', indent_offset = 0): 10 | self.indent_offset = indent_offset 11 | if direction == 'forward': 12 | indent_match = self.search(self.search_str(), self.next_point()) or 0 13 | possible_matches = [indent_match] 14 | 15 | if indent_offset == 0: 16 | block_match = self.find_last_line_of_block() 17 | possible_matches.append(block_match) 18 | 19 | return max(possible_matches) 20 | else: 21 | if self.previous_point() < 0: 22 | end = 0 23 | else: 24 | end = self.previous_point() 25 | 26 | indent_match = self.reverse_search(self.search_str(), 0, end) 27 | possible_matches = [indent_match] 28 | 29 | if indent_offset == 0: 30 | block_match = self.find_first_line_of_block(end) 31 | possible_matches.append(block_match) 32 | 33 | return min(possible_matches) 34 | 35 | def adapt_indent(self, indent_str): 36 | tab_size = self.view.settings().get("tab_size") 37 | indent = max(0, len(indent_str) + tab_size * self.indent_offset) 38 | return indent 39 | 40 | def search_str(self): 41 | settings = sublime.load_settings("jump_along_indent.sublime-settings") 42 | respect_cursor_position = settings.get("respect_cursor_position") 43 | between_leading_spaces = re.match(r"^\s*$", self.str_to_left()) and re.match(r"^\s+\S+", self.str_to_right()) 44 | 45 | if respect_cursor_position and between_leading_spaces: 46 | indent = self.adapt_indent(self.str_to_left()) 47 | search_str = "^ {0," + str(indent) + "}\S+" 48 | else: 49 | indent = self.adapt_indent(self.leading_spaces()) 50 | search_str = "^ {" + str(indent) + "}\S" 51 | return search_str 52 | 53 | def str_to_left(self): 54 | view = self.view 55 | sel_pt = self.view_helper.initial_cursor_position() 56 | left_pt = view.line(sel_pt).a 57 | left_region = sublime.Region(left_pt, sel_pt) 58 | return view.substr(left_region) 59 | 60 | def str_to_right(self): 61 | view = self.view 62 | sel_pt = self.view_helper.initial_cursor_position() 63 | right_pt = view.line(sel_pt).b 64 | right_region = sublime.Region(sel_pt, right_pt) 65 | return view.substr(right_region) 66 | 67 | def leading_spaces(self): 68 | spaces = re.match(r"(\s*)", self.current_line()) 69 | return spaces.group(0) 70 | 71 | def current_line(self): 72 | view = self.view 73 | line_region = view.line(self.view_helper.initial_cursor_position()) 74 | return view.substr(line_region) 75 | 76 | def previous_point(self, position = None): 77 | position = position or self.view_helper.initial_cursor_position() 78 | return self.view.line(position).a - 1 79 | 80 | def previous_line(self, position = None): 81 | return self.view.rowcol(self.previous_point(position))[0] 82 | 83 | def next_point(self, position = None): 84 | position = position or self.view_helper.initial_cursor_position() 85 | return self.view.line(position).b + 1 86 | 87 | def next_line(self, position = None): 88 | return self.view.rowcol(self.next_point(position))[0] 89 | 90 | def search(self, pattern, start = None, flags = 0): 91 | view = self.view 92 | start = start or self.view_helper.initial_cursor_position() 93 | match = view.find(pattern, start, flags) 94 | 95 | if not match.a == -1: 96 | matched_row = view.rowcol(match.begin())[0] 97 | return matched_row 98 | 99 | def reverse_search(self, pattern, start = 0, end = -1): 100 | view = self.view 101 | 102 | if end == -1: 103 | end = view.size() 104 | 105 | end = self.view_helper.find_eol(view.line(end).a) 106 | match = self.find_last_match(pattern, start, end) 107 | 108 | if match == None: 109 | return self.view_helper.initial_row() 110 | else: 111 | matched_row = view.rowcol(match.begin())[0] 112 | return matched_row 113 | 114 | def find_last_match(self, pattern, start, end): 115 | matches = self.view.find_all(pattern) 116 | filtered_matches = [match for match in matches if match.begin() >= start and match.begin() <= end] 117 | 118 | if len(filtered_matches) > 0: 119 | return filtered_matches[-1] 120 | 121 | def find_last_line_of_block(self): 122 | view = self.view 123 | matched_line = self.search(self.block_pattern()) 124 | last_line = view.rowcol(view.size())[0] 125 | 126 | if not matched_line or (matched_line == last_line - 1 and self.block_extends_to_bottom()): 127 | return last_line 128 | else: 129 | return matched_line - 1 130 | 131 | def find_first_line_of_block(self, end): 132 | pattern = self.block_pattern() + "|(^\A.*$)" 133 | matched_line = self.reverse_search(pattern, 0, end) 134 | 135 | if matched_line == 0 and self.block_extends_to_top(): 136 | return 0 137 | else: 138 | return matched_line + 1 139 | 140 | def block_extends_to_top(self): 141 | return self.view.find("\A" + self.leading_spaces() + "\S", 0) 142 | 143 | def block_extends_to_bottom(self): 144 | return self.view.find("^" + self.leading_spaces() + "\S.*\z", self.next_point()) 145 | 146 | def block_pattern(self): 147 | pattern = "(^\s*$)" 148 | space_count = len(self.leading_spaces()) 149 | 150 | if space_count > 0: 151 | pattern += "|(^ {0," + str(space_count - 1) + "}\S+)" 152 | 153 | pattern += "|(^ {" + str(space_count + 1) + ",}\S+)" 154 | return pattern 155 | -------------------------------------------------------------------------------- /jump_along_indent.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | from .file_scanner import FileScanner 4 | from .view_helper import ViewHelpers 5 | 6 | 7 | class JumpIndentCommand(object): 8 | def run(self, edit, extend_selection=False, indent_offset=0): 9 | self.indent_offset = indent_offset 10 | self.clear_selection() 11 | for self.view_helper in ViewHelpers(self.view): 12 | self.scanner = FileScanner(self.view, self.view_helper) 13 | 14 | if not extend_selection: 15 | self.jump() 16 | elif self.check_selection_pos(): 17 | self.deselect() 18 | else: 19 | self.select() 20 | 21 | self.update_selection() 22 | 23 | def clear_selection(self): 24 | self.listsel = [] 25 | 26 | def build_selection(self, new_region): 27 | self.listsel.append(new_region) 28 | 29 | def update_selection(self): 30 | self.view.sel().clear() 31 | for s in self.listsel: 32 | self.view.sel().add(s) 33 | self.view.show(s) 34 | 35 | def jump(self): 36 | target = self.target_point() 37 | new_region = sublime.Region(target, target, self.view_helper.initial_xpos()) 38 | self.build_selection(new_region) 39 | 40 | def select(self): 41 | target = self.target_point() 42 | new_region = sublime.Region(self.get_select_begin_pos(), target, self.view_helper.initial_xpos()) 43 | self.build_selection(new_region) 44 | 45 | def deselect(self): 46 | matched_row = self.scanner.scan(self.direction, self.indent_offset) 47 | target = self.target_point(matched_row) 48 | new_region = sublime.Region(self.get_deselect_begin_pos(), target, self.view_helper.initial_xpos()) 49 | self.build_selection(new_region) 50 | 51 | def target_point(self, matched_row=None): 52 | matched_row = matched_row or self.scanner.scan(self.direction, self.indent_offset) 53 | selection_offset = self.indent_offset 54 | 55 | if matched_row == self.view_helper.initial_row(): 56 | selection_offset = 0 57 | 58 | matched_point_bol = self.view.text_point(matched_row, 0) 59 | return self.view.text_point(matched_row, self.view_helper.target_column(matched_point_bol, selection_offset)) 60 | 61 | 62 | class JumpNextIndentCommand(JumpIndentCommand, sublime_plugin.TextCommand): 63 | def __init__(self, *args, **kwargs): 64 | super(JumpNextIndentCommand, self).__init__(*args, **kwargs) 65 | self.direction = 'forward' 66 | 67 | def check_selection_pos(self): 68 | return self.view_helper.cursor_at_top_of_selection() 69 | 70 | def get_select_begin_pos(self): 71 | return self.view_helper.initial_selection().begin() 72 | 73 | def get_deselect_begin_pos(self): 74 | return self.view_helper.initial_selection().end() 75 | 76 | 77 | class JumpPrevIndentCommand(JumpIndentCommand, sublime_plugin.TextCommand): 78 | def __init__(self, *args, **kwargs): 79 | super(JumpPrevIndentCommand, self).__init__(*args, **kwargs) 80 | self.direction = 'backward' 81 | 82 | def check_selection_pos(self): 83 | return self.view_helper.cursor_at_bottom_of_selection() 84 | 85 | def get_select_begin_pos(self): 86 | return self.view_helper.initial_selection().end() 87 | 88 | def get_deselect_begin_pos(self): 89 | return self.view_helper.initial_selection().begin() 90 | -------------------------------------------------------------------------------- /jump_along_indent.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // By default, "Jump Along Indent" will take the indent before the cursor 3 | // into account. 4 | // If you want that the cursor position within the line doesn't play a role 5 | // and instead always the full indent of the current line is relevant, set 6 | // respect_cursor_position to false. 7 | "respect_cursor_position" : true 8 | } -------------------------------------------------------------------------------- /tests/helper.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from unittest import TestCase 3 | 4 | class TestHelper(TestCase): 5 | def setUp(self): 6 | self.view = sublime.active_window().new_file() 7 | self.view.settings().set("tab_size", 2) 8 | 9 | def tearDown(self): 10 | if self.view: 11 | self.view.set_scratch(True) 12 | self.view.window().run_command('close_file') 13 | 14 | def set_text(self, lines): 15 | for line in lines: 16 | self.view.run_command('move_to', { 'to': 'bol', 'extend': True }) 17 | self.view.run_command('insert', { 'characters': line + "\n" }) 18 | 19 | def check_command(self, text, start, end, extend_selection=False, indent_offset=0): 20 | self.set_text(text) 21 | self.view.sel().clear() 22 | self.view.sel().add(sublime.Region(start[0], start[1])) 23 | self.view.run_command(self.command(), { 'extend_selection': extend_selection, 'indent_offset': indent_offset }) 24 | 25 | self.assertEqual(self.view.sel()[0], sublime.Region(end[0], end[1])) 26 | -------------------------------------------------------------------------------- /tests/test_jump_next_indent.py: -------------------------------------------------------------------------------- 1 | from helper import TestHelper 2 | import sublime 3 | 4 | class TestJumpNextIndent(TestHelper): 5 | def command(self): 6 | return 'jump_next_indent' 7 | 8 | def test_empty_lines(self): 9 | lines = [ 10 | 'Lorem ipsum dolor sit amet', 11 | '', 12 | '', 13 | 'Lorem ipsum dolor sit amet' 14 | ] 15 | starting_selection = [0, 0] 16 | ending_selection = [29, 29] 17 | 18 | self.check_command(lines, starting_selection, ending_selection) 19 | 20 | def test_indented_lines(self): 21 | lines = [ 22 | 'Lorem ipsum dolor sit amet', 23 | ' Lorem ipsum dolor sit amet', 24 | ' Lorem ipsum dolor sit amet', 25 | 'Lorem ipsum dolor sit amet' 26 | ] 27 | 28 | starting_selection = [0, 0] 29 | ending_selection = [85, 85] 30 | 31 | self.check_command(lines, starting_selection, ending_selection) 32 | 33 | def test_end_of_file(self): 34 | lines = [ 35 | 'Lorem ipsum dolor sit amet', 36 | ' Lorem ipsum dolor sit amet', 37 | ' Lorem ipsum dolor sit amet' 38 | ] 39 | 40 | starting_selection = [0, 0] 41 | ending_selection = [0, 0] 42 | 43 | self.check_command(lines, starting_selection, ending_selection) 44 | 45 | def test_maintain_column(self): 46 | lines = [ 47 | 'Lorem ipsum dolor sit amet', 48 | 'Lorem ipsum dolor sit amet', 49 | 'Lorem ipsum dolor sit amet' 50 | ] 51 | 52 | starting_selection = [12, 12] 53 | ending_selection = [66, 66] 54 | 55 | self.check_command(lines, starting_selection, ending_selection) 56 | 57 | def test_jump_to_shorter_line(self): 58 | lines = [ 59 | 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet', 60 | '', 61 | 'Lorem ipsum dolor sit amet' 62 | ] 63 | 64 | starting_selection = [53, 53] 65 | ending_selection = [81, 81] 66 | 67 | self.check_command(lines, starting_selection, ending_selection) 68 | 69 | def test_jump_to_first_intersection(self): 70 | lines = [ 71 | ' Lorem ipsum dolor sit amet', 72 | '', 73 | ' Lorem ipsum dolor sit amet' 74 | ] 75 | 76 | starting_selection = [3, 3] 77 | ending_selection = [35, 35] 78 | 79 | self.check_command(lines, starting_selection, ending_selection) 80 | 81 | def test_create_selection(self): 82 | lines = [ 83 | 'Lorem ipsum dolor sit amet', 84 | ' Lorem ipsum dolor sit amet', 85 | ' Lorem ipsum dolor sit amet', 86 | 'Lorem ipsum dolor sit amet' 87 | ] 88 | 89 | starting_selection = [0, 0] 90 | ending_selection = [0, 85] 91 | 92 | self.check_command(lines, starting_selection, ending_selection, extend_selection = True) 93 | 94 | def test_extend_selection(self): 95 | lines = [ 96 | 'Lorem ipsum dolor sit amet', 97 | ' Lorem ipsum dolor sit amet', 98 | ' Lorem ipsum dolor sit amet', 99 | 'Lorem ipsum dolor sit amet' 100 | ] 101 | 102 | starting_selection = [0, 27] 103 | ending_selection = [0, 85] 104 | 105 | self.check_command(lines, starting_selection, ending_selection, extend_selection = True) 106 | 107 | def test_subtract_selection(self): 108 | lines = [ 109 | 'Lorem ipsum dolor sit amet', 110 | ' Lorem ipsum dolor sit amet', 111 | ' Lorem ipsum dolor sit amet', 112 | 'Lorem ipsum dolor sit amet' 113 | ] 114 | 115 | starting_selection = [111, 0] 116 | ending_selection = [111, 85] 117 | 118 | self.check_command(lines, starting_selection, ending_selection, extend_selection = True) 119 | 120 | def test_respect_cursor_position(self): 121 | lines = [ 122 | ' Lorem ipsum dolor sit amet', 123 | 'Lorem ipsum dolor sit amet', 124 | ' Lorem ipsum dolor sit amet' 125 | ] 126 | 127 | starting_selection = [0, 0] 128 | ending_selection = [29, 29] 129 | 130 | self.check_command(lines, starting_selection, ending_selection) 131 | 132 | def test_disrespect_cursor_position(self): 133 | settings = sublime.load_settings("jump_along_indent.sublime-settings") 134 | settings.set("respect_cursor_position", False) 135 | 136 | lines = [ 137 | ' Lorem ipsum dolor sit amet', 138 | 'Lorem ipsum dolor sit amet', 139 | ' Lorem ipsum dolor sit amet' 140 | ] 141 | 142 | starting_selection = [0, 0] 143 | ending_selection = [56, 56] 144 | 145 | self.check_command(lines, starting_selection, ending_selection) 146 | 147 | settings.set("respect_cursor_position", True) 148 | -------------------------------------------------------------------------------- /tests/test_jump_next_offset_indent.py: -------------------------------------------------------------------------------- 1 | from helper import TestHelper 2 | 3 | class TestJumpNextOffsetIndent(TestHelper): 4 | def command(self): 5 | return 'jump_next_indent' 6 | 7 | def test_positive_indent_offset(self): 8 | lines = [ 9 | 'Lorem ipsum dolor sit amet', 10 | 'Lorem ipsum dolor sit amet', 11 | '', 12 | ' Lorem ipsum dolor sit amet' 13 | ] 14 | starting_selection = [0, 0] 15 | ending_selection = [57, 57] 16 | 17 | self.check_command(lines, starting_selection, ending_selection, indent_offset = 1) 18 | 19 | def test_negative_indent_offset(self): 20 | lines = [ 21 | ' Lorem ipsum dolor sit amet', 22 | '', 23 | 'Lorem ipsum dolor sit amet', 24 | ' Lorem ipsum dolor sit amet' 25 | ] 26 | starting_selection = [2, 2] 27 | ending_selection = [30, 30] 28 | 29 | self.check_command(lines, starting_selection, ending_selection, indent_offset = -1) 30 | 31 | def test_block_skip(self): 32 | lines = [ 33 | ' Lorem ipsum dolor sit amet', 34 | 'Lorem ipsum dolor sit amet', 35 | 'Lorem ipsum dolor sit amet' 36 | ] 37 | starting_selection = [2, 2] 38 | ending_selection = [29, 29] 39 | 40 | self.check_command(lines, starting_selection, ending_selection, indent_offset = -1) 41 | 42 | def test_ignore_if_no_match(self): 43 | lines = [ 44 | 'Lorem ipsum dolor sit amet', 45 | 'Lorem ipsum dolor sit amet', 46 | ' Lorem ipsum dolor sit amet' 47 | ] 48 | starting_selection = [0, 0] 49 | ending_selection = [0, 0] 50 | 51 | self.check_command(lines, starting_selection, ending_selection, indent_offset = 1) 52 | -------------------------------------------------------------------------------- /tests/test_jump_prev_indent.py: -------------------------------------------------------------------------------- 1 | from helper import TestHelper 2 | import sublime 3 | 4 | class TestJumpPrevIndent(TestHelper): 5 | def command(self): 6 | return 'jump_prev_indent' 7 | 8 | def test_empty_lines(self): 9 | lines = [ 10 | 'Lorem ipsum dolor sit amet', 11 | '', 12 | '', 13 | 'Lorem ipsum dolor sit amet' 14 | ] 15 | starting_selection = [29, 29] 16 | ending_selection = [0, 0] 17 | 18 | self.check_command(lines, starting_selection, ending_selection) 19 | 20 | def test_indented_lines(self): 21 | lines = [ 22 | 'Lorem ipsum dolor sit amet', 23 | ' Lorem ipsum dolor sit amet', 24 | ' Lorem ipsum dolor sit amet', 25 | 'Lorem ipsum dolor sit amet' 26 | ] 27 | 28 | starting_selection = [85, 85] 29 | ending_selection = [0, 0] 30 | 31 | self.check_command(lines, starting_selection, ending_selection) 32 | 33 | def test_beginning_of_file(self): 34 | lines = [ 35 | ' Lorem ipsum dolor sit amet', 36 | ' Lorem ipsum dolor sit amet', 37 | 'Lorem ipsum dolor sit amet' 38 | ] 39 | 40 | starting_selection = [58, 58] 41 | ending_selection = [58, 58] 42 | 43 | self.check_command(lines, starting_selection, ending_selection) 44 | 45 | def test_maintain_column(self): 46 | lines = [ 47 | 'Lorem ipsum dolor sit amet', 48 | 'Lorem ipsum dolor sit amet', 49 | 'Lorem ipsum dolor sit amet' 50 | ] 51 | 52 | starting_selection = [66, 66] 53 | ending_selection = [12, 12] 54 | 55 | def test_jump_to_shorter_line(self): 56 | lines = [ 57 | 'Lorem ipsum dolor sit amet', 58 | '', 59 | 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet' 60 | ] 61 | 62 | starting_selection = [81, 81] 63 | ending_selection = [26, 26] 64 | 65 | self.check_command(lines, starting_selection, ending_selection) 66 | 67 | def test_jump_to_first_intersection(self): 68 | lines = [ 69 | ' Lorem ipsum dolor sit amet', 70 | '', 71 | ' Lorem ipsum dolor sit amet' 72 | ] 73 | 74 | starting_selection = [33, 33] 75 | ending_selection = [3, 3] 76 | 77 | self.check_command(lines, starting_selection, ending_selection) 78 | 79 | def test_create_selection(self): 80 | lines = [ 81 | 'Lorem ipsum dolor sit amet', 82 | ' Lorem ipsum dolor sit amet', 83 | ' Lorem ipsum dolor sit amet', 84 | 'Lorem ipsum dolor sit amet' 85 | ] 86 | 87 | starting_selection = [85, 85] 88 | ending_selection = [85, 0] 89 | 90 | self.check_command(lines, starting_selection, ending_selection, extend_selection = True) 91 | 92 | def test_extend_selection(self): 93 | lines = [ 94 | 'Lorem ipsum dolor sit amet', 95 | ' Lorem ipsum dolor sit amet', 96 | ' Lorem ipsum dolor sit amet', 97 | 'Lorem ipsum dolor sit amet' 98 | ] 99 | 100 | starting_selection = [85, 56] 101 | ending_selection = [85, 0] 102 | 103 | self.check_command(lines, starting_selection, ending_selection, extend_selection = True) 104 | 105 | def test_subtract_selection(self): 106 | lines = [ 107 | 'Lorem ipsum dolor sit amet', 108 | ' Lorem ipsum dolor sit amet', 109 | ' Lorem ipsum dolor sit amet', 110 | 'Lorem ipsum dolor sit amet' 111 | ] 112 | 113 | starting_selection = [0, 85] 114 | ending_selection = [0, 0] 115 | 116 | self.check_command(lines, starting_selection, ending_selection, extend_selection = True) 117 | 118 | def test_respect_cursor_position(self): 119 | lines = [ 120 | 'Lorem ipsum dolor sit amet', 121 | ' Lorem ipsum dolor sit amet', 122 | ' Lorem ipsum dolor sit amet' 123 | ] 124 | 125 | starting_selection = [57, 57] 126 | ending_selection = [1, 1] 127 | 128 | self.check_command(lines, starting_selection, ending_selection) 129 | 130 | def test_disrespect_cursor_position(self): 131 | settings = sublime.load_settings("jump_along_indent.sublime-settings") 132 | settings.set("respect_cursor_position", False) 133 | 134 | lines = [ 135 | 'Lorem ipsum dolor sit amet', 136 | ' Lorem ipsum dolor sit amet', 137 | ' Lorem ipsum dolor sit amet' 138 | ] 139 | 140 | starting_selection = [57, 57] 141 | ending_selection = [28, 28] 142 | 143 | self.check_command(lines, starting_selection, ending_selection) 144 | 145 | settings.set("respect_cursor_position", True) 146 | -------------------------------------------------------------------------------- /tests/test_jump_prev_offset_indent.py: -------------------------------------------------------------------------------- 1 | from helper import TestHelper 2 | 3 | class TestJumpPrevOffsetIndent(TestHelper): 4 | def command(self): 5 | return 'jump_prev_indent' 6 | 7 | def test_positive_indent_offset(self): 8 | lines = [ 9 | ' Lorem ipsum dolor sit amet', 10 | '', 11 | 'Lorem ipsum dolor sit amet', 12 | 'Lorem ipsum dolor sit amet' 13 | ] 14 | starting_selection = [57, 57] 15 | ending_selection = [2, 2] 16 | 17 | self.check_command(lines, starting_selection, ending_selection, indent_offset = 1) 18 | 19 | def test_negative_indent_offset(self): 20 | lines = [ 21 | ' Lorem ipsum dolor sit amet', 22 | '', 23 | 'Lorem ipsum dolor sit amet', 24 | ' Lorem ipsum dolor sit amet' 25 | ] 26 | starting_selection = [59, 59] 27 | ending_selection = [30, 30] 28 | 29 | self.check_command(lines, starting_selection, ending_selection, indent_offset = -1) 30 | 31 | def test_block_skip(self): 32 | lines = [ 33 | 'Lorem ipsum dolor sit amet', 34 | 'Lorem ipsum dolor sit amet', 35 | ' Lorem ipsum dolor sit amet' 36 | ] 37 | starting_selection = [56, 56] 38 | ending_selection = [27, 27] 39 | 40 | self.check_command(lines, starting_selection, ending_selection, indent_offset = -1) 41 | 42 | def test_ignore_if_no_match(self): 43 | lines = [ 44 | ' Lorem ipsum dolor sit amet', 45 | 'Lorem ipsum dolor sit amet', 46 | 'Lorem ipsum dolor sit amet' 47 | ] 48 | starting_selection = [58, 58] 49 | ending_selection = [58, 58] 50 | 51 | self.check_command(lines, starting_selection, ending_selection, indent_offset = 1) 52 | -------------------------------------------------------------------------------- /view_helper.py: -------------------------------------------------------------------------------- 1 | class ViewHelpers: 2 | def __init__(self, view): 3 | self.view = view 4 | 5 | def __iter__(self): 6 | for sel in self.view.sel(): 7 | yield ViewHelper(self.view, sel) 8 | 9 | 10 | class ViewHelper: 11 | def __init__(self, view, sel): 12 | self.view = view 13 | self.sel = sel 14 | 15 | def initial_xpos(self): 16 | return self.view.text_to_layout(self.initial_cursor_position())[0] 17 | 18 | def initial_cursor_position(self): 19 | return self.initial_selection().b 20 | 21 | def initial_selection(self): 22 | return self.sel 23 | 24 | def initial_column(self): 25 | return self.view.rowcol(self.initial_cursor_position())[1] 26 | 27 | def initial_row(self): 28 | return self.view.rowcol(self.initial_cursor_position())[0] 29 | 30 | def cursor_at_top_of_selection(self): 31 | return self.initial_selection().a > self.initial_selection().b 32 | 33 | def cursor_at_bottom_of_selection(self): 34 | return self.initial_selection().b > self.initial_selection().a 35 | 36 | def target_column(self, target, indent_offset): 37 | tab_size = self.view.settings().get("tab_size") 38 | offset_column = self.initial_column() + tab_size * indent_offset 39 | end_of_line = self.view.rowcol(self.find_eol(target))[1] 40 | 41 | if offset_column > end_of_line: 42 | return end_of_line 43 | else: 44 | return offset_column 45 | 46 | def find_eol(self, point): 47 | return self.view.line(point).end() 48 | 49 | def find_bol(self, point): 50 | return self.view.line(point).begin() 51 | --------------------------------------------------------------------------------