-
Notifications
You must be signed in to change notification settings - Fork 6
/
elastic_tabstops.py
259 lines (224 loc) · 7.33 KB
/
elastic_tabstops.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
"""
UPDATE
Use this command to reprocess the whole file. Shouldn't be necessary except
in specific cases.
{ "keys": ["super+j"], "command": "elastic_tabstops_update"},
"""
import sublime
import sublime_plugin
import re
import sys
if sys.version_info[0] < 3:
from edit import Edit
from itertools import izip, izip_longest
zip = izip
zip_longest = izip_longest
else:
from ElasticTabstops.edit import Edit
from itertools import zip_longest
def lines_in_buffer(view):
row, col = view.rowcol(view.size())
#"row" is the index of the last row; need to add 1 to get number of rows
return row + 1
def get_selected_rows(view):
selected_rows = set()
for s in view.sel():
begin_row,_ = view.rowcol(s.begin())
end_row,_ = view.rowcol(s.end())
# Include one row before and after the selection, to cover cases like
# hitting enter at the beginning of a line: affect both the newly-split
# block and the block remaining above.
list(map(selected_rows.add, range(begin_row-1, end_row+1 + 1)))
return selected_rows
def tabs_for_row(view, row):
row_tabs = []
for tab in re.finditer("\t", view.substr(view.line(view.text_point(row,0)))):
row_tabs.append(tab.start())
return row_tabs
def selection_columns_for_row(view, row):
selections = []
for s in view.sel():
if s.empty():
r, c =view.rowcol(s.a)
if r == row:
selections.append(c)
return selections
def rightmost_selection_in_cell(selection_columns, cell_right_edge):
rightmost = 0
if len(selection_columns):
rightmost = max([s if s <= cell_right_edge else 0 for s in selection_columns])
return rightmost
def cell_widths_for_row(view, row):
selection_columns = selection_columns_for_row(view, row)
tabs = [-1] + tabs_for_row(view, row)
widths = [0] * (len(tabs) - 1)
line = view.substr(view.line(view.text_point(row,0)))
for i in range(0,len(tabs)-1):
left_edge = tabs[i]+1
right_edge = tabs[i+1]
rightmost_selection = rightmost_selection_in_cell(selection_columns, right_edge)
cell = line[left_edge:right_edge]
widths[i] = max(len(cell.rstrip()), rightmost_selection - left_edge)
return widths
def find_cell_widths_for_block(view, row):
cell_widths = []
#starting row and backward
row_iter = row
while row_iter >= 0:
widths = cell_widths_for_row(view, row_iter)
if len(widths) == 0:
break
cell_widths.insert(0, widths)
row_iter -= 1
first_row = row_iter + 1
#forward (not including starting row)
row_iter = row
num_rows = lines_in_buffer(view)
while row_iter < num_rows - 1:
row_iter += 1
widths = cell_widths_for_row(view, row_iter)
if len(widths) == 0:
break
cell_widths.append(widths)
return cell_widths, first_row
def adjust_row(view, glued, row, widths):
row_tabs = tabs_for_row(view, row)
if len(row_tabs) == 0:
return glued
bias = 0
location = -1
for w, it in zip(widths,row_tabs):
location += 1 + w
it += bias
difference = location - it
if difference == 0:
continue
end_tab_point = view.text_point(row, it)
partial_line = view.substr(view.line(end_tab_point))[0:it]
stripped_partial_line = partial_line.rstrip()
ispaces = len(partial_line) - len(stripped_partial_line)
if difference > 0:
view.run_command("maybe_mark_undo_groups_for_gluing")
glued = True
with Edit(view, "ElasticTabstops") as edit:
#put the spaces after the tab and then delete the tab, so any insertion
#points behave as expected
edit.insert(end_tab_point+1, (' ' * difference) + "\t")
edit.erase(sublime.Region(end_tab_point, end_tab_point + 1))
bias += difference
if difference < 0 and ispaces >= -difference:
view.run_command("maybe_mark_undo_groups_for_gluing")
glued = True
with Edit(view, "ElasticTabstops") as edit:
edit.erase(sublime.Region(end_tab_point, end_tab_point + difference))
bias += difference
return glued
def set_block_cell_widths_to_max(cell_widths):
starting_new_block = True
for c, column in enumerate(zip_longest(*cell_widths, fillvalue=-1)):
#add an extra -1 to the end so that the end of the column automatically
#finishes a block
column += (-1,)
done = False
for r, width in enumerate(column):
if starting_new_block:
block_start_row = r
starting_new_block = False
max_width = 0
if width == -1:
#block ended
block_end_row = r
for j in range(block_start_row, block_end_row):
cell_widths[j][c] = max_width
starting_new_block = True
max_width = max(max_width, width)
def process_rows(view, rows):
glued = False
checked_rows = set()
for row in rows:
if row in checked_rows:
continue
cell_widths_by_row, row_index = find_cell_widths_for_block(view, row)
set_block_cell_widths_to_max(cell_widths_by_row)
for widths in cell_widths_by_row:
checked_rows.add(row_index)
glued = adjust_row(view, glued, row_index, widths)
row_index += 1
if glued:
view.run_command("glue_marked_undo_groups")
def fix_view(view):
# When modifying a clone of a view, Sublime Text will only pass in
# the original view ID, which means we refer to the wrong selections.
# Fix which view we have.
active_view = sublime.active_window().active_view()
if view == None:
view = active_view
elif view.id() != active_view.id() and view.buffer_id() == active_view.buffer_id():
view = active_view
return view
class ElasticTabstopsListener(sublime_plugin.EventListener):
selected_rows_by_view = {}
running = False
def on_modified(self, view):
if self.running:
return
view = fix_view(view)
history_item = view.command_history(1)[1]
if history_item:
if history_item.get('name') == "ElasticTabstops":
return
if history_item.get('commands') and history_item['commands'][0][1].get('name') == "ElasticTabstops":
return
selected_rows = self.selected_rows_by_view.get(view.id(), set())
selected_rows = selected_rows.union(get_selected_rows(view))
try:
self.running = True
translate = False
if view.settings().get("translate_tabs_to_spaces"):
translate = True
view.settings().set("translate_tabs_to_spaces", False)
process_rows(view, selected_rows)
finally:
self.running = False
if translate:
view.settings().set("translate_tabs_to_spaces",True)
def on_selection_modified(self, view):
view = fix_view(view)
self.selected_rows_by_view[view.id()] = get_selected_rows(view)
def on_activated(self, view):
view = fix_view(view)
self.selected_rows_by_view[view.id()] = get_selected_rows(view)
class ElasticTabstopsUpdateCommand(sublime_plugin.TextCommand):
def run(self, edit):
rows = range(0,lines_in_buffer(self.view))
process_rows(self.view, rows)
class MoveByCellsCommand(sublime_plugin.TextCommand):
def run(self, edit, direction, extend):
new_regions = []
for s in self.view.sel():
line = self.view.substr(self.view.line(s.b))
row, col = self.view.rowcol(s.b)
if direction == "right":
next_tab_col = line[col+1:].find('\t')
if next_tab_col == -1:
next_tab_col = len(line)
else:
next_tab_col += col + 1
elif direction == "left":
next_tab_col = line[:max(col-1, 0)].rfind('\t')
if next_tab_col == -1:
next_tab_col = 0
else:
next_tab_col += 1
else:
raise Exception("invalid direction")
next_tab_col = s.b
b = self.view.text_point(row, next_tab_col)
if extend:
new_regions.append(sublime.Region(s.a, b))
else:
new_regions.append(sublime.Region(b, b))
sel = self.view.sel()
sel.clear()
for r in new_regions:
sel.add(r)