-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathlang_go.py
More file actions
519 lines (449 loc) · 19.3 KB
/
lang_go.py
File metadata and controls
519 lines (449 loc) · 19.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
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
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
#!/usr/bin/env python
"""Go language support for codeintel."""
from __future__ import absolute_import
import os
import sys
import json
import logging
import process
import time
try:
from codeintel2.common import LazyClassAttribute
except ImportError:
import warnings
warnings.warn("Unable to import zope.cachedescriptors.property")
# Fallback to regular properties.
LazyClassAttribute = property
import ciElementTree as ET
import which
import SilverCity
from SilverCity.Lexer import Lexer
from SilverCity import ScintillaConstants
from codeintel2.accessor import AccessorCache
from codeintel2.citadel import CitadelLangIntel, CitadelBuffer
from codeintel2.common import Trigger, TRG_FORM_CALLTIP, TRG_FORM_CPLN, TRG_FORM_DEFN, CILEDriver, Definition, CodeIntelError
from codeintel2.langintel import ParenStyleCalltipIntelMixin, ProgLangTriggerIntelMixin, PythonCITDLExtractorMixin
from codeintel2.udl import UDLBuffer
from codeintel2.tree import tree_from_cix
from SilverCity.ScintillaConstants import (
SCE_C_COMMENT, SCE_C_COMMENTDOC, SCE_C_COMMENTDOCKEYWORD,
SCE_C_COMMENTDOCKEYWORDERROR, SCE_C_COMMENTLINE,
SCE_C_COMMENTLINEDOC, SCE_C_DEFAULT, SCE_C_IDENTIFIER, SCE_C_NUMBER,
SCE_C_OPERATOR, SCE_C_STRING, SCE_C_CHARACTER, SCE_C_STRINGEOL, SCE_C_WORD)
try:
from xpcom.server import UnwrapObject
_xpcom_ = True
except ImportError:
_xpcom_ = False
#---- globals
lang = "Go"
log = logging.getLogger("codeintel.go")
# log.setLevel(logging.DEBUG)
try:
sys.path.append(os.path.dirname(__file__))
from langinfo_go import GoLangInfo
except:
class GoLangInfo:
reserved_keywords = set([])
predeclared_identifiers = set([])
predeclared_functions = set([])
default_encoding = "utf-8"
import styles
if lang not in styles.StateMap:
map = styles.StateMap['C++'].copy()
styles.addSharedStyles(map)
styles.StateMap[lang] = map
finally:
sys.path.pop()
#---- Lexer class
class GoLexer(Lexer):
lang = lang
def __init__(self):
self._properties = SilverCity.PropertySet()
self._lexer = SilverCity.find_lexer_module_by_id(ScintillaConstants.SCLEX_CPP)
self._keyword_lists = [
SilverCity.WordList(' '.join(sorted(GoLangInfo.reserved_keywords))),
SilverCity.WordList(' '.join(
sorted(GoLangInfo.predeclared_identifiers.
union(GoLangInfo.predeclared_functions)))),
]
#---- LangIntel class
class GoLangIntel(CitadelLangIntel,
ParenStyleCalltipIntelMixin,
ProgLangTriggerIntelMixin,
PythonCITDLExtractorMixin):
lang = lang
citdl_from_literal_type = {"string": "string"}
calltip_trg_chars = tuple('(')
trg_chars = tuple(' (."')
completion_name_mapping = {
'var': 'variable',
'func': 'function',
'package': 'module',
'type': 'class',
'const': 'constant',
}
def codeintel_type_from_completion_data(self, completion_entry):
"""Given a dictionary containing 'class' and 'type' keys return a
codeintel type. Used for selecting icon in completion list.
"""
completion_type = self.completion_name_mapping.get(completion_entry['class']) or completion_entry['class']
if completion_entry['type'].startswith('[]'):
completion_type = '@variable'
elif completion_entry['type'].startswith('map['):
completion_type = '%variable'
return completion_type
def trg_from_pos(self, buf, pos, implicit=True,
lang=None):
log.debug("trg_from_pos(pos=%r)", pos)
if pos < 2:
return None
accessor = buf.accessor
last_pos = pos - 1
last_char = accessor.char_at_pos(last_pos)
if last_char == '.': # must be "complete-object-members" or None
log.debug(" triggered 'complete-object-members'")
return Trigger(self.lang, TRG_FORM_CPLN,
"object-members", pos, implicit)
elif last_char == '(':
log.debug(" triggered 'calltip-call-signature'")
return Trigger(self.lang, TRG_FORM_CALLTIP, "call-signature", pos, implicit)
elif last_char in '\'"`' or last_char == "@":
# Check if it's an import
log.debug(" checking for import statement")
ac = AccessorCache(accessor, pos, fetchsize=100)
prev_style = accessor.style_at_pos(last_pos-1)
if prev_style == SCE_C_STRING:
# It's the end of a string then, not what we are looking for.
return False
# Bug in Komodo 8 - peek at the previous style to ensure the cache
# is primed. No needed in Komodo 9 onwards.
p, ch, style = ac.peekPrevPosCharStyle()
log.debug(" p1 %r, ch %r, style %r", p, ch, style)
loops_left = 100
while loops_left:
loops_left -= 1
p, ch, style = ac.getPrecedingPosCharStyle(style)
log.debug(" p %r, ch %r, style %r", p, ch, style)
if p is None:
break
if style == SCE_C_WORD:
p, text = ac.getTextBackWithStyle(style)
log.debug(" p %r, text %r", p, text)
if text == "import":
log.debug(" triggered 'complete-imports'")
return Trigger(self.lang, TRG_FORM_CPLN, "imports", pos, implicit)
break
elif style not in (SCE_C_DEFAULT, SCE_C_OPERATOR, SCE_C_STRING,
SCE_C_COMMENT, SCE_C_COMMENTDOC, SCE_C_COMMENTLINE):
break
log.debug(" triggered 'complete-any'")
return Trigger(self.lang, TRG_FORM_CPLN, "any", pos, implicit)
def preceding_trg_from_pos(self, buf, pos, curr_pos, preceding_trg_terminators=None, DEBUG=False):
#DEBUG = True
if DEBUG:
log.debug("pos: %d", pos)
log.debug("ch: %r", buf.accessor.char_at_pos(pos))
log.debug("curr_pos: %d", curr_pos)
if pos != curr_pos and self._last_trg_type == "names":
# The last trigger type was a 3-char trigger "names", we must try
# triggering from the same point as before to get other available
# trigger types defined at the same poisition or before.
trg = ProgLangTriggerIntelMixin.preceding_trg_from_pos(
self, buf, pos+2, curr_pos, preceding_trg_terminators,
DEBUG=DEBUG)
else:
trg = ProgLangTriggerIntelMixin.preceding_trg_from_pos(
self, buf, pos, curr_pos, preceding_trg_terminators,
DEBUG=DEBUG)
names_trigger = None
style = None
if pos > 0:
accessor = buf.accessor
if pos == curr_pos:
# We actually care about whats left of the cursor.
pos -= 1
style = accessor.style_at_pos(pos)
if DEBUG:
style_names = buf.style_names_from_style_num(style)
log.debug(" style: %s (%s)", style, ", ".join(style_names))
if style in (1,2):
ac = AccessorCache(accessor, pos)
prev_pos, prev_ch, prev_style = ac.getPrecedingPosCharStyle(style)
if prev_style is not None and (pos - prev_pos) > 3:
# We need at least 3 character for proper completion handling.
names_trigger = self.trg_from_pos(buf, prev_pos + 4, implicit=False)
if DEBUG:
log.debug("trg: %r", trg)
log.debug("names_trigger: %r", names_trigger)
log.debug("last_trg_type: %r", self._last_trg_type)
if names_trigger:
if not trg:
trg = names_trigger
# Two triggers, choose the best one.
elif trg.pos == names_trigger.pos:
if self._last_trg_type != "names":
# The names trigger gets priority over the other trigger
# types, unless the previous trigger was also a names trg.
trg = names_trigger
elif trg.pos < names_trigger.pos:
trg = names_trigger
if trg:
self._last_trg_type = trg.type
return trg
def async_eval_at_trg(self, buf, trg, ctlr):
# if a definition lookup, use godef
log.debug('async_eval_at_trg:: %r' % (trg, ))
if trg.form == TRG_FORM_DEFN:
return self.lookup_defn(buf, trg, ctlr)
elif trg.name == "go-complete-imports":
return self.available_imports(buf, trg, ctlr)
# otherwise use gocode
return self.invoke_gocode(buf, trg, ctlr)
def lookup_defn(self, buf, trg, ctlr):
env = buf.env
godef_path = self._get_gotool('godef', env)
# We can't store the path and watch prefs because this isn't a
# true xpcom object.
if godef_path is None:
godef_path = 'godef'
cmd = [godef_path, '-i=true', '-t=true', '-f=%s' % buf.path, '-o=%s' % trg.pos]
log.debug("running [%s]", cmd)
p = process.ProcessOpen(cmd, env=buf.env.get_all_envvars())
output, error = p.communicate(buf.accessor.text)
if error:
log.debug("'gocode' stderr: [%s]", error)
raise CodeIntelError(error)
lines = output.splitlines()
log.debug(output)
defparts = lines[0].rsplit(":",2)
if len(defparts) == 2:
# current file
path = buf.path
line = defparts[0]
else:
# other file
path = defparts[0]
line = defparts[1]
name, typeDesc = lines[1].split(' ', 1)
d = Definition("Go",path,
blobname=None,
lpath=None,
name=name,
line=line,
ilk='function' if typeDesc.startswith('func') else typeDesc,
citdl=None,
signature=typeDesc,
doc='\n'.join(lines[1:]),
)
log.debug(d)
ctlr.start(buf, trg)
ctlr.set_defns([d])
ctlr.done("success")
_packages_from_exe = {}
def available_imports(self, buf, trg, ctlr):
env = buf.env
go_exe = self.get_go_exe(env)
if not go_exe:
raise CodeIntelError("Unable to locate go executable")
if go_exe not in self._packages_from_exe:
cmd = [go_exe, 'list', 'std']
cwd = None
if buf.path != "<Unsaved>":
cwd = os.path.dirname(buf.path)
env_vars = env.get_all_envvars()
log.debug("running cmd %r", cmd)
try:
p = process.ProcessOpen(cmd, cwd=cwd, env=env_vars)
except OSError as e:
raise CodeIntelError("Error executing '%s': %s" % (cmd, e))
output, error = p.communicate()
if error:
log.warn("cmd %r error [%s]", cmd, error)
raise CodeIntelError(error)
package_names = [x.strip() for x in output.splitlines() if x]
log.debug("retrieved %d package names", len(package_names))
self._packages_from_exe[go_exe] = package_names
stdlib_imports = self._packages_from_exe.get(go_exe, [])
ctlr.start(buf, trg)
ctlr.set_cplns([("import", name) for name in stdlib_imports])
ctlr.done("success")
def invoke_gocode(self, buf, trg, ctlr):
pos = trg.pos
if trg.type == "call-signature":
pos = pos - 1
env = buf.env
gocode_path = self._get_gotool('gocode', buf.env)
# We can't store the path and watch prefs because this isn't a
# true xpcom object.
if gocode_path is None:
gocode_path = 'gocode'
cmd = [gocode_path, '-f=json', 'autocomplete', buf.path, '%s' % pos]
log.debug("running [%s]", cmd)
try:
p = process.ProcessOpen(cmd, env=env.get_all_envvars())
except OSError as e:
log.error("Error executing '%s': %s", cmd[0], e)
return
output, error = p.communicate(buf.accessor.text)
if error:
log.warn("'%s' stderr: [%s]", cmd[0], error)
try:
completion_data = json.loads(output)
log.debug('full completion_data: %r', completion_data)
completion_data = completion_data[1]
except IndexError:
# exit on empty gocode output
return
except ValueError as e:
log.exception('Exception while parsing json')
return
ctlr.start(buf, trg)
completion_data = [x for x in completion_data if x['class'] != 'PANIC'] # remove PANIC entries if present
if not completion_data:
# Only contained PANIC entries.
ctlr.error("no valid response from gocode: check gocode is running")
ctlr.done("error")
return
if trg.type == "object-members":
ctlr.set_cplns([(self.codeintel_type_from_completion_data(entry), entry['name']) for entry in completion_data])
ctlr.done("success")
elif trg.type == "call-signature":
entry = completion_data[0]
ctlr.set_calltips(['%s %s' % (entry['name'], entry['type'])])
ctlr.done("success")
elif trg.type == "any" and trg.implicit == False:
ctlr.set_cplns([(self.codeintel_type_from_completion_data(entry), entry['name']) for entry in completion_data])
ctlr.done("success")
def get_go_exe(self, env):
golang_path = env.get_pref("golangDefaultLocation", "")
if golang_path and golang_path != "":
return golang_path
path = [d.strip()
for d in env.get_envvar("PATH", "").split(os.pathsep)
if d.strip()]
try:
return which.which('go', path=path)
except which.WhichError:
return 'go' # Might as well go for broke
def _get_gotool(self, tool_name, env):
# First try the pref
# Then try which
# Finally try relative to the golang executable
tool_path = env.get_pref(tool_name + "DefaultLocation", "")
if tool_path and os.path.exists(tool_path):
return tool_path
path = [d.strip()
for d in env.get_envvar("PATH", "").split(os.pathsep)
if d.strip()]
try:
return which.which(tool_name, path=path)
except which.WhichError:
pass
go_exe = self.get_go_exe(env)
if go_exe:
ext = sys.platform.startswith("win") and ".exe" or ""
tool_path = os.path.join(os.path.dirname(go_exe), tool_name + ext)
if os.path.exists(tool_path):
return tool_path
return None
#---- Buffer class
class GoBuffer(CitadelBuffer):
lang = lang
# '/' removed as import packages use that
cpln_fillup_chars = "~`!@#$%^&()-=+{}[]|\\;:'\",.<>? "
cpln_stop_chars = "~`!@#$%^&*()-=+{}[]|\\;:'\",.<>? "
ssl_lang = lang
# The ScintillaConstants all start with this prefix:
sce_prefixes = ["SCE_C_"]
#---- CILE Driver class
class GoCILEDriver(CILEDriver):
lang = lang
_gooutline_executable_and_error = None
@LazyClassAttribute
def golib_dir(self):
ext_path = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
return os.path.join(ext_path, "golib")
def compile_gooutline(self, buf):
go_exe = buf.langintel.get_go_exe(buf.env)
if not go_exe:
raise CodeIntelError("Unable to locate go executable")
if self._gooutline_executable_and_error is None or go_exe != self._gooutline_executable_and_error[0]:
self._gooutline_executable_and_error = (go_exe, None, "Unknown Error")
import tempfile
tempdir = tempfile.gettempdir()
outline_exe = os.path.join(tempdir, "outline")
if sys.platform.startswith("win"):
outline_exe += ".exe"
outline_src = os.path.join(self.golib_dir, "outline.go")
cmd = [go_exe, "build", outline_src]
cwd = tempdir
env = buf.env.get_all_envvars()
try:
# Compile the executable.
p = process.ProcessOpen(cmd, cwd=cwd, env=env, stdin=None)
output, error = p.communicate()
if error:
log.warn("'%s' stderr: [%s]", cmd, error)
# Remember the executable.
self._gooutline_executable_and_error = (go_exe, outline_exe, None)
except Exception as ex:
error_message = "Unable to compile 'outline.go'" + str(ex)
self._gooutline_executable_and_error = (go_exe, None, error_message)
if self._gooutline_executable_and_error[1]:
return self._gooutline_executable_and_error[1]
raise CodeIntelError(self._gooutline_executable_and_error[2])
def scan_purelang(self, buf, mtime=None, lang="Go"):
"""Scan the given GoBuffer return an ElementTree (conforming
to the CIX schema) giving a summary of its code elements.
@param buf {GoBuffer} is the Go buffer to scan
@param mtime {int} is a modified time for the file (in seconds since
the "epoch"). If it is not specified the _current_ time is used.
Note that the default is not to stat() the file and use that
because the given content might not reflect the saved file state.
"""
# Dev Notes:
# - This stub implementation of the Go CILE return an "empty"
# summary for the given content, i.e. CIX content that says "there
# are no code elements in this Go content".
# - Use the following command (in the extension source dir) to
# debug/test your scanner:
# codeintel scan -p -l Go <example-Go-file>
# "codeintel" is a script available in the Komodo SDK.
log.info("scan '%s'", buf.path)
if mtime is None:
mtime = int(time.time())
# The 'path' attribute must use normalized dir separators.
if sys.platform.startswith("win"):
path = buf.path.replace('\\', '/')
else:
path = buf.path
try:
gooutline_exe_path = self.compile_gooutline(buf)
except Exception as e:
log.error("Error compiling outline: %s", e)
raise
cmd = [gooutline_exe_path, buf.path]
env = buf.env.get_all_envvars()
log.debug("running [%s]", cmd)
try:
p = process.ProcessOpen(cmd, env=env)
except OSError as e:
log.error("Error executing '%s': %s", cmd, e)
return
output, error = p.communicate()
if error:
log.warn("'%s' stderr: [%s]", cmd[0], error)
xml = '<codeintel version="2.0">\n' + output + '</codeintel>'
return tree_from_cix(xml)
#---- registration
def register(mgr):
"""Register language support with the Manager."""
mgr.set_lang_info(
lang,
silvercity_lexer=GoLexer(),
buf_class=GoBuffer,
langintel_class=GoLangIntel,
import_handler_class=None,
cile_driver_class=GoCILEDriver,
is_cpln_lang=True)