-
-
Notifications
You must be signed in to change notification settings - Fork 22
/
cs_indent.py
156 lines (139 loc) · 6.01 KB
/
cs_indent.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import sublime, sublime_plugin
from . import cs_common, cs_parser
def search_path(node, pos):
"""
Looks for the deepest node that wraps pos (start < pos < end).
Returns full path to that node from the top
"""
res = [node]
for child in node.children:
if child.start < pos < child.end:
res += search_path(child, pos)
elif pos < child.start:
break
return res
def indent(view, point, parsed = None):
"""
Given point, returns (tag, row, indent) for that line, where indent
is a correct indent based on the last unclosed paren before point.
Tag could be 'string' (don't change anything, we're inside string),
'top-level' (set to 0, we are at top level) or 'indent' (normal behaviour)
Row is row number of the token for which this indent is based on (row of open paren)
"""
parsed = parsed or cs_parser.parse(view.substr(sublime.Region(0, point)) + ' ')
if path := search_path(parsed, point):
node = None
first_form = None
# try finding unmatched open paren
for child in path[-1].children:
if child.start >= point:
break
if child.name == 'error' and child.text in ['(', '[', '{', '"']:
node = child
first_form = None
elif first_form is None:
first_form = child
# try indent relative to wrapping paren
if not node:
for n in reversed(path):
if n.name in ['string', 'parens', 'braces', 'brackets']:
node = n
first_form = node.body.children[0] if node.body and node.body.children else None
break
# top level
if not node:
row, _ = view.rowcol(point)
return ('top-level', row, 0)
row, col = view.rowcol(node.open.end if node.open else node.end)
offset = 0
if node.name == 'string':
return ('string', row, col)
elif node.name == 'parens' or (node.name == 'error' and node.text == '('):
# no first form -- indent to paren
if not first_form:
offset = 0
# first form is a list/map/vector -- indent to paren
elif first_form.end <= point and first_form.name in ['parens', 'braces', 'brackets']:
offset = 0
# form itself is a reader conditional
elif node.open and node.open.text in ['#?(', '#?@(']:
offset = 0
else:
offset = 1
return ('indent', row, col + offset)
def skip_spaces(view, point):
"""
Starting from point, skips as much spaces as it can without going to the new line,
and returns new point
"""
def is_space(point):
s = view.substr(sublime.Region(point, point + 1))
return s.isspace() and s not in ['\n', '\r']
while point < view.size() and is_space(point):
point = point + 1
return point
def indent_lines(view, selections, edit):
"""
Given set of sorted ranges (`selections`), indents all lines touched by those selections
"""
# Calculate all replacements first
parsed = cs_parser.parse(view.substr(sublime.Region(0, view.size())) + ' ')
replacements = {} # row -> (begin, delta_i)
for sel in selections:
for line in view.lines(sel):
begin = line.begin()
end = skip_spaces(view, begin)
# do not touch empty lines
if end == line.end():
continue
row, _ = view.rowcol(begin)
type, base_row, i = indent(view, begin, parsed)
# do not re-indent multiline strings
if type == 'string':
continue
# if we moved line before and depend on it, take that into account
_, base_delta_i = replacements.get(base_row, (0, 0))
delta_i = i - (end - begin) + base_delta_i
if delta_i != 0:
replacements[row] = (begin, delta_i)
# Now apply all replacements, recalculating begins as we go
change_id = view.change_id()
for row in replacements:
begin, delta_i = replacements[row]
begin = view.transform_region_from(sublime.Region(begin, begin), change_id).begin()
if delta_i < 0:
view.replace(edit, sublime.Region(begin, begin - delta_i), "")
else:
view.replace(edit, sublime.Region(begin, begin), " " * delta_i)
class ClojureSublimedReindentBufferOnSave(sublime_plugin.EventListener):
def on_pre_save(self, view):
if cs_common.setting("format_on_save", False) and view.syntax().name == 'Clojure (Sublimed)':
view.run_command('clojure_sublimed_reindent_buffer')
class ClojureSublimedReindentBufferCommand(sublime_plugin.TextCommand):
def run(self, edit):
view = self.view
with cs_common.Measure("Reindent Buffer {} chars", view.size()):
indent_lines(view, [sublime.Region(0, view.size())], edit)
class ClojureSublimedReindentLinesCommand(sublime_plugin.TextCommand):
def run(self, edit):
view = self.view
with cs_common.Measure("Reindent Lines {}", view.sel()):
indent_lines(view, view.sel(), edit)
class ClojureSublimedInsertNewlineCommand(sublime_plugin.TextCommand):
def run(self, edit):
view = self.view
# Calculate all replacements first
replacements = []
for sel in view.sel():
end = skip_spaces(view, sel.end())
_, _, i = indent(view, sel.begin())
replacements.append((sublime.Region(sel.begin(), end), "\n" + " " * i))
# Now apply them all at once
change_id_sel = view.change_id()
view.sel().clear()
for region, string in replacements:
region = view.transform_region_from(region, change_id_sel)
point = region.begin() + len(string)
view.replace(edit, region, string)
# Add selection at the end of newly inserted region
view.sel().add(sublime.Region(point, point))