-
-
Notifications
You must be signed in to change notification settings - Fork 21
/
Copy pathcs_indent.py
237 lines (212 loc) · 9.46 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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import collections, re
import sublime, sublime_plugin
from . import cs_cljfmt, cs_common, cs_parser, cs_printer
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 == '('):
if first_form and cs_parser.is_symbol(first_form):
offset = 1
else:
offset = 0
return ('indent', row, col + offset)
def newline_indent(view, point):
return indent(view, point)[2]
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
delta_total = 0
for row in replacements:
begin, delta_i = replacements[row]
begin = begin + delta_total
delta_total += delta_i
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 ('Clojure' in view.syntax().name or 'EDN' in view.syntax().name):
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()):
if 'cljfmt' == cs_common.setting('formatter'):
cs_cljfmt.indent_lines(view, [sublime.Region(0, view.size())], edit)
else:
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 {} chars", sum([r.size() for r in view.sel()])):
if 'cljfmt' == cs_common.setting('formatter'):
cs_cljfmt.indent_lines(view, view.sel(), edit)
else:
indent_lines(view, view.sel(), edit)
class ClojureSublimedReindentCommand(sublime_plugin.TextCommand):
def run(self, edit):
view = self.view
if all(r.empty() for r in view.sel()):
view.run_command('clojure_sublimed_reindent_buffer')
else:
view.run_command('clojure_sublimed_reindent_lines')
class ClojureSublimedPrettyPrintCommand(sublime_plugin.TextCommand):
def run(self, edit):
view = self.view
change_id = view.change_id()
for region in [r for r in view.sel()]:
region = view.transform_region_from(region, change_id)
if region.empty():
region = cs_parser.topmost_form(view, region.begin())
form = view.substr(region)
node = cs_parser.parse(form)
formatted = cs_printer.format(form, node, limit = cs_common.wrap_width(view))
view.replace(edit, region, formatted)
class ClojureSublimedSelectTopmostFormCommand(sublime_plugin.TextCommand):
def run(self, edit):
view = self.view
sel = view.sel()
for region in [r for r in sel]:
sel.add(cs_parser.topmost_form(view, region.begin()))
def cljfmt_indent(view, point):
i = None
try:
i = cs_cljfmt.newline_indent(view, point)
except:
pass
return newline_indent(view, point) if i is None else i
class ClojureSublimedInsertNewlineCommand(sublime_plugin.TextCommand):
def run(self, edit):
view = self.view
newline_indent_fn = cljfmt_indent if 'cljfmt' == cs_common.setting('formatter') else newline_indent
# Calculate all replacements first
replacements = []
for sel in view.sel():
end = skip_spaces(view, sel.end())
i = newline_indent_fn(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))
class ClojureSublimedAlignCursorsCommand(sublime_plugin.TextCommand):
def run(self, edit):
view = self.view
by_row = collections.defaultdict(list)
for region in view.sel():
row, col = view.rowcol(region.a)
by_row[row].append(region)
# print('by_row', by_row)
cols = max(len(line_regions) for line_regions in by_row.values())
# print('cols', cols)
change_id = view.change_id()
# upd = lambda r: view.transform_region_from(r, change_id)
for col in range(0, cols):
col_regions = [line_regions[col] for line_regions in by_row.values() if len(line_regions) > col]
col_regions = [view.transform_region_from(r, change_id) for r in col_regions]
col_regions.sort(key = lambda r: view.rowcol(r.a)[0])
# print('col', col, col_regions)
max_col = max(view.rowcol(r.a)[1] for r in col_regions)
max_len = max(r.size() for r in col_regions)
# print("max_col", max_col, "max_len", max_len)
change_id_2 = view.change_id()
for r in col_regions:
r = view.transform_region_from(r, change_id_2)
_, col = view.rowcol(r.begin())
length = r.size()
prepend = max_col - col
append = max_len - length
# print("r", r, "col", col, "len", length, "left", max_col - col, "right", max_len - length)
view.replace(edit, sublime.Region(r.begin()), ' ' * prepend)
# r = view.transform_region_from(r, change_id)
# print("new r", r)
view.replace(edit, sublime.Region(r.end() + prepend), ' ' * append)
# change_id = view.change_id()
# for region in list(view.sel()):
# region = view.transform_region_from(region, change_id)
# _, col = view.rowcol(region.a)
# view.replace(edit, region, ' ' * (max_col - col))