-
-
Notifications
You must be signed in to change notification settings - Fork 84
/
Copy pathsyntax_dev.py
354 lines (286 loc) · 12.4 KB
/
syntax_dev.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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
import functools
import itertools
import re
import sublime
import sublime_plugin
from sublime_lib.flags import RegionOption
from .lib.scope_data import COMPILED_HEADS
from .lib import syntax_paths
__all__ = (
'SyntaxDefRegexCaptureGroupHighlighter',
'SyntaxDefCompletionsListener',
'PackagedevCommitScopeCompletionCommand'
)
PACKAGE_NAME = __package__.split('.')[0]
def status(msg, console=False):
msg = "[%s] %s" % (PACKAGE_NAME, msg)
sublime.status_message(msg)
if console:
print(msg)
class SyntaxDefRegexCaptureGroupHighlighter(sublime_plugin.ViewEventListener):
# TODO multiple views into the same file
@classmethod
def is_applicable(cls, settings):
return settings.get('syntax') == syntax_paths.SYNTAX_DEF
def on_selection_modified(self):
prefs = sublime.load_settings('PackageDev.sublime-settings')
scope = prefs.get('syntax_captures_highlight_scope', 'text')
styles = prefs.get('syntax_captures_highlight_styles', ['DRAW_NO_FILL'])
style_flags = RegionOption(*styles)
self.view.add_regions(
key='captures',
regions=list(self.get_regex_regions()),
scope=scope,
flags=style_flags,
)
def get_regex_regions(self):
locations = [
region.begin()
for selection in self.view.sel()
if self.view.match_selector(
selection.begin(),
'source.yaml.sublime.syntax meta.expect-captures'
)
for region in self.view.split_by_newlines(selection)
]
for loc in locations:
# Find the line number.
match = re.search(r'(\d+):', self.view.substr(self.view.line(loc)))
if not match:
continue
n = int(match.group(1))
# Find the associated regexp. Assume it's the preceding one.
try:
regexp_region = [
region
for region in self.view.find_by_selector('source.regexp.oniguruma')
if region.end() < loc
][-1]
except IndexError:
continue
if n == 0:
yield regexp_region
continue
# Find parens that define capture groups.
regexp_offset = regexp_region.begin()
parens = iter(
(match.group(), match.start() + regexp_offset)
for match in re.finditer(r'\(\??|\)', self.view.substr(regexp_region))
if self.view.match_selector(
match.start() + regexp_offset,
'keyword.control.group'
)
)
# Find the start of the nth capture group.
start = None
count = 0
for p, i in parens:
if p == '(': # Not (?
count += 1
if count == n:
start = i
break
# Find the end of that capture group
end = None
depth = 0
for p, i in parens:
if p in {'(', '(?'}:
depth += 1
else:
if depth == 0:
end = i + 1
break
else:
depth -= 1
if end is not None:
yield sublime.Region(start, end)
def _inhibit_word_completions(func):
"""Decorator that inhibits ST's word completions if non-None value is returned."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
if ret is not None:
return (ret, sublime.INHIBIT_WORD_COMPLETIONS)
return wrapper
def _build_completions(base_keys=(), dict_keys=(), list_keys=()):
generator = itertools.chain(
(("{0}\t{0}:".format(s), "%s: " % s) for s in base_keys),
(("{0}\t{0}:".format(s), "%s:\n " % s) for s in dict_keys),
(("{0}\t{0}:".format(s), "%s:\n- " % s) for s in list_keys)
)
return sorted(generator)
class SyntaxDefCompletionsListener(sublime_plugin.ViewEventListener):
base_completions_root = _build_completions(
base_keys=('name', 'scope', 'first_line_match'),
dict_keys=('variables', 'contexts'),
list_keys=('file_extensions',),
)
base_completions_contexts = _build_completions(
base_keys=('scope', 'match', 'include', 'push', 'with_prototype', # 'pop',
'embed', 'embed_scope', 'escape',
'branch_point', 'fail',
'meta_scope', 'meta_content_scope', 'meta_include_prototype', 'clear_scopes'),
dict_keys=('captures', 'escape_captures'),
)
base_completions_contexts += (("pop\tpop: true", "pop: ${1:true}"),)
# These instance variables are for communicating
# with our PostCompletionsListener instance.
base_suffix = None
@classmethod
def applies_to_primary_view_only(cls):
return False
@classmethod
def is_applicable(cls, settings):
return settings.get('syntax') == syntax_paths.SYNTAX_DEF
@_inhibit_word_completions
def on_query_completions(self, prefix, locations):
def verify_scope(selector, offset=0):
"""Verify scope for each location."""
return all(self.view.match_selector(point + offset, selector)
for point in locations)
# None of our business
if not verify_scope("- comment - (source.regexp - keyword.other.variable)"):
return None
# Scope name completions based on our scope_data database
if verify_scope("meta.expect-scope, meta.scope", -1):
return self._complete_scope(prefix, locations)
# Auto-completion for include values using the 'contexts' keys and for
if verify_scope("meta.expect-context-list-or-content"
" | meta.context-list-or-content", -1):
return self._complete_keyword(prefix, locations) + \
self._complete_context(prefix, locations)
# Auto-completion for include values using the 'contexts' keys
if verify_scope("meta.expect-context-list | meta.expect-context"
" | meta.include | meta.context-list", -1):
return self._complete_context(prefix, locations)
# Auto-completion for branch points with 'fail' key
if verify_scope("meta.expect-branch-point-reference"
" | meta.branch-point-reference", -1):
return self._complete_branch_point()
# Auto-completion for variables in match patterns using 'variables' keys
if verify_scope("keyword.other.variable"):
return self._complete_variable()
# Standard completions for unmatched regions
return self._complete_keyword(prefix, locations)
def _line_prefix(self, point):
_, col = self.view.rowcol(point)
line = self.view.substr(self.view.line(point))
return line[:col]
def _complete_context(self, prefix, locations):
# Verify that we're not looking for an external include
for point in locations:
line_prefix = self._line_prefix(point)
real_prefix = re.search(r"[^,\[ ]*$", line_prefix).group(0)
if real_prefix.startswith("scope:") or "/" in real_prefix:
return [] # Don't show any completions here
elif real_prefix != prefix:
# print("Unexpected prefix mismatch: {} vs {}".format(real_prefix, prefix))
return []
context_names = (
self.view.substr(r)
for r in self.view.find_by_selector("entity.name.function.context")
)
return [(ctx + "\tcontext", ctx) for ctx in context_names]
def _complete_keyword(self, prefix, locations):
def verify_scope(selector, offset=0):
"""Verify scope for each location."""
return all(self.view.match_selector(point + offset, selector)
for point in locations)
prefixes = set()
for point in locations:
# Ensure that we are completing a key name everywhere
line_prefix = self._line_prefix(point)
real_prefix = re.sub(r"^ +(- +)*", " ", line_prefix) # collapse leading whitespace
prefixes.add(real_prefix)
if len(prefixes) != 1:
return None
else:
real_prefix = next(iter(prefixes))
# (Supposedly) all keys start their own line
match = re.match(r"^(\s*)[\w-]*$", real_prefix)
if not match:
return None
elif not match.group(1):
return self.base_completions_root
elif verify_scope("meta.block.contexts"):
return self.base_completions_contexts
else:
return None
def _complete_scope(self, prefix, locations):
# Determine entire prefix
prefixes = set()
for point in locations:
*_, real_prefix = self._line_prefix(point).rpartition(" ")
prefixes.add(real_prefix)
if len(prefixes) > 1:
return None
else:
real_prefix = next(iter(prefixes))
# Tokenize the current selector
tokens = real_prefix.split(".")
if len(tokens) <= 1:
# No work to be done here, just return the heads
return COMPILED_HEADS.to_completion()
base_scope_completion = self._complete_base_scope(tokens[-1])
# Browse the nodes and their children
nodes = COMPILED_HEADS
for i, token in enumerate(tokens[:-1]):
node = nodes.find(token)
if not node:
status("`%s` not found in scope naming conventions" % '.'.join(tokens[:i + 1]))
break
nodes = node.children
if not nodes:
status("No nodes available in scope naming conventions after `%s`"
% '.'.join(tokens[:-1]))
break
else:
# Offer to complete from conventions or base scope
return nodes.to_completion() + base_scope_completion
# Since we don't have anything to offer,
# just complete the base scope appendix/suffix.
return base_scope_completion
def _complete_base_scope(self, last_token):
regions = self.view.find_by_selector("meta.scope string - meta.block")
if len(regions) != 1:
status("Warning: Could not determine base scope uniquely", console=True)
self.base_suffix = None
return []
base_scope = self.view.substr(regions[0])
*_, base_suffix = base_scope.rpartition(".")
# Only useful when the base scope suffix is not already the last one
# In this case it is even useful to inhibit other completions completely
if last_token == base_suffix:
self.base_suffix = None
return []
self.base_suffix = base_suffix
return [(base_suffix + "\tbase suffix", base_suffix)]
def _complete_variable(self):
variable_names = (
self.view.substr(r)
for r in self.view.find_by_selector("entity.name.constant")
)
return [(var + "\tvariable", var) for var in variable_names]
def _complete_branch_point(self):
branch_names = (
self.view.substr(r)
for r in self.view.find_by_selector("entity.name.label.branch-point")
)
return [(var + "\tbranch point", var) for var in branch_names]
class PackagedevCommitScopeCompletionCommand(sublime_plugin.TextCommand):
def run(self, edit):
self.view.run_command("commit_completion")
# Don't add duplicated dot, if scope is edited in the middle.
if self.view.substr(self.view.sel()[0].a) == ".":
return
# Check if the completed value was the base suffix
# and don't re-open auto complete in that case.
listener = sublime_plugin.find_view_event_listener(self.view, SyntaxDefCompletionsListener)
if listener and listener.base_suffix:
point = self.view.sel()[0].a
region = sublime.Region(point - len(listener.base_suffix) - 1, point)
if self.view.substr(region) == "." + listener.base_suffix:
return
# Insert a . and trigger next completion
self.view.run_command('insert', {'characters': "."})
self.view.run_command('auto_complete', {'disable_auto_insert': True})