This repository has been archived by the owner on Aug 4, 2022. It is now read-only.
forked from SublimeHaskell/SublimeHaskell
-
Notifications
You must be signed in to change notification settings - Fork 0
/
autocomplete.py
341 lines (268 loc) · 13.7 KB
/
autocomplete.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
# -*- coding: UTF-8 -*-
"""SublimeHaskell autocompletion support."""
import re
import SublimeHaskell.internals.atomics as Atomics
import SublimeHaskell.internals.backend_mgr as BackendManager
import SublimeHaskell.internals.logging as Logging
import SublimeHaskell.internals.settings as Settings
import SublimeHaskell.internals.utils as Utils
import SublimeHaskell.sublime_haskell_common as Common
# Checks if we are in an import statement.
IMPORT_RE = re.compile(r'.*import(\s+qualified)?\s+')
IMPORT_RE_PREFIX = re.compile(r'^\s*import(\s+qualified)?\s+([\w\d\.]*)$')
IMPORT_QUALIFIED_POSSIBLE_RE = re.compile(r'.*import\s+(?P<qualifiedprefix>\S*)$')
# Checks if a word contains only alphanums, -, and _, and dot
NO_SPECIAL_CHARS_RE = re.compile(r'^(\w|[\-\.])*$')
# Export module
EXPORT_MODULE_RE = re.compile(r'\bmodule\s+[\w\d\.]*$')
def sort_completions(comps):
comps.sort(key=lambda k: k[0])
def sorted_completions(comps):
return sorted(set(comps)) # unique
def make_completions(suggestions):
# return sorted_completions([s.suggest() for s in suggestions or []])
return sorted([s.suggest() for s in suggestions or []])
def make_locations(comps):
return sorted([[s.brief(), s.get_source_location()] for s in comps if s.has_source_location()],
key=lambda k: k[0])
class CompletionCache(Atomics.LockedObject):
def __init__(self):
super().__init__()
self.files = {}
self.cabal = []
self.sources = []
self.global_comps = []
self.source_locs = None
def __enter__(self):
super().__enter__()
return self
# Technically, useless super() delegation. Uncomment if more is needed.
# def __exit__(self, otype, value, traceback):
# super().__exit__(otype, value, traceback)
def set_files(self, filename, comps):
self.files[filename] = comps
def set_cabal(self, comps):
self.cabal = comps
self.global_comps = sorted_completions(self.cabal + self.sources)
def set_sources(self, comps):
self.sources = comps
self.global_comps = sorted_completions(self.cabal + self.sources)
def set_locs(self, locs):
self.source_locs = locs
def global_completions(self):
return self.global_comps
# Autocompletion data
class AutoCompleter(object):
'''All of the logic (or lack thereof) behind Haskell symbol completions.
'''
def __init__(self):
self.language_pragmas = []
self.flags_pragmas = []
# cabal name => set of modules, where cabal name is 'cabal' for cabal or sandbox path
# for cabal-devs
self.module_completions = Atomics.AtomicDuck()
# keywords
self.keywords = ['do', 'case', 'of', 'let', 'in', 'data', 'instance', 'type', 'newtype', 'where',
'deriving', 'import', 'module']
self.current_filename = None
# filename ⇒ preloaded completions + None ⇒ all completions
self.cache = CompletionCache()
def keyword_completions(self, query):
return [(k + '\tkeyword', k) for k in self.keywords if k.startswith(query)] if isinstance(query, ''.__class__) else []
def generate_completions_cache(self, project_name, file_name, contents=None):
def log_result(result):
retval = result or []
if Settings.COMPONENT_DEBUG.completions:
print('completions: {0}'.format(len(retval)))
return retval
comps = []
update_cabal = False
update_sources = False
with self.cache as cache_:
if file_name in cache_.files:
del cache_.files[file_name]
else:
update_cabal = not cache_.cabal
update_sources = not cache_.sources
## Not sure what these were supposed to do -- the actual methods are no-ops.
if update_cabal:
self.update_cabal_completions()
if update_sources:
self.update_sources_completions()
with self.cache as cache_:
comps = cache_.global_completions()
import_names = []
if file_name:
if Settings.COMPONENT_DEBUG.completions:
print('preparing completions for {0} ({1})'.format(project_name, file_name))
backend = BackendManager.active_backend()
comps = make_completions(backend.complete(Common.QualifiedSymbol(''), file_name, contents=contents))
current_module = Utils.head_of(backend.module(project_name, file_name))
if Settings.COMPONENT_DEBUG.completions:
print('current_module {0}'.format(current_module))
if current_module:
# Get import names
#
# Note, that if module imported with 'as', then it can be used only with its synonym
# instead of full name
import_names.extend([('{0}\tmodule {1}'.format(i.import_as, i.module), i.import_as)
for i in current_module.imports if i.import_as])
import_names.extend([('{0}\tmodule'.format(i.module), i.module)
for i in current_module.imports if not i.import_as])
comps.extend(import_names)
sort_completions(comps)
with self.cache as cache_:
cache_.files[file_name] = comps
return log_result(cache_.files[file_name])
else:
return log_result(comps)
def drop_completions_async(self, file_name=None):
Logging.log('drop prepared completions', Logging.LOG_DEBUG)
with self.cache as cache_:
if file_name is None:
cache_.files.clear()
elif file_name in cache_.files:
del cache_.files[file_name]
def update_cabal_completions(self):
pass
def update_sources_completions(self):
pass
def get_completions(self, view, locations):
"Get all the completions related to the current file."
current_file_name = view.file_name()
if not current_file_name:
return []
if Settings.COMPONENT_DEBUG.completions:
print('AutoCompleter.get_completions.')
self.current_filename = current_file_name
_, project_name = Common.locate_cabal_project_from_view(view)
line_contents = Common.get_line_contents(view, locations[0])
qsymbol = Common.get_qualified_symbol(line_contents)
qualified_prefix = qsymbol.qualified_name()
if Settings.COMPONENT_DEBUG.completions:
print('qsymbol {0}'.format(qsymbol))
print('current_file_name {0} qualified_prefix {1}'.format(current_file_name, qualified_prefix))
view_settings = view.settings()
wide = view_settings.get('subhask_wide_completion')
backend = BackendManager.active_backend()
suggestions = []
completions = []
if qsymbol.module:
if qsymbol.is_import_list:
current_module = Utils.head_of(backend.module(project_name, current_file_name))
if current_module and current_module.location.project:
# Search for declarations of qsymbol.module within current project
q_module = Utils.head_of(backend.scope_modules(project_name, current_file_name, lookup=qsymbol.module,
search_type='exact'))
if q_module is not None:
if q_module.by_source():
proj_module = backend.resolve(file=q_module.location.filename, exports=True)
if proj_module:
suggestions = proj_module.declarations.values()
elif q_module.by_cabal():
cabal_module = Utils.head_of(backend.module(project_name, lookup=q_module.name, search_type='exact',
package=q_module.location.package.name))
if cabal_module:
suggestions = cabal_module.declarations.values()
else:
if Settings.COMPONENT_DEBUG.completions:
print('completions: querying module-specific completions')
suggestions = backend.complete(qsymbol, current_file_name, wide=wide)
completions = make_completions(suggestions)
else:
with self.cache as cache_:
if wide:
if Settings.COMPONENT_DEBUG.completions:
print('completions: returning global completions')
completions += cache_.global_completions()
else:
if Settings.COMPONENT_DEBUG.completions:
print('completions: returning file-specific completions')
completions += cache_.files.get(current_file_name, cache_.global_completions())
completions += self.keyword_completions(qsymbol.name)
sort_completions(completions)
return completions
def completions_for_module(self, project_name, module, filename):
"""
Returns completions for module
"""
retval = []
backend = BackendManager.active_backend()
if module:
mods = backend.scope_modules(project_name, filename, lookup=module, search_type='exact') if filename else []
mod_file = mods[0].location.filename if mods and mods[0].by_source() else None
cache_db = mods[0].location.db if mods and mods[0].by_cabal() else None
package = mods[0].location.package.name if mods and mods[0].by_cabal() else None
mod_decls = Utils.head_of(backend.module(project_name, lookup=module, search_type='exact', file=mod_file,
symdb=cache_db, package=package))
retval = make_completions(mod_decls.declarations.values()) if mod_decls else []
return retval
def get_import_completions(self, project_name, filename, _locations, line_contents):
# Autocompletion for import statements
if Settings.PLUGIN.auto_complete_imports:
self.current_filename = filename
match_import_list = Common.IMPORT_SYMBOL_RE.search(line_contents)
if match_import_list:
module_name = match_import_list.group('module')
return self.completions_for_module(project_name, module_name, self.current_filename)
match_import = IMPORT_RE_PREFIX.match(line_contents)
if match_import:
(_, prefix) = match_import.groups()
import_completions = list(set(self.get_module_completions_for(project_name, prefix)))
sort_completions(import_completions)
# Right after "import "? Propose "qualified" as well!
qualified_match = IMPORT_QUALIFIED_POSSIBLE_RE.match(line_contents)
if qualified_match:
qualified_prefix = qualified_match.group('qualifiedprefix')
if qualified_prefix == "" or "qualified".startswith(qualified_prefix):
import_completions.insert(0, (u"qualified", "qualified "))
return import_completions
return []
def get_lang_completions(self, project_name):
retval = []
if Settings.PLUGIN.auto_complete_language_pragmas:
retval = [[c, c] for c in BackendManager.active_backend().langs(project_name)]
sort_completions(retval)
return retval
def get_flag_completions(self, project_name):
retval = []
if Settings.PLUGIN.auto_complete_language_pragmas:
retval = [[c, c] for c in BackendManager.active_backend().flags(project_name)]
sort_completions(retval)
return retval
def get_module_completions_for(self, project_name, qualified_prefix, modules=None, current_dir=None):
def module_next_name(mname):
"""
Returns next name for prefix
pref = Control.Con, mname = Control.Concurrent.MVar, result = Concurrent.MVar
"""
suffix = mname.split('.')[(len(qualified_prefix.split('.')) - 1):]
# Sublime replaces full module name with suffix, if it contains no dots?
return suffix[0]
module_list = modules if modules else self.get_current_module_completions(project_name, current_dir)
return list(set((module_next_name(m) + '\tmodule', module_next_name(m))
for m in module_list if m.startswith(qualified_prefix)))
def get_current_module_completions(self, project_name, current_dir):
"""
Get modules, that are in scope of file/project
In case of file we just return 'scope modules' result
In case of dir we look for a related project or sandbox:
project - get dependent modules
sandbox - get sandbox modules
"""
backend = BackendManager.active_backend()
if self.current_filename:
return set([m.name for m in backend.scope_modules(project_name, self.current_filename)])
elif current_dir:
proj = backend.project(path=current_dir)
if proj and 'path' in proj:
return set([m.name for m in backend.list_modules(deps=proj['path'])])
sbox = backend.sandbox(path=current_dir)
if sbox and isinstance(sbox, dict) and 'sandbox' in sbox:
sbox = sbox.get('sandbox')
if sbox:
mods = backend.list_modules(sandbox=sbox) or []
return set([m.name for m in mods])
else:
mods = backend.list_modules(cabal=True) or []
return set([m.name for m in mods])