-
Notifications
You must be signed in to change notification settings - Fork 658
/
Copy pathgocode.py
229 lines (186 loc) · 5.6 KB
/
gocode.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
import sublime, sublime_plugin, subprocess, difflib
# go to balanced pair, e.g.:
# ((abc(def)))
# ^
# \--------->^
#
# returns -1 on failure
def skip_to_balanced_pair(str, i, open, close):
count = 1
i += 1
while i < len(str):
if str[i] == open:
count += 1
elif str[i] == close:
count -= 1
if count == 0:
break
i += 1
if i >= len(str):
return -1
return i
# split balanced parens string using comma as separator
# e.g.: "ab, (1, 2), cd" -> ["ab", "(1, 2)", "cd"]
# filters out empty strings
def split_balanced(s):
out = []
i = 0
beg = 0
while i < len(s):
if s[i] == ',':
out.append(s[beg:i].strip())
beg = i+1
i += 1
elif s[i] == '(':
i = skip_to_balanced_pair(s, i, "(", ")")
if i == -1:
i = len(s)
else:
i += 1
out.append(s[beg:i].strip())
return list(filter(bool, out))
def extract_arguments_and_returns(sig):
sig = sig.strip()
if not sig.startswith("func"):
return [], []
# find first pair of parens, these are arguments
beg = sig.find("(")
if beg == -1:
return [], []
end = skip_to_balanced_pair(sig, beg, "(", ")")
if end == -1:
return [], []
args = split_balanced(sig[beg+1:end])
# find the rest of the string, these are returns
sig = sig[end+1:].strip()
sig = sig[1:-1] if sig.startswith("(") and sig.endswith(")") else sig
returns = split_balanced(sig)
return args, returns
# takes gocode's candidate and returns sublime's hint and subj
def hint_and_subj(cls, name, type):
subj = name
if cls == "func":
hint = cls + " " + name
args, returns = extract_arguments_and_returns(type)
if returns:
hint += "\t" + ", ".join(returns)
if args:
sargs = []
for i, a in enumerate(args):
ea = a.replace("{", "\\{").replace("}", "\\}")
sargs.append("${{{0}:{1}}}".format(i+1, ea))
subj += "(" + ", ".join(sargs) + ")"
else:
subj += "()"
else:
hint = cls + " " + name + "\t" + type
return hint, subj
def diff_sanity_check(a, b):
if a != b:
raise Exception("diff sanity check mismatch\n-%s\n+%s" % (a, b))
class GocodeGofmtCommand(sublime_plugin.TextCommand):
def run(self, edit):
view = self.view
src = view.substr(sublime.Region(0, view.size()))
gofmt = subprocess.Popen(["gofmt"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
sout, serr = gofmt.communicate(src.encode())
if gofmt.returncode != 0:
print(serr.decode(), end="")
return
newsrc = sout.decode()
diff = difflib.ndiff(src.splitlines(), newsrc.splitlines())
i = 0
for line in diff:
if line.startswith("?"): # skip hint lines
continue
l = (len(line)-2)+1
if line.startswith("-"):
diff_sanity_check(view.substr(sublime.Region(i, i+l-1)), line[2:])
view.erase(edit, sublime.Region(i, i+l))
elif line.startswith("+"):
view.insert(edit, i, line[2:]+"\n")
i += l
else:
diff_sanity_check(view.substr(sublime.Region(i, i+l-1)), line[2:])
i += l
class Gocode(sublime_plugin.EventListener):
"""Sublime Text gocode integration."""
def __init__(self):
self._running = False
self._completions = None
self._location = 0
self._prefix = ""
def fetch_query_completions(self, view, prefix, location):
"""Fetches the query completions of for the given location
Execute gocode and parse the returned csv. If the cursor location did not change, a
result got returned and the current cursor location is still at the same position will the query completions
method be called again (to render the results).
:param view: currently active sublime view
:type view: sublime.View
:param prefix: string for completions
:type prefix: basestring
:param locations: offset from beginning
:type locations: int
"""
self._running = True
self._location = location
src = view.substr(sublime.Region(0, view.size()))
filename = view.file_name()
cloc = "c{0}".format(location)
gocode = subprocess.Popen(["gocode", "-f=csv", "autocomplete", filename, cloc],
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
out = gocode.communicate(src.encode())[0].decode()
result = []
for line in filter(bool, out.split("\n")):
arg = line.split(",,")
hint, subj = hint_and_subj(arg[0], arg[1], arg[2])
result.append([hint, subj])
# Exit conditions:
if len(result) == 0:
return
if self._prefix != prefix:
return
# Check if this query completions request is for the "latest" location
if self._location != location:
return
self._completions = result
self._running = False
self.open_query_completions(view)
def open_query_completions(self, view):
"""Opens (forced) the sublime autocomplete window"""
view.run_command("hide_auto_complete")
sublime.set_timeout(
lambda: view.run_command("auto_complete")
)
def on_query_completions(self, view, prefix, locations):
"""Sublime autocomplete event handler.
Get completions depends on current cursor position and return
them as list of ('possible completion', 'completion type')
:param view: currently active sublime view
:type view: sublime.View
:param prefix: string for completions
:type prefix: basestring
:param locations: offset from beginning
:type locations: int
:return: list of tuple(str, str)
"""
loc = locations[0]
if not view.match_selector(loc, "source.go"):
return []
if self._completions:
completions = self._completions
self._completions = None
self._prefix = ""
return completions
if self._running and len(prefix) != 0:
return []
self._prefix = prefix
sublime.set_timeout_async(
lambda: self.fetch_query_completions(view, prefix, loc)
)
return []
def on_pre_save(self, view):
if not view.match_selector(0, "source.go"):
return
view.run_command('gocode_gofmt')