-
Notifications
You must be signed in to change notification settings - Fork 182
/
completion.py
252 lines (216 loc) · 10.5 KB
/
completion.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
import sublime
import sublime_plugin
try:
from typing import Any, List, Dict, Tuple, Callable, Optional
assert Any and List and Dict and Tuple and Callable and Optional
except ImportError:
pass
from .core.protocol import Request
from .core.events import global_events
from .core.settings import settings, client_configs
from .core.logging import debug
from .core.protocol import CompletionItemKind, Range
from .core.registry import session_for_view, client_for_view
from .core.configurations import is_supported_syntax
from .core.documents import get_document_position
from .core.sessions import Session
NO_COMPLETION_SCOPES = 'comment, string'
completion_item_kind_names = {v: k for k, v in CompletionItemKind.__dict__.items()}
class CompletionState(object):
IDLE = 0
REQUESTING = 1
APPLYING = 2
CANCELLING = 3
last_text_command = None
class CompletionHelper(sublime_plugin.EventListener):
def on_text_command(self, view, command_name, args):
global last_text_command
last_text_command = command_name
class CompletionHandler(sublime_plugin.ViewEventListener):
def __init__(self, view):
self.view = view
self.initialized = False
self.enabled = False
self.trigger_chars = [] # type: List[str]
self.state = CompletionState.IDLE
self.completions = [] # type: List[Any]
self.next_request = None # type: Optional[Tuple[str, List[int]]]
self.last_prefix = ""
self.last_location = 0
@classmethod
def is_applicable(cls, settings):
syntax = settings.get('syntax')
if syntax is not None:
return is_supported_syntax(syntax, client_configs.all)
else:
return False
def initialize(self):
self.initialized = True
session = session_for_view(self.view)
if session:
completionProvider = session.get_capability(
'completionProvider')
if completionProvider:
self.enabled = True
self.trigger_chars = completionProvider.get(
'triggerCharacters') or []
if self.trigger_chars:
self.register_trigger_chars(session)
def _view_language(self, config_name: str) -> 'Optional[str]':
languages = self.view.settings().get('lsp_language')
return languages.get(config_name) if languages else None
def register_trigger_chars(self, session: Session) -> None:
completion_triggers = self.view.settings().get('auto_complete_triggers', [])
view_language = self._view_language(session.config.name)
if view_language:
for language in session.config.languages:
if language.id == view_language:
for scope in language.scopes:
# debug("registering", self.trigger_chars, "for", scope)
scope_trigger = next(
(trigger for trigger in completion_triggers if trigger.get('selector', None) == scope),
None
)
if scope_trigger:
scope_trigger['characters'] = "".join(self.trigger_chars)
else:
completion_triggers.append({
'characters': "".join(self.trigger_chars),
'selector': scope
})
self.view.settings().set('auto_complete_triggers', completion_triggers)
def is_after_trigger_character(self, location):
if location > 0:
prev_char = self.view.substr(location - 1)
return prev_char in self.trigger_chars
def is_same_completion(self, prefix, locations):
# completion requests from the same location with the same prefix are cached.
current_start = locations[0] - len(prefix)
last_start = self.last_location - len(self.last_prefix)
return prefix.startswith(self.last_prefix) and current_start == last_start
def on_modified(self):
# hide completion when backspacing past last completion.
if self.view.sel()[0].begin() < self.last_location:
self.last_location = 0
self.view.run_command("hide_auto_complete")
# cancel current completion if the previous input is an space
prev_char = self.view.substr(self.view.sel()[0].begin() - 1)
if self.state == CompletionState.REQUESTING and prev_char.isspace():
self.state = CompletionState.CANCELLING
def on_query_completions(self, prefix, locations):
if prefix != "" and self.view.match_selector(locations[0], NO_COMPLETION_SCOPES):
# debug('discarding completion because no completion scope with prefix {}'.format(prefix))
return (
[],
sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS
)
if not self.initialized:
self.initialize()
if self.enabled:
reuse_completion = self.is_same_completion(prefix, locations)
if self.state == CompletionState.IDLE:
if not reuse_completion:
self.last_prefix = prefix
self.last_location = locations[0]
self.do_request(prefix, locations)
self.completions = []
elif self.state in (CompletionState.REQUESTING, CompletionState.CANCELLING):
self.next_request = (prefix, locations)
self.state = CompletionState.CANCELLING
elif self.state == CompletionState.APPLYING:
self.state = CompletionState.IDLE
return (
self.completions,
0 if not settings.only_show_lsp_completions
else sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS
)
def do_request(self, prefix: str, locations: 'List[int]'):
self.next_request = None
view = self.view
# don't store client so we can handle restarts
client = client_for_view(view)
if not client:
return
if settings.complete_all_chars or self.is_after_trigger_character(locations[0]):
global_events.publish("view.on_purge_changes", self.view)
document_position = get_document_position(view, locations[0])
if document_position:
client.send_request(
Request.complete(document_position),
self.handle_response,
self.handle_error)
self.state = CompletionState.REQUESTING
def format_completion(self, item: dict) -> 'Tuple[str, str]':
# Sublime handles snippets automatically, so we don't have to care about insertTextFormat.
if settings.prefer_label_over_filter_text:
trigger = item["label"]
else:
trigger = item.get("filterText", item["label"])
# choose hint based on availability and user preference
hint = None
if settings.completion_hint_type == "auto":
hint = item.get("detail")
if not hint:
kind = item.get("kind")
if kind:
hint = completion_item_kind_names[kind]
elif settings.completion_hint_type == "detail":
hint = item.get("detail")
elif settings.completion_hint_type == "kind":
kind = item.get("kind")
if kind:
hint = completion_item_kind_names.get(kind)
# label is an alternative for insertText if neither textEdit nor insertText is provided
replacement = self.text_edit_text(item) or item.get("insertText") or trigger
if len(replacement) > 0 and replacement[0] == '$': # sublime needs leading '$' escaped.
replacement = '\\$' + replacement[1:]
# only return trigger with a hint if available
return "\t ".join((trigger, hint)) if hint else trigger, replacement
def text_edit_text(self, item) -> 'Optional[str]':
text_edit = item.get("textEdit")
if text_edit:
edit_range, edit_text = text_edit.get("range"), text_edit.get("newText")
if edit_range and edit_text:
edit_range = Range.from_lsp(edit_range)
last_start = self.last_location - len(self.last_prefix)
last_row, last_col = self.view.rowcol(last_start)
if last_row == edit_range.start.row == edit_range.end.row and edit_range.start.col <= last_col:
# sublime does not support explicit replacement with completion
# at given range, but we try to trim the textEdit range and text
# to the start location of the completion
return edit_text[last_col - edit_range.start.col:]
return None
def handle_response(self, response: 'Optional[Dict]'):
if self.state == CompletionState.REQUESTING:
items = [] # type: List[Dict]
if isinstance(response, dict):
items = response["items"] or []
elif isinstance(response, list):
items = response
items = sorted(items, key=lambda item: item.get("sortText") or item["label"])
self.completions = list(self.format_completion(item) for item in items)
# if insert_best_completion was just ran, undo it before presenting new completions.
prev_char = self.view.substr(self.view.sel()[0].begin() - 1)
if prev_char.isspace():
if last_text_command == "insert_best_completion":
self.view.run_command("undo")
self.state = CompletionState.APPLYING
self.view.run_command("hide_auto_complete")
self.run_auto_complete()
elif self.state == CompletionState.CANCELLING:
self.state = CompletionState.IDLE
if self.next_request:
prefix, locations = self.next_request
self.do_request(prefix, locations)
else:
debug('Got unexpected response while in state {}'.format(self.state))
def handle_error(self, error: dict):
sublime.status_message('Completion error: ' + str(error.get('message')))
self.state = CompletionState.IDLE
def run_auto_complete(self):
self.view.run_command(
"auto_complete", {
'disable_auto_insert': True,
'api_completions_only': settings.only_show_lsp_completions,
'next_completion_if_showing': False
})