-
Notifications
You must be signed in to change notification settings - Fork 5
/
CursorRuler.py
339 lines (251 loc) · 12.2 KB
/
CursorRuler.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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
'''
CursorRuler 1.1.6
A plugin for the Sublime Text editor which marks
the current cursor position(s) using dynamic rulers.
See README.md for details.
'''
import sublime
import sublime_plugin
#
# It's important for us to know if we're running in ST3 or ST2.
#
# Build 3010 and older of the Sublime Text 3 builds do not have the API
# available during program startup. As of build 3011 `sublime.version()`
# is available during startup. If we're unable to get the version build
# number we make the assumption that it's 3000.
#
# We're also assuming that if we're not in ST3 then we're in ST2.
#
st = 3000 if sublime.version() == '' else int(sublime.version())
# ------------------------------------------------------------------------------
class CursorRuler(object):
@classmethod
def __draw_on_view(cls, view, active_view):
cursors = active_view.sel()
em_width = active_view.em_width()
view_size = active_view.size()
dynamic_rulers = []
# Setup rulers for each cursor.
for cursor in cursors:
#
# Get the cursor's true horizontal position.
#
# To do this we need to know a few things:
#
# The cursor position is usually represented technically as an
# empty region. While in this case the region's `a` and `b`
# properties are the same, in the case of a selection region
# being made the `b` property more accurately represents where
# the cursor is at.
#
# A cursor's `xpos` is its target horizontal layout position.
# It is the position where the cursor would be at if it weren't
# affected by lack of virtual whitespace, word-wrapping, or
# varying font widths.
#
# If it's non-negative then it represents the position the cursor
# was just at in the previous line. It also indicates that the
# cursor was moved vertically with either the up or down arrow key.
#
# If it's negative then it's actually not a position but rather
# an indicator that the cursor was moved horizontally with either
# the left or right arrow key or repositioned with a mouse click.
#
# First we get what would normally be the current cursor position.
cur_x = view.text_to_layout(cursor.b)[0]
# We then take into account the strangeness of word-wrapping.
# It only matters when we're not at the end of the file.
next_x = view.text_to_layout(cursor.b + 1)[0]
xpos = cursor.xpos if st >= 3000 else cursor.xpos()
if xpos >= 0 and xpos < cur_x and cur_x > next_x and cursor.b < view_size:
if not cls.indent_subsequent_lines:
cur_x = 0
else:
line = view.substr(view.line(cursor))
if line:
line_length = len(line)
stripped_line_length = len(line.lstrip())
# We keep going if the line is not entirely whitespace.
if stripped_line_length > 0:
# We find out where the first non-whitespace
# character of the line is and we use its position.
cur_row = view.rowcol(cursor.b)[0]
beginning_pos = line_length - stripped_line_length
beginning_point = view.text_point(cur_row, beginning_pos)
cur_x = view.text_to_layout(beginning_point)[0]
# Get the cursor position in terms of columns.
cur_col = cur_x / em_width
# Setup the current dynamic rulers to be included
# with the static rulers.
dynamic_rulers += [cur_col + offset for offset in cls.cursor_rulers]
active_rulers = cls.rulers + dynamic_rulers
if st < 3000:
#
# For some reason in ST2 we'll get into some sort of infinite
# recursion when trying to set the rulers. Here we only update the
# "rulers" setting if changes have been made (i.e. a cursor ruler
# changes location).
#
# Note: ST2 uses Python 2 so we can use the convenient `cmp()`
# function which is unavailable in Python 3.
#
if cmp(active_rulers, view.settings().get('rulers')) != 0:
view.settings().set('rulers', active_rulers)
else:
view.settings().set('rulers', active_rulers)
# ..........................................................................
@classmethod
def __setup(cls):
default_cursor_rulers = [-0.1, 0.2]
cls.rulers = cls.editor_settings.get('rulers', [])
cls.indent_subsequent_lines = bool(cls.editor_settings.get('indent_subsequent_lines', True))
cls.cursor_rulers = cls.settings.get('cursor_rulers', default_cursor_rulers)
cls.synchronized = bool(cls.settings.get('synchronized', True))
# Ensure the rulers settings are valid lists.
if not isinstance(cls.rulers, list):
cls.rulers = []
if not isinstance(cls.cursor_rulers, list):
cls.cursor_rulers = default_cursor_rulers
#
# We shouldn't draw our own rulers when our plugin is ignored.
# We need to check for this because if our plugin is active it will,
# until the next time Sublime Text is restarted, stay active after
# being disabled or being manually added to `ignored_packages` in
# the user settings.
#
# For some reason the `sublime` module is sometimes not available.
if sublime is None:
ignored_packages = []
else:
ignored_packages = sublime.load_settings('Preferences.sublime-settings').get('ignored_packages', [])
cls.enabled = 'CursorRuler' not in ignored_packages and bool(cls.settings.get('enabled', True))
# ..........................................................................
@classmethod
def draw(cls, view):
if cls.synchronized:
# Draw the dynamic rulers for every view of the same buffer
# as the active view.
view_buffer_id = view.buffer_id()
for window in sublime.windows():
for v in window.views():
if (v.buffer_id() == view_buffer_id):
cls.__draw_on_view(v, view)
else:
# Draw the dynamic rulers for the current view.
cls.__draw_on_view(view, view)
# ..........................................................................
@classmethod
def init(cls):
plugin_name = 'CursorRuler'
cls.editor_settings = sublime.load_settings('Preferences.sublime-settings')
cls.settings = sublime.load_settings(plugin_name + '.sublime-settings')
# In ST3 the `add_on_change()` was not implemented until build 3013.
if st < 3000 or st >= 3013:
cls.editor_settings.add_on_change(plugin_name.lower() + '-reload', cls.__setup)
cls.settings.add_on_change('reload', cls.__setup)
cls.__setup()
# ..........................................................................
@classmethod
def is_enabled(cls, view):
return cls.enabled and not view.settings().get('is_widget', False)
# ..........................................................................
@classmethod
def reset(cls, view):
if cls.synchronized:
# Reset all the views of the same buffer.
view_buffer_id = view.buffer_id()
for window in sublime.windows():
for v in window.views():
if (v.buffer_id() == view_buffer_id):
v.settings().set('rulers', cls.rulers)
else:
# Reset the current view.
view.settings().set('rulers', cls.rulers)
# ..........................................................................
@classmethod
def reset_all(cls):
# Remove the rulers we created and restore any regular rulers.
for window in sublime.windows():
for view in window.views():
if not view.settings().get('is_widget', False):
view.settings().set('rulers', cls.rulers)
# ------------------------------------------------------------------------------
class CursorRulerToggleCommand(sublime_plugin.TextCommand):
def run(self, edit):
if CursorRuler.is_enabled(self.view):
# It's important that we turn off `enabled` before resetting.
# Otherwise, the dynamic rulers will stick around and not
# get cleared.
CursorRuler.enabled = False
CursorRuler.reset_all()
else:
CursorRuler.enabled = True
CursorRuler.draw(self.view)
CursorRuler.settings.set('enabled', CursorRuler.enabled)
sublime.save_settings('CursorRuler.sublime-settings')
# ------------------------------------------------------------------------------
class CursorRulerWrapLinesCommand(sublime_plugin.TextCommand):
def run(self, edit):
if CursorRuler.is_enabled(self.view):
# Temporarily turn off CursorRuler.
CursorRuler.reset(self.view)
CursorRuler.enabled = False
# Do our line wrapping without the unwanted
# influence of the dynamic cursor rulers.
self.view.run_command('wrap_lines')
# Reactivate CursorRuler.
CursorRuler.draw(self.view)
CursorRuler.enabled = True
else:
self.view.run_command('wrap_lines')
# ------------------------------------------------------------------------------
class CursorRulerListener(sublime_plugin.EventListener):
def on_activated(self, view):
if not view.is_loading() and CursorRuler.is_enabled(view):
CursorRuler.draw(view)
view.settings().add_on_change('command_mode', self.on_command_mode_change)
def on_close(self, view):
CursorRuler.reset(view)
def on_deactivated(self, view):
if CursorRuler.is_enabled(view):
CursorRuler.reset(view)
def on_load(self, view):
# In ST2 the initialization phase needs to happen here
# for static rulers to not disappear.
if st < 3000:
CursorRuler.init()
if CursorRuler.is_enabled(view):
CursorRuler.draw(view)
else:
CursorRuler.reset(view)
def on_selection_modified(self, view):
# For some reason the `sublime` module is sometimes not available.
if sublime is None: return
active_window = sublime.active_window()
if active_window is None: return
# The view parameter doesn't always match the active view
# the cursor is in. This happens when there are multiple
# views of the same file.
active_view = active_window.active_view()
# An empty window has no active views. A newly-opened window
# created by the "New Window" command is empty. When the
# `close_windows_when_empty` user setting is true a non-empty
# window can be left empty by closing its contained views.
if active_view is None: return
if CursorRuler.is_enabled(active_view):
CursorRuler.draw(active_view)
def on_command_mode_change(self):
self.on_selection_modified(None)
# ------------------------------------------------------------------------------
# In ST3 this will get called automatically once the full API becomes available.
def plugin_loaded():
CursorRuler.init()
# ------------------------------------------------------------------------------
def plugin_unloaded():
CursorRuler.reset_all()
# ------------------------------------------------------------------------------
# In ST2 this prevents an error from happening if this particular file
# is saved. This also prevents a missing attribute error from occurring
# at startup.
if st < 3000:
plugin_loaded()