├── .gitignore ├── CHANGELOG ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── README.md ├── css_reform.py ├── funcy.py ├── messages.json ├── messages ├── 1.1.0.txt ├── 1.2.0.txt ├── 1.3.0.txt ├── 1.4.0.txt └── install.txt ├── reform.py ├── scopes.py └── viewtools.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 1.4 2 | - added inline_expr command 3 | - make select_up balance curlies 4 | - position cursor onto nearest block after delete_block 5 | - fixed python decorators selection in latest ST3dev 6 | - improved C-style lang support 7 | - some optimizations 8 | 9 | 1.3 10 | - support Java better 11 | - delete_block will now delete commented block first 12 | - don't select part of a function when on cursor on decorator 13 | - don't select comment in prev line with scope_up 14 | - change encall command to capture other calls 15 | - prefix palette commands with "Reform: " 16 | - removed deprecated select_block command 17 | - fixed python function selection for last sublime 18 | 19 | 1.2.0 20 | - add expand_next_word command 21 | - add select_scope_words command 22 | - made select_scope_up select comment blocks and line-separated one 23 | - made smart_* hit blocks 24 | - added def_* commands to jump by declarations 25 | - better C# support 26 | - fixed selecting decorators with function def in python 27 | - fixed scope up for non programming language sources 28 | - fixed selecting last function in ST2 29 | 30 | 1.1.0 31 | - added select_scope_up command 32 | - added select_scope_down command 33 | - better function detection in JavaScript 34 | - support non balanced {} in comments and strings 35 | - added PHP support for extract_expr (Bailey Parker) 36 | - added plugin commands to panel 37 | - smart_up/down for text/markdown/rst will jump by paragraphs 38 | 39 | 1.0.3 40 | - better install message 41 | 42 | 1.0.2 43 | - support Sublime Text 2 44 | 45 | 1.0.1 46 | - added install message 47 | 48 | 1.0.0 49 | Initial version 50 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+up"], "command": "find_word_up" }, 3 | { "keys": ["ctrl+down"], "command": "find_word_down" }, 4 | { "keys": ["alt+up"], "command": "def_up" }, 5 | { "keys": ["alt+down"], "command": "def_down" }, 6 | { "keys": ["alt+["], "command": "smart_up" }, 7 | { "keys": ["alt+]"], "command": "smart_down" }, 8 | 9 | { "keys": ["ctrl+alt+/"], "command": "move_word_right" }, 10 | { "keys": ["ctrl+alt+."], "command": "move_word_left" }, 11 | { "keys": ["ctrl+alt+;"], "command": "move_block_up" }, 12 | { "keys": ["ctrl+alt+'"], "command": "move_block_down" }, 13 | 14 | { "keys": ["ctrl+alt+d"], "command": "delete_block" }, 15 | { "keys": ["alt+enter"], "command": "extract_expr", "context": [ 16 | { "key": "setting.is_widget", "operator": "equal", "operand": false} 17 | ]}, 18 | { "keys": ["alt+="], "command": "inline_expr"}, 19 | 20 | { "keys": ["alt+d"], "command": "expand_next_word" }, 21 | { "keys": ["alt+shift+d"], "command": "select_scope_words" }, 22 | 23 | { "keys": ["ctrl+shift+;"], "command": "select_scope_up" }, 24 | { "keys": ["ctrl+shift+'"], "command": "select_scope_down" } 25 | ] 26 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+up"], "command": "find_word_up" }, 3 | { "keys": ["ctrl+down"], "command": "find_word_down" }, 4 | { "keys": ["alt+up"], "command": "def_up" }, 5 | { "keys": ["alt+down"], "command": "def_down" }, 6 | { "keys": ["alt+["], "command": "smart_up" }, 7 | { "keys": ["alt+]"], "command": "smart_down" }, 8 | 9 | { "keys": ["ctrl+alt+/"], "command": "move_word_right" }, 10 | { "keys": ["ctrl+alt+."], "command": "move_word_left" }, 11 | { "keys": ["ctrl+alt+;"], "command": "move_block_up" }, 12 | { "keys": ["ctrl+alt+'"], "command": "move_block_down" }, 13 | 14 | { "keys": ["ctrl+alt+d"], "command": "delete_block" }, 15 | { "keys": ["alt+enter"], "command": "extract_expr", "context": [ 16 | { "key": "setting.is_widget", "operator": "equal", "operand": false} 17 | ]}, 18 | 19 | { "keys": ["alt+d"], "command": "expand_next_word" }, 20 | { "keys": ["alt+shift+d"], "command": "select_scope_words" }, 21 | 22 | { "keys": ["ctrl+shift+;"], "command": "select_scope_up" }, 23 | { "keys": ["ctrl+shift+'"], "command": "select_scope_down" } 24 | ] 25 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+up"], "command": "find_word_up" }, 3 | { "keys": ["ctrl+down"], "command": "find_word_down" }, 4 | { "keys": ["alt+up"], "command": "def_up" }, 5 | { "keys": ["alt+down"], "command": "def_down" }, 6 | { "keys": ["alt+["], "command": "smart_up" }, 7 | { "keys": ["alt+]"], "command": "smart_down" }, 8 | 9 | { "keys": ["ctrl+alt+/"], "command": "move_word_right" }, 10 | { "keys": ["ctrl+alt+."], "command": "move_word_left" }, 11 | { "keys": ["ctrl+alt+;"], "command": "move_block_up" }, 12 | { "keys": ["ctrl+alt+'"], "command": "move_block_down" }, 13 | 14 | { "keys": ["ctrl+alt+d"], "command": "delete_block" }, 15 | { "keys": ["alt+enter"], "command": "extract_expr", "context": [ 16 | { "key": "setting.is_widget", "operator": "equal", "operand": false} 17 | ]}, 18 | 19 | { "keys": ["alt+d"], "command": "expand_next_word" }, 20 | { "keys": ["alt+shift+d"], "command": "select_scope_words" }, 21 | 22 | { "keys": ["ctrl+shift+;"], "command": "select_scope_up" }, 23 | { "keys": ["ctrl+shift+'"], "command": "select_scope_down" } 24 | ] 25 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Reform: Find Word Up", 4 | "command": "find_word_up" 5 | }, 6 | { 7 | "caption": "Reform: Find Word Down", 8 | "command": "find_word_down" 9 | }, 10 | { 11 | "caption": "Reform: Smart Up", 12 | "command": "smart_up" 13 | }, 14 | { 15 | "caption": "Reform: Smart Down", 16 | "command": "smart_down" 17 | }, 18 | { 19 | "caption": "Reform: Move Word Right", 20 | "command": "move_word_right" 21 | }, 22 | { 23 | "caption": "Reform: Move Word Left", 24 | "command": "move_word_left" 25 | }, 26 | 27 | { 28 | "caption": "Reform: Move Block Up", 29 | "command": "move_block_up" 30 | }, 31 | { 32 | "caption": "Reform: Move Block Down", 33 | "command": "move_block_down" 34 | }, 35 | { 36 | "caption": "Reform: Delete Block", 37 | "command": "delete_block" 38 | }, 39 | 40 | { 41 | "caption": "Reform: Select Scope Up", 42 | "command": "select_scope_up" 43 | }, 44 | { 45 | "caption": "Reform: Select Scope Down", 46 | "command": "select_scope_down" 47 | }, 48 | { 49 | "caption": "Reform: Extract Expression", 50 | "command": "extract_expr" 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reform 2 | 3 | [![Join the chat at https://gitter.im/Suor/sublime-reform](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Suor/sublime-reform?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | This thing enables you to move through and reform your code like magic. 6 | At least it aims to do it :) 7 | 8 | Here is a list of supported commands: 9 | 10 | Command | Key* | Description 11 | ----------------- | ------------ | --------------------------------------------------------------- 12 | find_word_up | ctrl+up | Jump to previous occurrence of a word at cursor 13 | find_word_down | ctrl+down | Jump to next occurrence of a word at cursor 14 | def_up | alt+up | Jump to previous function or class declaration 15 | def_down | alt+down | Jump to next function or class declaration 16 | smart_up | alt+[ | Jump to previous declaration or block2 17 | smart_down | alt+] | Jump to next declaration or block2 18 | move_word_right | ctrl+alt+/ | Swap word at cursor with a next one 19 | move_word_left | ctrl+alt+. | Swap word at cursor with a previous one 20 | move_block_up | ctrl+alt+; | Swap block with a previous one 21 | move_block_down | ctrl+alt+' | Swap block with a next one 22 | expand_next_word | alt+d | Expand selection to next word matching one at cursor1 23 | select_scope_words| alt+shift+d | Select words in function scope matching word at cursor1,3 24 | select_scope_up | ctrl+shift+; | Select block2/function/class at cursor, select enclosing one on next hit3 25 | select_scope_down | ctrl+shift+' | Undo last select_scope_up 26 | delete_block | ctrl+alt+d | Delete block at cursor with appropriate adjusting empty lines 27 | extract_expr | alt+enter | Extract selected expression into an assignment4 28 | inline_expr | alt+= | Inline variable defined on line at cursor 29 | 30 | 31 | * Current key bindings are very experimental, especially on OS X.
32 | 1 Matches only whole words, case-sensitive, comments and strings are skipped.
33 | 2 Block is a adjacent commented lines or a blob of text surrounded with empty lines.
34 | 3 Works for python, js, plain text. Tries to work for other languages.
35 | 4 Works for python, js, ruby, php (and any languages with no keyword to define var).
36 | 37 | 38 | ## Installation 39 | 40 | - [Install Package Control](https://sublime.wbond.net/installation). 41 | - Bring up the Command Palette with Ctrl+Shift+p (Cmd+Shift+p on OS X). 42 | - Select "Package Control: Install Package" (it'll take a few seconds). 43 | - Select or type in "Reform" when the list appears. 44 | 45 | 46 | ## TODO 47 | 48 | I have plans. Here is a list if you want to help and looking where to start: 49 | 50 | - Move functions up and down. 51 | - Better select words in scope: expand to next scope on subsequent hit, autodetect name scope. 52 | - Break long lines. 53 | - Break long strings, several variants including switching to multiline separators. 54 | - Reform dicts (object literals) from one-line to multi-line and back. 55 | - Same for calls, calls with keyword arguments, array literals. 56 | - Reform multiline list, set, dict comprehensions and generator expressions. 57 | - Align =, =>, :, \ and other punctuation 58 | - Switch brackets, parentheses, whatever. 59 | - Move blocks respecting functions. 60 | 61 | Also, support for more programming languages for language-dependent commands will help. 62 | -------------------------------------------------------------------------------- /css_reform.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sublime, sublime_plugin 3 | 4 | # TODO: 5 | # - don't spoil anything when out of scope 6 | # - preserve cursor position 7 | # - support css comments 8 | # - support css hacks 9 | # - text-wrapping variant 10 | 11 | class CssReformCommand(sublime_plugin.TextCommand): 12 | def run(self, edit): 13 | sel = self.view.sel() 14 | pos = sel[0].begin() 15 | 16 | # word = self.view.word(pos) 17 | # sublime.message_dialog(self.view.substr(word)) 18 | # return 19 | 20 | scope = find_scope(self.view, pos) 21 | if not scope: 22 | return 23 | 24 | props_scope = step_in(scope) 25 | css = self.view.substr(props_scope) 26 | props = parse_props(css) 27 | 28 | if in_one_line(self.view, scope): 29 | indent = self.view.settings().get('tab_size', 4) 30 | new_css = column_props(props, indent) 31 | else: 32 | new_css = line_props(props) 33 | self.view.replace(edit, scope, new_css) 34 | 35 | sel.clear() 36 | sel.add(props_scope.begin()) 37 | 38 | 39 | def column_props(props, indent): 40 | template = ' ' * indent + '%s: %s;\n' 41 | css = ''.join(template % pair for pair in props) 42 | return '{\n%s}' % css 43 | 44 | def line_props(props): 45 | css = '; '.join('%s: %s' % pair for pair in props) 46 | return '{%s}' % css 47 | 48 | def parse_props(css): 49 | # NOTE: wastes anything unmatched, can miss some text, 50 | # css comments also work weird 51 | """ 52 | css = (comment | rule | junk)* 53 | comment = "/*" .* "*/" 54 | rule = name ":" values (";" | $ | "/*") 55 | name = \S+ 56 | values = (comment | value)+ 57 | value = ??? 58 | junk = \S+ 59 | """ 60 | return re.findall(r'([\w-]+)\s*:\s*([^;}]+?)\s*(?:[;}]|$)', css) 61 | 62 | 63 | def in_one_line(view, region): 64 | begin_rc = view.rowcol(region.begin()) 65 | end_rc = view.rowcol(region.end()) 66 | return begin_rc[0] == end_rc[0] 67 | 68 | def step_in(region): 69 | return sublime.Region(region.begin() + 1, region.end() - 1) 70 | 71 | 72 | def find_scope(view, pos): 73 | # BUG: works weird when not in scope 74 | start = find_back(view, pos, '{') 75 | end = find_forward(view, pos, r'\}') 76 | if not start or not end: 77 | return None 78 | return sublime.Region(start, end + 1) 79 | 80 | # NOTE: these two functions are asymmetric, watch out! 81 | def find_forward(view, pos, pattern): 82 | result = view.find(pattern, pos) 83 | return result.begin() if result else None 84 | 85 | def find_back(view, pos, s): 86 | to_pos = sublime.Region(0, pos) 87 | text = view.substr(to_pos) 88 | result_pos = text.rfind(s) 89 | return None if result_pos < 0 else result_pos 90 | 91 | 92 | -------------------------------------------------------------------------------- /funcy.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | from operator import methodcaller 4 | from itertools import tee, chain, islice 5 | try: 6 | from itertools import izip as zip 7 | except ImportError: 8 | pass 9 | 10 | 11 | ### funcy seqs 12 | 13 | def first(seq): 14 | return next(iter(seq), None) 15 | 16 | def last(seq): 17 | try: 18 | return seq[-1] 19 | except IndexError: 20 | return None 21 | except TypeError: 22 | item = None 23 | for item in seq: 24 | pass 25 | return item 26 | 27 | 28 | def nth(n, seq): 29 | """Returns nth item in the sequence or None if no such item exists.""" 30 | try: 31 | return seq[n] 32 | except IndexError: 33 | return None 34 | except TypeError: 35 | return next(islice(seq, n, None), None) 36 | 37 | 38 | def remove(pred, seq): 39 | return filter(complement(pred), seq) 40 | 41 | def lremove(pred, seq): 42 | return list(remove(pred, seq)) 43 | 44 | 45 | def without(seq, *items): 46 | for value in seq: 47 | if value not in items: 48 | yield value 49 | 50 | def lwithout(seq, *items): 51 | return list(without(seq, *items)) 52 | 53 | def pairwise(seq): 54 | a, b = tee(seq) 55 | next(b, None) 56 | return zip(a, b) 57 | 58 | def with_next(seq, fill=None): 59 | a, b = tee(seq) 60 | next(b, None) 61 | return zip(a, chain(b, [fill])) 62 | 63 | 64 | def count_reps(seq): 65 | """Counts number occurrences of each value in the sequence.""" 66 | result = defaultdict(int) 67 | for item in seq: 68 | result[item] += 1 69 | return result 70 | 71 | 72 | ### funcy strings 73 | 74 | def _make_getter(regex): 75 | if regex.groups == 0: 76 | return methodcaller('group') 77 | elif regex.groups == 1 and regex.groupindex == {}: 78 | return methodcaller('group', 1) 79 | elif regex.groupindex == {}: 80 | return methodcaller('groups') 81 | elif regex.groups == len(regex.groupindex): 82 | return methodcaller('groupdict') 83 | else: 84 | return identity 85 | 86 | _re_type = type(re.compile(r'')) 87 | 88 | def _prepare(regex, flags): 89 | if not isinstance(regex, _re_type): 90 | regex = re.compile(regex, flags) 91 | return regex, _make_getter(regex) 92 | 93 | 94 | def re_all(regex, s, flags=0): 95 | return list(re_iter(regex, s, flags)) 96 | 97 | def re_find(regex, s, flags=0): 98 | return re_finder(regex, flags)(s) 99 | 100 | def re_test(regex, s, flags=0): 101 | return re_tester(regex, flags)(s) 102 | 103 | 104 | def re_finder(regex, flags=0): 105 | regex, getter = _prepare(regex, flags) 106 | return lambda s: iffy(getter)(regex.search(s)) 107 | 108 | def re_tester(regex, flags=0): 109 | return lambda s: bool(re.search(regex, s, flags)) 110 | 111 | 112 | ### funcy funcs 113 | 114 | EMPTY = object() 115 | 116 | def identity(x): 117 | return x 118 | 119 | def complement(func): 120 | return lambda *a, **kw: not func(*a, **kw) 121 | 122 | def iffy(pred, action=EMPTY, default=identity): 123 | if action is EMPTY: 124 | return iffy(bool, pred) 125 | else: 126 | return lambda v: action(v) if pred(v) else \ 127 | default(v) if callable(default) else \ 128 | default 129 | 130 | 131 | ### types tests 132 | 133 | def isa(*types): 134 | return lambda x: isinstance(x, types) 135 | 136 | from collections import Iterable 137 | iterable = isa(Iterable) 138 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt", 3 | "1.1.0": "messages/1.1.0.txt", 4 | "1.2.0": "messages/1.2.0.txt", 5 | "1.3.0": "messages/1.3.0.txt", 6 | "1.4.0": "messages/1.4.0.txt" 7 | } 8 | -------------------------------------------------------------------------------- /messages/1.1.0.txt: -------------------------------------------------------------------------------- 1 | Hey, you just got some Reform improvements: 2 | 3 | - select_func command is replaced with select_scope_up, 4 | which selects enclosing functions and classes on subsequent hits. 5 | - new select_scope_down command to undo last select_scope_up. 6 | - much better support for JavaScript functions for smart move and select commands. 7 | - PHP support to extract_expr command, thanks to Bailey Parker. 8 | 9 | 10 | You can see full changelog here: 11 | 12 | https://github.com/Suor/sublime-reform/blob/master/CHANGELOG 13 | 14 | 15 | And full list of available commands and key bindings here: 16 | 17 | https://github.com/Suor/sublime-reform 18 | -------------------------------------------------------------------------------- /messages/1.2.0.txt: -------------------------------------------------------------------------------- 1 | New and improved things just got installed: 2 | 3 | - new expand_next_word command is basically an improved version of Ctrl+D/Cmd+D. 4 | Next word matching the one at cursor will be added to selection on each hotkey 5 | (Alt+D) hit, this respects word boundaries, case, comments and strings. 6 | 7 | - select_scope_words (Alt+Shift+D) will select all words in current function 8 | scope matching the one at cursor. Word boundaries, case, comments and strings 9 | are respected. 10 | 11 | - select_scope_up will now select blocks of comments and line-separated blocks 12 | and generally became smater. It's now useful for non-code sources too. 13 | Remember - just hit Ctrl+Shift+; more to select enclosing scopes. 14 | 15 | - smart_up and smart_down commands now also visit blocks and were rebinded to 16 | Alt+[ and Alt+]. Old hotkeys now refer to def_up and def_down commands and 17 | behave the same. 18 | 19 | Reform also got better Python and C# support and other minor enhancements. 20 | 21 | 22 | Full list of available commands and key bindings is here: 23 | 24 | https://github.com/Suor/sublime-reform 25 | -------------------------------------------------------------------------------- /messages/1.3.0.txt: -------------------------------------------------------------------------------- 1 | New and improved things just got installed: 2 | 3 | - smarter smart selects with comments and python decorators 4 | - use delete_block now to easily delete commented block 5 | - encall now captures other calls 6 | 7 | Reform got better Java support and other minor enhancements. 8 | 9 | 10 | Full list of available commands and key bindings is here: 11 | 12 | https://github.com/Suor/sublime-reform 13 | -------------------------------------------------------------------------------- /messages/1.4.0.txt: -------------------------------------------------------------------------------- 1 | New and improved Reform just got installed: 2 | 3 | - added inline_expr command to inline expression (Alt+=) 4 | - balance curlies in smart select 5 | - smarter block deletion 6 | - fixed Python decorators support in latest ST3dev 7 | 8 | Also got better C-style langs support and some optimizations. 9 | 10 | 11 | Full list of available commands and key bindings is here: 12 | 13 | https://github.com/Suor/sublime-reform 14 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | This enables you to move through and reform your code like magic. 2 | At least it aims to do it :) 3 | 4 | See full list of available commands and key bindings here: 5 | 6 | https://github.com/Suor/sublime-reform 7 | -------------------------------------------------------------------------------- /reform.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sublime, sublime_plugin 3 | import re 4 | 5 | try: 6 | from .funcy import * 7 | from .viewtools import ( 8 | cursor_pos, list_cursors, set_cursor, 9 | set_selection, 10 | source, 11 | 12 | word_at, word_b, word_f, 13 | block_at, list_blocks, 14 | region_at, region_b, region_f, 15 | swap_regions, 16 | expand_min_gap, 17 | ) 18 | except ValueError: # HACK: for ST2 compatability 19 | from funcy import * 20 | from viewtools import ( 21 | cursor_pos, list_cursors, set_cursor, 22 | set_selection, 23 | source, 24 | 25 | word_at, word_b, word_f, 26 | block_at, list_blocks, 27 | region_at, region_b, region_f, 28 | swap_regions, 29 | expand_min_gap, 30 | ) 31 | 32 | 33 | ### Word commands 34 | 35 | # TODO: alter word boundaries on context, 36 | # e.g. if it's a css class then "-"" should be word symbol 37 | class FindWordDownCommand(sublime_plugin.TextCommand): 38 | def run(self, edit): 39 | pos = cursor_pos(self.view) 40 | region = word_at(self.view, pos) 41 | if not region: 42 | return 43 | word = self.view.substr(region) 44 | 45 | all_regions = self.view.find_all(r'\b%s\b' % word) 46 | next_region = region_f(all_regions, region.end()) or first(all_regions) 47 | 48 | set_cursor(self.view, next_region.begin()) 49 | 50 | 51 | class FindWordUpCommand(sublime_plugin.TextCommand): 52 | def run(self, edit): 53 | pos = cursor_pos(self.view) 54 | region = word_at(self.view, pos) 55 | if not region: 56 | return 57 | word = self.view.substr(region) 58 | 59 | all_regions = self.view.find_all(r'\b%s\b' % word) 60 | next_region = region_b(all_regions, region.begin() - 1) or last(all_regions) 61 | 62 | set_cursor(self.view, next_region.begin()) 63 | 64 | 65 | class MoveWordRightCommand(sublime_plugin.TextCommand): 66 | def run(self, edit): 67 | # We go from right to left to correctly handle overlapping regions 68 | for pos in reversed(list_cursors(self.view)): 69 | word1 = word_at(self.view, pos) 70 | word2 = word_f(self.view, pos) 71 | swap_regions(self.view, edit, word1, word2) 72 | 73 | 74 | class MoveWordLeftCommand(sublime_plugin.TextCommand): 75 | def run(self, edit): 76 | for pos in list_cursors(self.view): 77 | word1 = word_at(self.view, pos) 78 | word2 = word_b(self.view, word1.begin()) 79 | swap_regions(self.view, edit, word2, word1) 80 | 81 | 82 | ### Block commands 83 | 84 | class MoveBlockUpCommand(sublime_plugin.TextCommand): 85 | def run(self, edit): 86 | blocks = list_blocks(self.view) 87 | this_block = region_at(blocks, cursor_pos(self.view)) 88 | if not this_block: 89 | return 90 | prev_block = region_b(blocks, this_block.begin() - 1) 91 | if not prev_block: 92 | return 93 | 94 | swap_regions(self.view, edit, prev_block, this_block) 95 | self.view.show(prev_block) 96 | 97 | 98 | class MoveBlockDownCommand(sublime_plugin.TextCommand): 99 | def run(self, edit): 100 | blocks = list_blocks(self.view) 101 | this_block = region_at(blocks, cursor_pos(self.view)) 102 | if not this_block: 103 | return 104 | next_block = region_f(blocks, this_block.end()) 105 | if not next_block: 106 | return 107 | 108 | swap_regions(self.view, edit, this_block, next_block) 109 | self.view.show(next_block) 110 | 111 | 112 | ### Other commands 113 | 114 | class EncallCommand(sublime_plugin.TextCommand): 115 | def run(self, edit): 116 | new_sels = [] 117 | for s in self.view.sel(): 118 | line = self.view.line(s.b) 119 | line_str = self.view.substr(line) 120 | m = match_around(r'[\w\.]+', line_str, s.b - line.begin()) 121 | if m: 122 | r = sublime.Region(m[0] + line.begin(), m[1] + line.begin()) 123 | if self.view.substr(r.end()) == '(': 124 | closing = find_matching_paren(self.view, sublime.Region(r.end(), r.end()+1)) 125 | r = r.cover(closing) 126 | s = self.view.substr(r) 127 | self.view.replace(edit, r, '(%s)' % s) 128 | new_sels.append(r.begin()) 129 | 130 | set_selection(self.view, new_sels) 131 | 132 | from .scopes import is_escaped 133 | 134 | def find_matching_paren(view, paren): 135 | count = 1 136 | while count > 0 and paren.a != -1: 137 | paren = view.find(r'[()]', paren.b) 138 | if not is_escaped(view, paren.a): 139 | if view.substr(paren) == '(': 140 | count += 1 141 | else: 142 | count -= 1 143 | return paren 144 | 145 | 146 | class ExtractExprCommand(sublime_plugin.TextCommand): 147 | TEMPLATES = { 148 | 'js': ('let = {0};\n', 4), 149 | 'php': (' = {0};\n', 0), 150 | 'nut': ('local = {0};\n', 6), 151 | } 152 | DEFAULT = (' = {0}\n', 0) 153 | 154 | def run(self, edit): 155 | # Get expression 156 | sel = self.view.sel()[0] 157 | pos = sel.begin() 158 | expr = self.view.substr(sel) 159 | 160 | # Prepare new line 161 | line = self.view.line(pos) 162 | line_str = self.view.substr(line) 163 | prefix = re_find(r'^\s*', line_str) 164 | template, cursor_shift = self.TEMPLATES.get(source(self.view, pos), self.DEFAULT) 165 | exracted_line = prefix + template.format(expr) 166 | 167 | # Modify text 168 | self.view.insert(edit, line.begin(), exracted_line) 169 | 170 | # Create cursor for name 171 | name_pos = line.begin() + len(prefix) + cursor_shift 172 | self.view.sel().add(sublime.Region(name_pos, name_pos)) 173 | 174 | 175 | _re_type = type(re.compile(r'')) 176 | 177 | def match_around(regex, s, pos): 178 | if not isinstance(regex, _re_type): 179 | regex = re.compile(regex) 180 | 181 | p = 0 182 | m = None 183 | while p <= pos: 184 | m = regex.search(s, p) 185 | if not m or m.start() <= pos <= m.end(): 186 | break 187 | else: 188 | p = m.end() 189 | 190 | return (m.start(), m.end()) if m else None 191 | -------------------------------------------------------------------------------- /scopes.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | ST3 = sublime.version() >= '3000' 3 | 4 | from functools import partial, reduce 5 | from itertools import takewhile 6 | 7 | try: 8 | from .funcy import * 9 | from .viewtools import * 10 | except ValueError: # HACK: for ST2 compatability 11 | from funcy import * 12 | from viewtools import * 13 | 14 | 15 | class TestCommandCommand(sublime_plugin.WindowCommand): 16 | def run(self): 17 | pass 18 | 19 | 20 | class ScopesTestCommand(sublime_plugin.TextCommand): 21 | def run(self, edit): 22 | print("test") 23 | # print(list_defs(self.view)) 24 | 25 | # scopes = [_expand_def(view, adef) for adef in list_defs(view)] 26 | scopes = list_func_defs(self.view) 27 | set_selection(self.view, scopes) 28 | return 29 | 30 | pos = cursor_pos(self.view) 31 | scope = region_b(scopes, pos) 32 | print(scope) 33 | scope = _expand_def(self.view, scope) 34 | set_selection(self.view, [scope]) 35 | # # from .viewtools import word_f, cursor_pos 36 | # # set_selection(self.view, word_f(self.view, cursor_pos(self.view))) 37 | # # print(self.view.find_by_selector('meta.function')) 38 | # # block = scope_up(self.view, self.view.sel()[0]) 39 | # # set_selection(self.view, block) 40 | # # set_selection(self.view, list_defs(self.view)) 41 | # # ls = self.view.find_by_class(pos, False, sublime.CLASS_LINE_START) 42 | # ls = newline_f(self.view, pos) 43 | # set_selection(self.view, ls) 44 | 45 | 46 | class SmartUpCommand(sublime_plugin.TextCommand): 47 | def run(self, edit): 48 | regions = order_regions(list_defs(self.view) + list_blocks(self.view)) 49 | map_selection(self.view, partial(smart_up, regions)) 50 | 51 | class SmartDownCommand(sublime_plugin.TextCommand): 52 | def run(self, edit): 53 | regions = order_regions(list_defs(self.view) + list_blocks(self.view)) 54 | map_selection(self.view, partial(smart_down, regions)) 55 | 56 | class DefUpCommand(sublime_plugin.TextCommand): 57 | def run(self, edit): 58 | # TODO: jump by selectors in css/less/... 59 | regions = list_defs(self.view) or list_blocks(self.view) 60 | map_selection(self.view, partial(smart_up, regions)) 61 | 62 | class DefDownCommand(sublime_plugin.TextCommand): 63 | def run(self, edit): 64 | regions = list_defs(self.view) or list_blocks(self.view) 65 | map_selection(self.view, partial(smart_down, regions)) 66 | 67 | def smart_up(regions, current): 68 | target = region_b(regions, current.begin() - 1) or last(regions) 69 | return target.begin() 70 | 71 | def smart_down(regions, current): 72 | target = region_f(regions, current.end()) or first(regions) 73 | return target.begin() 74 | 75 | 76 | class DeleteBlockCommand(sublime_plugin.TextCommand): 77 | def run(self, edit): 78 | pos = cursor_pos(self.view) 79 | comments_block = comments_block_at(self.view, pos) 80 | # Skip one line comments 81 | if comments_block and self.view.substr(comments_block).count('\n') == 1: 82 | comments_block = None 83 | block = comments_block or block_at(self.view, pos) 84 | 85 | ex_block = expand_min_gap(self.view, block) 86 | new_pos = ex_block.begin() - 1 if ex_block.begin() < block.begin() else ex_block.end() 87 | set_cursor(self.view, new_pos) 88 | self.view.erase(edit, ex_block) 89 | 90 | 91 | class ExpandNextWordCommand(sublime_plugin.TextCommand): 92 | def run(self, edit): 93 | region = self.view.sel()[-1] 94 | if region.empty(): 95 | region = word_at(self.view, region.a) 96 | if region: 97 | self.view.sel().add(region) 98 | return 99 | 100 | words = get_words(self.view, region) 101 | 102 | # filter out already selected words 103 | begins = set(r.begin() for r in self.view.sel()) 104 | words = [w for w in words if w.begin() not in begins] 105 | 106 | next_word = first(r for r in words if r.begin() > region.end()) or first(words) 107 | if next_word: 108 | self.view.sel().add(next_word) 109 | self.view.show(next_word) 110 | 111 | class SelectScopeWordsCommand(sublime_plugin.TextCommand): 112 | def run(self, edit): 113 | # TODO: make this not include decorators in python 114 | region = self.view.sel()[-1] 115 | if region.empty(): 116 | region = word_at(self.view, region.a) 117 | if not region: 118 | return 119 | 120 | words = get_words(self.view, region) 121 | 122 | # filter by scope 123 | scope = scope_at(self.view, region.end()) or block_at(self.view, region.end()) 124 | words = [w for w in words if scope.contains(w)] 125 | 126 | set_selection(self.view, words) 127 | 128 | def get_words(view, region): 129 | word = view.substr(region) 130 | words = view.find_all(r'\b%s\b' % word) 131 | 132 | # filter out words in strings and comments 133 | allow_escaped = any(is_escaped(view, r.begin()) for r in view.sel()) 134 | if not allow_escaped: 135 | words = [w for w in words if not is_escaped(view, w.a)] 136 | 137 | return words 138 | 139 | 140 | class InlineExprCommand(sublime_plugin.TextCommand): 141 | def run(self, edit): 142 | sel = self.view.sel()[0] 143 | pos = sel.begin() 144 | line = self.view.line(pos) 145 | line_str = self.view.substr(line) 146 | 147 | try: 148 | var, expr = re_find(r'^\s*(?:\w+\s+)?(\w+)\s*=\s*(.*)$', line_str) 149 | expr = expr.rstrip(';') 150 | except TypeError: 151 | return 152 | scope = scope_at(self.view, pos) or sublime.Region(0, self.view.size()) 153 | scope = scope.intersection(sublime.Region(pos, self.view.size())) 154 | var_regions = self.view.find_all(r'\b%s\b' % var) 155 | var_regions = [r for r in var_regions if scope.contains(r)] 156 | for r in reversed(var_regions): 157 | if not scope_re(self.view, r.begin(), '^(comment|string|variable\.parameter)'): 158 | self.view.replace(edit, r, expr) 159 | self.view.erase(edit, self.view.full_line(pos)) 160 | 161 | 162 | class SelectScopeUpCommand(sublime_plugin.TextCommand): 163 | def run(self, edit): 164 | if not hasattr(self.view, '_selection_stack'): 165 | self.view._selection_stack = [] 166 | 167 | # Start stack afresh if nothing is selected 168 | sel = list(self.view.sel()) 169 | if all(r.empty() for r in sel): 170 | self.view._selection_stack = [] 171 | 172 | # Save current selection 173 | self.view._selection_stack.append(sel) 174 | 175 | map_selection(self.view, partial(smart_region_up, self.view)) 176 | 177 | # If nothing changed remove dup from stack 178 | if self.view._selection_stack[-1] == self.view.sel(): 179 | self.view._selection_stack.pop() 180 | 181 | # NOTE: this is deprecated 182 | class SelectFuncCommand(SelectScopeUpCommand): 183 | pass 184 | 185 | class SelectScopeDownCommand(sublime_plugin.TextCommand): 186 | def run(self, edit): 187 | if getattr(self.view, '_selection_stack'): 188 | set_selection(self.view, self.view._selection_stack.pop()) 189 | self.view.show(self.view.sel()) 190 | 191 | 192 | def smart_region_up(view, region): 193 | comments_block = comments_block_at(view, region.b) 194 | block = smart_block_at(view, region.begin()) 195 | scope = scope_up(view, region) 196 | 197 | if comments_block and not region.contains(comments_block): 198 | return comments_block 199 | elif block and not region.contains(block) and (not scope or scope.a < block.a): 200 | return block 201 | else: 202 | return scope or region 203 | 204 | def comments_block_at(view, pos): 205 | def grab_empty_line_start(region): 206 | line_start = view.line(region).a 207 | space = view.find(r'[ \t]+', line_start) 208 | if space and space.b == region.a: 209 | return region.cover(space) 210 | else: 211 | return region 212 | 213 | clines = list(map(grab_empty_line_start, view.find_by_selector('comment'))) 214 | 215 | pos = cursor_pos(view) 216 | this_line = first((i, r) for i, r in enumerate(clines) if r.contains(pos) and r.b != pos) 217 | if this_line: 218 | i, block = this_line 219 | for r in clines[i+1:]: 220 | if r.a == block.b: 221 | block = block.cover(r) 222 | else: 223 | break 224 | for r in reversed(clines[:i]): 225 | if r.b == block.a: 226 | block = block.cover(r) 227 | else: 228 | break 229 | return block 230 | 231 | 232 | def smart_block_at(view, pos): 233 | lang = source(view) 234 | block = block_at(view, pos) 235 | 236 | # Close non-pairing curlies 237 | curlies = count_curlies(view, block) 238 | if curlies > 0: 239 | curly = find_closing_curly(view, block.end(), count=curlies) 240 | if curly is not None: 241 | return block.cover(view.full_line(curly)) 242 | elif curlies < 0: 243 | curly = find_opening_curly(view, block.begin(), count=curlies) 244 | if curly is not None: 245 | return block.cover(view.full_line(curly)) 246 | return block 247 | 248 | 249 | def list_func_defs(view): 250 | lang = source(view) 251 | if lang in ('cs', 'java'): 252 | return view.find_by_selector('meta.method.identifier') 253 | 254 | # Sublime doesn't think "function() {}" (mind no space) is a func definition. 255 | # It however thinks constructor and prototype have something to do with it. 256 | if lang == 'js': 257 | # Functions in javascript are often declared in expression manner, 258 | # we add function binding to prototype or object property as part of declaration. 259 | func_def = r'([\t ]*(?:\w+ *:|(?:(?:var|let|const) +)?[\w.]+ *=) *)?\bfunction\b' 260 | raw_funcs = view.find_all(func_def) 261 | is_junk = lambda r: is_escaped(view, r.a) 262 | funcs = lremove(is_junk, raw_funcs) 263 | return funcs + view.find_by_selector('meta.class-method') 264 | 265 | if lang == "nut": 266 | return view.find_by_selector('entity.name.function') 267 | 268 | funcs = view.find_by_selector('meta.function') 269 | if lang == 'python': 270 | is_junk = lambda r: re_test(r'^(lambda|\s*\@)', view.substr(r)) 271 | funcs = lremove(is_junk, funcs) 272 | 273 | return funcs 274 | 275 | def list_class_defs(view): 276 | lang = source(view) 277 | if lang in ('cs', 'java'): 278 | return view.find_by_selector('meta.class.identifier') 279 | else: 280 | return view.find_by_selector('meta.class') 281 | 282 | def list_defs(view): 283 | funcs = list_func_defs(view) 284 | if source(view) == 'js': 285 | return funcs 286 | classes = list_class_defs(view) 287 | return order_regions(funcs + classes) 288 | 289 | 290 | def scope_up(view, region): 291 | scopes = list(scopes_up(view, region.end())) 292 | expansion = first(s for s in scopes if s != region and s.contains(region)) 293 | if expansion: 294 | return expansion 295 | if region.empty(): 296 | return first(scopes) 297 | 298 | def scope_at(view, pos): 299 | scopes = scopes_up(view, pos) 300 | return first(s for s in scopes if s.contains(pos)) 301 | 302 | def scopes_up(view, pos): 303 | for scope, upper in with_next(_scopes_up(view, pos)): 304 | yield scope 305 | if upper and not upper.contains(scope): 306 | continue 307 | 308 | def _scopes_up(view, pos): 309 | scopes = [_expand_def(view, adef) for adef in list_defs(view)] 310 | 311 | scope = region_b(scopes, pos) 312 | while scope: 313 | yield scope 314 | scope = region_b(scopes, scope.begin() - 1) 315 | 316 | 317 | def _expand_def(view, adef): 318 | lang = source(view, adef.begin()) 319 | 320 | if lang == 'python': 321 | next_line = newline_f(view, adef.end()) 322 | adef = adef.cover(view.indented_region(next_line)) 323 | prefix = re_find(r'^[ \t]*', view.substr(view.line(adef.begin()))) 324 | while True: 325 | p = line_b_begin(view, adef.begin()) 326 | if p is None: 327 | break 328 | line_b_str = view.substr(view.line(p)) 329 | if line_b_str.startswith(prefix) and re_test(r'meta.(annotation|\w+.decorator)', 330 | scope_name(view, p + len(prefix))): 331 | adef = adef.cover(sublime.Region(p, p)) 332 | else: 333 | break 334 | return adef 335 | elif lang in ('js', 'cs', 'java', 'nut'): 336 | # Extend to matching bracket 337 | start_bracket = view.find(r'{', adef.end(), sublime.LITERAL) 338 | end_bracket = find_closing_curly(view, start_bracket.b) 339 | adef = adef.cover(view.full_line(end_bracket)) 340 | 341 | # Match , or ; in case it's an expression 342 | if lang == 'js': 343 | punct = view.find(r'\s*[,;]', adef.end()) 344 | if punct and punct.a == adef.b: 345 | adef = adef.cover(punct) 346 | else: 347 | adef = adef.cover(view.line(adef.begin())) 348 | 349 | return adef 350 | else: 351 | # Heuristics based on indentation for all other languages 352 | next_line = newline_f(view, adef.end()) 353 | indented = view.indented_region(next_line) 354 | last_line = view.line(indented.end()) 355 | return adef.cover(last_line) 356 | -------------------------------------------------------------------------------- /viewtools.py: -------------------------------------------------------------------------------- 1 | """ 2 | A collection of tools to deal with scopes and text in sublime view. 3 | """ 4 | import sublime 5 | ST3 = sublime.version() >= '3000' 6 | 7 | try: 8 | from .funcy import * 9 | except ValueError: # HACK: for ST2 compatability 10 | from funcy import * 11 | 12 | 13 | ### Cursor and selection 14 | 15 | def cursor_pos(view): 16 | return view.sel()[0].b 17 | 18 | def list_cursors(view): 19 | return [s.b for s in view.sel()] 20 | 21 | def set_cursor(view, pos): 22 | if iterable(pos): 23 | regions = [sublime.Region(p, p) for p in pos] 24 | else: 25 | regions = sublime.Region(pos, pos) 26 | set_selection(view, regions) 27 | 28 | def map_selection(view, f): 29 | set_selection(view, map(f, view.sel())) 30 | 31 | def set_selection(view, region): 32 | # NOTE: we need to materialize a possible iterator beore clearing selection, 33 | # as mapping selection is a common techique. 34 | if iterable(region): 35 | region = list(region) 36 | 37 | view.sel().clear() 38 | add_selection(view, region) 39 | view.show(view.sel()) 40 | 41 | def add_selection(view, region): 42 | if iterable(region): 43 | if ST3: 44 | view.sel().add_all(list(region)) 45 | else: 46 | # .add_all() doesn't work with python lists in ST2 47 | for r in region: 48 | view.sel().add(r) 49 | else: 50 | view.sel().add(region) 51 | 52 | 53 | ### Words 54 | 55 | if ST3: 56 | def word_at(view, pos): 57 | if view.classify(pos) & (512 | sublime.CLASS_WORD_START | sublime.CLASS_WORD_END): 58 | return view.word(pos) 59 | 60 | def word_b(view, pos): 61 | start = view.find_by_class(pos - 1, False, sublime.CLASS_WORD_START) 62 | return view.word(start) 63 | 64 | def word_f(view, pos): 65 | start = view.find_by_class(pos, True, sublime.CLASS_WORD_START) 66 | return view.word(start) 67 | else: 68 | def _word_at(view, pos): 69 | word = view.word(pos) 70 | return word, re_test(r'^\w+$', view.substr(word)) 71 | 72 | def word_at(view, pos): 73 | word, is_word = _word_at(view, pos) 74 | if is_word: 75 | return word 76 | 77 | def word_b(view, pos): 78 | pos -= 1 79 | is_word = False 80 | while pos and not is_word: 81 | word, is_word = _word_at(view, pos - 1) 82 | if is_word: 83 | return word 84 | else: 85 | pos = word.begin() 86 | 87 | def word_f(view, pos): 88 | start = view.find(r'\b\w', pos + 1) 89 | return view.word(start) 90 | 91 | 92 | ### Lines 93 | 94 | def line_at(view, pos): 95 | return view.line(pos) 96 | 97 | def line_start(view, pos): 98 | line = view.line(pos) 99 | return sublime.Region(line.begin(), pos) 100 | 101 | def line_end(view, pos): 102 | line = view.line(pos) 103 | return sublime.Region(pos, line.end()) 104 | 105 | def list_lines_b(view, pos): 106 | while pos: 107 | yield view.full_line(pos) 108 | pos = view.find_by_class(pos, False, sublime.CLASS_LINE_END) 109 | 110 | def list_lines_f(view, pos): 111 | while pos < view.size(): 112 | yield view.full_line(pos) 113 | pos = view.find_by_class(pos, True, sublime.CLASS_LINE_START) 114 | 115 | if ST3: 116 | def line_b_begin(view, pos): 117 | if view.classify(pos) & sublime.CLASS_LINE_START: 118 | return newline_b(view, pos) 119 | else: 120 | return newline_b(view, newline_b(view, pos)) 121 | 122 | def newline_b(view, pos): 123 | if pos > 0: 124 | return view.find_by_class(pos, False, sublime.CLASS_LINE_START) 125 | 126 | def newline_f(view, pos): 127 | if pos < view.size(): 128 | return view.find_by_class(pos, True, sublime.CLASS_LINE_START) 129 | else: 130 | def line_b_begin(view, pos): 131 | line_start = view.line(pos).begin() 132 | return newline_b(view, min(pos, line_start)) 133 | 134 | def newline_b(view, pos): 135 | if pos > 0: 136 | return view.line(pos - 1).begin() 137 | 138 | def newline_f(view, pos): 139 | if pos < view.size(): 140 | region = view.find(r'^', pos + 1) 141 | return region.end() 142 | 143 | 144 | ### Blocks 145 | 146 | def block_at(view, pos): 147 | return region_at(list_blocks(view), pos) 148 | 149 | def block_b(view, pos): 150 | return region_b(list_blocks(view), pos) 151 | 152 | def block_f(view, pos): 153 | return region_f(list_blocks(view), pos) 154 | 155 | def list_blocks(view): 156 | empty_lines = view.find_all(r'^\s*\n') 157 | return invert_regions(view, empty_lines) 158 | 159 | 160 | ### Regions 161 | 162 | def region_at(regions, pos): 163 | return first(r for r in regions if r.begin() <= pos <= r.end()) 164 | 165 | def region_b(regions, pos): 166 | return first(r for r in reversed(regions) if r.begin() <= pos) 167 | 168 | def region_f(regions, pos): 169 | return first(r for r in regions if pos < r.begin()) 170 | 171 | 172 | def full_region(view): 173 | return sublime.Region(0, view.size()) 174 | 175 | def shifted_region(region, shift): 176 | return sublime.Region(region.a + shift, region.b + shift) 177 | 178 | 179 | def order_regions(regions): 180 | order = lambda r: (r.begin(), r.end()) 181 | return sorted(regions, key=order) 182 | 183 | def invert_regions(view, regions): 184 | # NOTE: regions should be non-overlapping and ordered, 185 | # no check here for performance reasons 186 | start = 0 187 | end = view.size() 188 | result = [] 189 | 190 | for r in regions: 191 | if r.a > start: 192 | result.append(sublime.Region(start, r.a)) 193 | start = r.b 194 | 195 | if start < end: 196 | result.append(sublime.Region(start, end)) 197 | 198 | return result 199 | 200 | def swap_regions(view, edit, region1, region2): 201 | assert region1.b <= region2.a 202 | 203 | # hide selection before move and save shifted position to set it after 204 | sel = view.sel() 205 | regions = [] 206 | for region in sel: 207 | if region1.contains(region): 208 | sel.subtract(region) 209 | regions.append(shifted_region(region, region2.b - region1.b)) 210 | elif region2.contains(region): 211 | sel.subtract(region) 212 | regions.append(shifted_region(region, region1.a - region2.a)) 213 | 214 | # swap text 215 | str1 = view.substr(region1) 216 | str2 = view.substr(region2) 217 | view.replace(edit, region2, str1) 218 | view.replace(edit, region1, str2) 219 | 220 | # set cursor position/selection to match moved regions 221 | add_selection(view, regions) 222 | 223 | def cover_regions(regions): 224 | return reduce(lambda a, b: a.cover(b), regions) 225 | 226 | 227 | ### Scope 228 | 229 | def scope_name(view, pos=None): 230 | if pos is None: 231 | pos = cursor_pos(view) 232 | return view.scope_name(pos) 233 | 234 | def parsed_scope(view, pos=None): 235 | return parse_scope(scope_name(view, pos)) 236 | 237 | def source(view, pos=None): 238 | return first(vec[1] for vec in parsed_scope(view, pos) if vec[0] == 'source') 239 | 240 | def parse_scope(scope_name): 241 | return [name.split('.') for name in scope_name.split()] 242 | 243 | def is_escaped(view, pos): 244 | return any(s[0] in ('comment', 'string') for s in parsed_scope(view, pos)) 245 | 246 | def is_comment(view, pos): 247 | return any(s[0] == 'comment' for s in parsed_scope(view, pos)) 248 | 249 | def scope_re(view, pos, pattern): 250 | scope_lines = scope_name(view, pos).split() 251 | return any(re.search(pattern, line) for line in scope_lines) 252 | 253 | 254 | ### Smarts 255 | 256 | def expand_min_gap(view, region): 257 | """ 258 | Expands region so that it will cover minimum gap of empty lines around it. 259 | """ 260 | empty_lines = view.find_all(r'^\s*\n') 261 | empty_neighbours = [r for r in empty_lines 262 | if r.end() == region.begin() or r.begin() == region.end()] 263 | 264 | if not empty_neighbours: 265 | return region 266 | elif len(empty_neighbours) == 1: 267 | if is_view_bordering(view, region): 268 | return region.cover(empty_neighbours[0]) 269 | else: 270 | return region 271 | else: 272 | min_gap = min(reversed(empty_neighbours), key=lambda r: view.substr(r).count('\n')) 273 | return region.cover(min_gap) 274 | 275 | def is_view_bordering(view, region): 276 | return region.begin() == 0 or region.end() == view.size() 277 | 278 | 279 | # String things 280 | 281 | def find_iter(view, pattern, pos_or_region): 282 | region = pos_or_region if isinstance(pos_or_region, sublime.Region) else \ 283 | sublime.Region(pos_or_region, view.size()) 284 | start, end = region.begin(), region.end() 285 | found = sublime.Region(start, start) 286 | while found.a != -1: 287 | found = view.find(pattern, found.b) 288 | if found.b > end: 289 | break 290 | if not is_escaped(view, found.a): 291 | yield found 292 | 293 | 294 | def count_curlies(view, region): 295 | curlies = count_reps(map(view.substr, find_iter(view, r'[{}]', region))) 296 | return curlies['{'] - curlies['}'] 297 | 298 | def find_closing_curly(view, pos, count=1): 299 | for curly in find_iter(view, r'[{}]', pos): 300 | count += 1 if view.substr(curly) == '{' else -1 301 | if count == 0: 302 | return curly 303 | 304 | 305 | def find_opening_curly(view, pos, count=-1): 306 | for curly in _find_iter_back(view, r'[{}]', pos): 307 | count += 1 if view.substr(curly) == '{' else -1 308 | if count == 0: 309 | return curly 310 | 311 | 312 | def _find_iter_back(view, pattern, pos): 313 | regex = re.compile(pattern) 314 | for line in _iter_lines_back(view, pos): 315 | base = line.begin() 316 | s = view.substr(line) 317 | for start, end in reversed(list(_re_iter_spans(regex, s))): 318 | if not is_escaped(view, base + start): 319 | yield sublime.Region(base + start, base + end) 320 | 321 | def _iter_lines_back(view, pos): 322 | yield line_start(view, pos) 323 | pos = line_b_begin(view, pos) 324 | while pos is not None: 325 | line = view.full_line(pos) 326 | yield line 327 | pos = newline_b(view, pos) 328 | 329 | def _re_iter_spans(pattern, s): 330 | regex = re.compile(pattern) 331 | p = 0 332 | while p < len(s): 333 | m = regex.search(s, p) 334 | if m is None: 335 | break 336 | _, e = span = m.span() 337 | yield span 338 | p = e + 1 339 | --------------------------------------------------------------------------------