-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathvalign.py
299 lines (241 loc) · 9.73 KB
/
valign.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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import sublime
import sublime_plugin
import re
# This function removes the duplicates from a list and returns a new list without them
# It also preserves the order of the list
def ordered_remove_duplicates(seq):
seen = set()
seen_add = seen.add
return [ x for x in seq if x not in seen and not seen_add(x)]
class ValignCommand(sublime_plugin.TextCommand):
# Returns the line string for the given row.
def get_line_string_for_row(self, row):
view = self.view
text_point = view.text_point(row, 0)
line = view.line(text_point)
return view.substr(line)
# Calculates the indentation level of a row
def get_indentation_for_row(self, row):
line_string = self.get_line_string_for_row(row)
# Skip empty lines
if len(line_string.strip()) == 0: return None
# Calculate indentation
match = re.search("^\s+", line_string)
indentation_string = match.group(0) if match else None
indentation = len(indentation_string) if indentation_string else 0
if self.use_spaces: indentation /= self.tab_size
# A bit of a hacky fix for issue #7: in JavaScript, we'll treat "var " at the
# beginning of a line as another level of indentation to allow alignment of common
# JavaScript formatting conventions. In the future we'll extract this out into a
# more general solution.
if self.treat_var_as_indent and re.search("^\s*var ", line_string): indentation += 1
return indentation
# Expands the set of rows to all of the lines that match the current indentation level and are
# not empty; except the stop_empty option is false
def expand_rows_to_indentation(self, start_row):
line_count, _ = self.view.rowcol(self.view.size())
start_indentation = self.get_indentation_for_row(start_row)
rows = [ start_row ]
# Expand upward and then downward from the selection.
for direction in [-1, 1]:
row = start_row + direction
while row >= 0 and row < line_count + 1:
# Calculate the current indentation level.
row_indentation = self.get_indentation_for_row(row)
# Stop at empty lines or skip them.
if row_indentation is None:
if self.stop_empty:
break
else:
row += direction
continue
# Append or prepend rows and break when we hit inconsistent indentation.
if row_indentation != start_indentation:
break
# Add row to the return value
if direction is -1:
rows.insert(0, row)
else:
rows.append(row)
# Next row
row += direction
# Return the calculated rows
return rows
# Returns the character to align on based on the start row. Returns None if no proper character
# is found.
def calculate_alignment_character(self, row):
line_string = self.get_line_string_for_row(row)
self.alignment_char = None
for alignment_char in self.alignment_chars:
if re.search(re.escape(alignment_char["char"]), line_string):
self.alignment_char = alignment_char
break
# Adjusts the current alignment range based on the alignment character so that the range
# contains only rows that contain the alignment character.
def adjust_rows_for_alignment_character(self, rows, start_row):
adjusted_rows = []
alignment_char = self.alignment_char
start_i = i = rows.index(start_row)
# Check upward and then downward from the start row.
for direction in [-1, 1]:
while i >= 0 and i < len(rows):
row = rows[i]
line_string = self.get_line_string_for_row(row)
# Make sure the character exists on this line.
if alignment_char == None:
if not re.search("\S+\s+\S+", line_string): break
else:
if not re.search(re.escape(alignment_char["char"]), line_string): break
# Add the row.
if direction == -1:
adjusted_rows.insert(0, row)
else:
adjusted_rows.append(row)
# Move on to the next row.
i += direction
# Reset i.
i = start_i + 1
# Return the new adjusted rows.
return adjusted_rows
# Normalizes the rows, creating a consistent format for alignment.
def normalize_rows(self, edit):
view = self.view
alignment_char = self.alignment_char
for row in self.rows:
line_string = self.get_line_string_for_row(row)
replace_pattern = ""
replace_string = ""
if alignment_char == None:
replace_pattern = "(?<=\S)\s+"
replace_string = " "
else:
replace_pattern = "\s*" + re.escape(alignment_char["char"]) + "\s*"
if alignment_char["left_space"]: replace_string += " "
for prefix in alignment_char["prefixes"]:
if re.search("\\" + prefix + re.escape(alignment_char["char"]), line_string):
replace_pattern = "\s*" + re.escape(prefix) + re.escape(alignment_char["char"]) + "\s*"
replace_string += prefix
break
replace_string += alignment_char["char"]
if alignment_char["right_space"]: replace_string += " "
match = re.search(replace_pattern, line_string)
column_span = match.span()
text_point = view.text_point(row, 0)
view.replace(edit, sublime.Region(text_point + column_span[0], text_point + column_span[1]), replace_string)
# Aligns all the rows after they've been calculated.
def align_rows(self, edit):
view = self.view
rows = self.rows
alignment_char = self.alignment_char
char_indexes = []
max_char_index = None
# Gather all of the character indexes.
for row in rows:
line_string = self.get_line_string_for_row(row)
index = 0
has_prefix = False
if alignment_char == None:
index = re.search("\S\s", line_string).start() + 1
else:
index = re.search(re.escape(alignment_char["char"]), line_string).start()
for prefix in alignment_char["prefixes"]:
if line_string[index - 1] == prefix:
index -= 1
has_prefix = True
break
if alignment_char["alignment"] == "left": index += 1
char_index = { "index": index, "has_prefix": has_prefix }
char_indexes.append(char_index)
if not max_char_index or index > max_char_index["index"]: max_char_index = char_index
# Do the alignment!
for i in range(len(rows)):
row = rows[i]
char_index = char_indexes[i]
extra_spaces_needed = max_char_index["index"] - char_index["index"]
line_string = self.get_line_string_for_row(row)
if char_index["has_prefix"]:
if not max_char_index["has_prefix"]: extra_spaces_needed -= 1
else:
if max_char_index["has_prefix"]: extra_spaces_needed += 1
view.insert(edit, view.text_point(row, 0) + char_index["index"], " " * extra_spaces_needed)
# Runs the command.
def run(self, edit):
view = self.view
selection = self.selection = view.sel()
settings = self.settings = view.settings()
# Get the "main" row; the row with the main cursor
main_row = view.rowcol(view.lines(selection[0])[0].a)[0]
# Load the settings from the user file
valign_settings = sublime.load_settings("VAlign.sublime-settings")
# Get settings from the VAlign setting file
self.align_words = valign_settings.get("align_words", settings.get("va_align_words", True))
self.alignment_chars = valign_settings.get("alignment_chars",
settings.get("va_alignment_chars", [
{
"char": "from",
"alignment": "right",
"left_space": True,
"right_space": True,
"prefixes": [],
"is_char": False
},
{
# PHP arrays
"char": "=>",
"alignment": "right",
"left_space": True,
"right_space": True,
"prefixes": []
},
{
"char": "===",
"alignment": "right",
"left_space": True,
"right_space": True,
"prefixes": []
},
{
"char": "=",
"alignment": "right",
"left_space": True,
"right_space": True,
"prefixes": ["+", "-", "&", "|", "<", ">", "!", "~", "%", "/", "*", ".", "?", "="]
},
{
"char": ":",
"alignment": "left",
"left_space": False,
"right_space": True,
"prefixes": []
}
])
)
self.stop_empty = valign_settings.get("stop_empty", settings.get("va_stop_empty", True))
self.tab_size = int(settings.get("tab_size", 8))
self.use_spaces = settings.get("translate_tabs_to_spaces")
self.treat_var_as_indent = settings.get("treat_var_as_indent", False)
self.calculate_alignment_character(main_row)
if self.alignment_char is None and not self.align_words: return
self.rows = []
for select in selection:
# Store some useful stuff.
lines = view.lines(select)
start_row = view.rowcol(lines[0].a)[0]
# Bail if the start row is already in the rows array.
if start_row in self.rows: continue
# Bail if our start row is empty.
if len(self.get_line_string_for_row(start_row).strip()) == 0: continue
# Calculate the rows that are on the same indentation level
calculated_rows = self.expand_rows_to_indentation(start_row)
# Filter the rows if they contain the alignment character
calculated_rows = self.adjust_rows_for_alignment_character(calculated_rows, start_row)
# Add the filtered rows to the rows
self.rows.extend(calculated_rows)
# Bail that there are not duplicates in the rows
self.rows = ordered_remove_duplicates(self.rows)
# Bail if we have no rows
if len(self.rows) == 0: return
# Normalize the rows to get consistent formatting for alignment.
self.normalize_rows(edit)
# If we have valid rows, align them.
if len(self.rows) > 0: self.align_rows(edit)