-
-
Notifications
You must be signed in to change notification settings - Fork 87
Expand file tree
/
Copy path__init__.py
More file actions
329 lines (285 loc) · 11.3 KB
/
__init__.py
File metadata and controls
329 lines (285 loc) · 11.3 KB
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
import html
import logging
import os
import sublime
import sublime_plugin
from ..lib import get_setting, inhibit_word_completions
from ..lib import syntax_paths
from ..lib.view_utils import region_flags_from_strings
from ..lib.weakmethod import WeakMethodProxy
from .region_math import (VALUE_SCOPE, KEY_SCOPE, KEY_COMPLETIONS_SCOPE,
get_key_region_at, get_last_key_region)
from .known_settings import KnownSettings, PREF_FILE
__all__ = (
'SettingsListener',
'GlobalSettingsListener',
)
POPUP_TEMPLATE = """
<body id="sublime-settings">
<style>
html.light {{
--html-background: color(var(--background) blend(black 91%));
--border-color: color(var(--html-background) blend(black 95%));
}}
html.dark {{
--html-background: color(var(--background) blend(white 93%));
--border-color: color(var(--html-background) blend(white 95%));
}}
html, body {{
margin: 0;
padding: 0;
background-color: var(--html-background);
color: var(--foreground);
}}
h1, h2 {{
border-bottom: 1px solid var(--border-color);
font-weight: normal;
margin: 0;
padding: 0.5rem 0.6rem;
}}
h1 {{
color: var(--orangish);
font-size: 1.0rem;
}}
html.light h1 {{
color: var(--redish);
}}
h2 {{
color: color(var(--html-background) blend(var(--foreground) 30%));
font-size: 1.0rem;
font-family: monospace;
}}
p {{
margin: 0;
padding: 0.5rem;
}}
a {{
text-decoration: none;
}}
</style>
{0}
</body>
"""
PHANTOM_TEMPLATE = """
<body id="sublime-settings-edit">
<style>
html, body {{
margin: 0;
padding: 0;
background-color: transparent;
}}
a {{
text-decoration: none;
}}
</style>
{0}
</body>
"""
WIDGET_SETTINGS_NAMES = {
"Console Input Widget",
"Regex Format Widget",
"Regex Replace Widget",
"Regex Widget",
"Widget",
}
# user package pattern
USER_PATH = "{0}Packages{0}User{0}".format(os.sep)
logger = logging.getLogger(__name__)
def is_widget_file(filename):
basename, ext = os.path.splitext(filename)
return (
ext == ".sublime-settings"
and basename in WIDGET_SETTINGS_NAMES
or any(basename.startswith(name + " - ") for name in WIDGET_SETTINGS_NAMES)
)
class SettingsListener(sublime_plugin.ViewEventListener):
is_completing_key = False
@classmethod
def applies_to_primary_view_only(cls):
return False
@classmethod
def is_applicable(cls, settings):
"""Enable the listener for Sublime Settings and Project syntax."""
return settings.get('syntax') in (syntax_paths.SETTINGS, syntax_paths.PROJECT)
def __init__(self, view):
"""Initialize view event listener object."""
# Need this "hack" to allow reloading of the module,
# because `super` tries to use the old reference (I think?).
# See also https://lists.gt.net/python/python/139992
sublime_plugin.ViewEventListener.__init__(self, view)
filepath = view.file_name()
logger.debug("initializing SettingsListener for %r", view.file_name())
self.known_settings = None
if filepath:
filename = os.path.basename(filepath)
if filepath.endswith(".sublime-project") or is_widget_file(filename):
logger.debug("Opening a widget settings or project file")
self.known_settings = KnownSettings(PREF_FILE)
elif filepath.endswith(".sublime-settings"):
self.known_settings = KnownSettings(filename)
if self.known_settings:
self.known_settings.add_on_loaded(self.do_linting)
else:
logger.error("Not a Sublime Text Settings or Project file: %r", filepath)
self.phantom_set = sublime.PhantomSet(self.view, "sublime-settings-edit")
if self._is_base_settings_view() and get_setting("settings.show_quick_edit_icon"):
self.build_phantoms()
def __del__(self):
logger.debug("deleting SettingsListener instance for %r", self.view.file_name())
self.view.erase_regions('unknown_settings_keys')
self.phantom_set.update([])
def on_modified_async(self):
"""Sublime Text modified event handler to update linting."""
self.do_linting()
if self._is_base_settings_view() and get_setting("settings.show_quick_edit_icon"):
# This may only occur for unpacked packages
self.build_phantoms()
@inhibit_word_completions
def on_query_completions(self, prefix, locations):
"""Sublime Text query completions event handler.
Create a list with completions for all known settings or values.
Arguments:
prefix (string):
the line content before cursor
locations (list of int):
the text positions of all characters in prefix
Returns:
tuple ([ [trigger, content], [trigger, content] ], flags):
the tuple with content ST needs to display completions
"""
if not get_setting('settings.auto_complete'):
return
if self.known_settings and len(locations) == 1:
point = locations[0]
self.is_completing_key = False
if self.view.match_selector(point, VALUE_SCOPE):
completions_aggregator = self.known_settings.value_completions
elif self.view.match_selector(point, KEY_COMPLETIONS_SCOPE):
completions_aggregator = self.known_settings.key_completions
self.is_completing_key = True
else:
return None
return completions_aggregator(self.view, prefix, point)
def on_hover(self, point, hover_zone):
"""Sublime Text hover event handler to show tooltip if needed."""
if not get_setting('settings.tooltip'):
return
# not a settings file or not hovering text
if not self.known_settings or hover_zone != sublime.HOVER_TEXT:
return
# settings key name under cursor
key_region = get_key_region_at(self.view, point)
if not key_region:
return
self.show_popup_for(key_region)
def show_popup_for(self, key_region):
key = self.view.substr(key_region)
body = self.known_settings.build_tooltip(self.view, key)
window_width = min(1000, int(self.view.viewport_extent()[0]) - 64)
# offset <h1> padding, if possible
key_start = key_region.begin()
location = max(key_start - 1, self.view.line(key_start).begin())
self.view.show_popup(
content=POPUP_TEMPLATE.format(body),
location=location,
max_width=window_width,
flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY | sublime.COOPERATE_WITH_AUTO_COMPLETE
)
def on_navigate(self, href):
"""Popup navigation event handler."""
command, _, argument = href.partition(":")
argument = html.unescape(argument)
if command == 'edit':
view_id = self.view.settings().get('edit_settings_other_view_id')
user_view = sublime.View(view_id)
if not user_view.is_valid():
return
result = user_view.find('"{}"'.format(argument), 0)
self.view.hide_popup()
if self.view.window():
self.view.window().focus_view(user_view)
if result.a == -1:
self.known_settings.insert_snippet(user_view, argument)
else:
user_view.sel().clear()
user_view.show_at_center(result.end())
user_view.sel().add(result.end() + 2)
def do_linting(self):
"""Highlight all unknown settings keys."""
unknown_regions = None
file_name = self.view.file_name() or ""
if (
self.known_settings
and (USER_PATH in file_name
or file_name.endswith(".sublime-project"))
and get_setting('settings.linting')
):
unknown_regions = [
region for region in self.view.find_by_selector(KEY_SCOPE)
if self.view.substr(region) not in self.known_settings
]
if unknown_regions:
styles = get_setting(
'settings.highlight_styles',
['DRAW_SOLID_UNDERLINE', 'DRAW_NO_FILL', 'DRAW_NO_OUTLINE']
)
self.view.add_regions(
'unknown_settings_keys',
unknown_regions,
scope=get_setting('settings.highlight_scope', "text"),
icon='dot',
flags=region_flags_from_strings(styles)
)
else:
self.view.erase_regions('unknown_settings_keys')
def build_phantoms(self):
"""Add links to side-by-side base file for editing this setting in the user file."""
if self.view.is_loading():
sublime.set_timeout(self.build_phantoms, 20)
return
logger.debug("Building phantom set for view %r", self.view.file_name())
key_regions = self.view.find_by_selector(KEY_SCOPE)
phantoms = []
for region in key_regions:
key_name = self.view.substr(region)
phantom_region = sublime.Region(region.end() + 1) # before colon
content = "<a href=\"edit:{0}\">✏</a>".format(html.escape(key_name))
phantoms.append(sublime.Phantom(
region=phantom_region,
content=PHANTOM_TEMPLATE.format(content),
layout=sublime.LAYOUT_INLINE,
# use weak reference for callback
# to allow for phantoms to be cleaned up in __del__
on_navigate=WeakMethodProxy(self.on_navigate),
))
logger.debug("Made %d phantoms", len(phantoms))
self.phantom_set.update(phantoms)
def _is_base_settings_view(self):
return self.view.settings().get('edit_settings_view') == 'base'
# Some hooks are not available to ViewEventListeners,
# which is why we need an EventListener as well.
class GlobalSettingsListener(sublime_plugin.EventListener):
def on_post_text_command(self, view, command_name, args):
if command_name == 'hide_auto_complete':
listener = sublime_plugin.find_view_event_listener(view, SettingsListener)
if listener:
listener.is_completing_key = False
elif command_name in ('commit_completion', 'insert_best_completion'):
listener = sublime_plugin.find_view_event_listener(view, SettingsListener)
if not (listener and listener.is_completing_key):
return
listener.is_completing_key = False
sel = view.sel()
if len(sel) != 1:
# unclear what to do, so just do nothing
return
point = sel[0].begin()
key_region = get_last_key_region(view, point)
if key_region:
key = view.substr(key_region)
logger.debug("showing popup after inserting key completion for %r", key)
listener.show_popup_for(key_region)
def on_post_save(self, view):
listener = sublime_plugin.find_view_event_listener(view, SettingsListener)
if listener and listener.known_settings:
listener.known_settings.trigger_settings_reload()