├── .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 | [](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 |
--------------------------------------------------------------------------------