Skip to content

Commit e46cddd

Browse files
authored
Create a cache for snippets (#83)
1 parent 10def44 commit e46cddd

File tree

5 files changed

+178
-127
lines changed

5 files changed

+178
-127
lines changed

CONFIGURATION.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ This server can be configured using `workspace/didChangeConfiguration` method. E
2222
| `pylsp.plugins.jedi_completion.include_class_objects` | `boolean` | Adds class objects as a separate completion item. | `true` |
2323
| `pylsp.plugins.jedi_completion.fuzzy` | `boolean` | Enable fuzzy when requesting autocomplete. | `false` |
2424
| `pylsp.plugins.jedi_completion.eager` | `boolean` | Resolve documentation and detail eagerly. | `false` |
25-
| `pylsp.plugins.jedi_completion.resolve_at_most_labels` | `number` | How many labels (at most) should be resolved? | `25` |
26-
| `pylsp.plugins.jedi_completion.cache_labels_for` | `array` of `string` items | Modules for which the labels should be cached. | `["pandas", "numpy", "tensorflow", "matplotlib"]` |
25+
| `pylsp.plugins.jedi_completion.resolve_at_most` | `number` | How many labels and snippets (at most) should be resolved? | `25` |
26+
| `pylsp.plugins.jedi_completion.cache_for` | `array` of `string` items | Modules for which labels and snippets should be cached. | `["pandas", "numpy", "tensorflow", "matplotlib"]` |
2727
| `pylsp.plugins.jedi_definition.enabled` | `boolean` | Enable or disable the plugin. | `true` |
2828
| `pylsp.plugins.jedi_definition.follow_imports` | `boolean` | The goto call will follow imports. | `true` |
2929
| `pylsp.plugins.jedi_definition.follow_builtin_imports` | `boolean` | If follow_imports is True will decide if it follow builtin imports. | `true` |

pylsp/config/schema.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -104,18 +104,18 @@
104104
"default": false,
105105
"description": "Resolve documentation and detail eagerly."
106106
},
107-
"pylsp.plugins.jedi_completion.resolve_at_most_labels": {
107+
"pylsp.plugins.jedi_completion.resolve_at_most": {
108108
"type": "number",
109109
"default": 25,
110-
"description": "How many labels (at most) should be resolved?"
110+
"description": "How many labels and snippets (at most) should be resolved?"
111111
},
112-
"pylsp.plugins.jedi_completion.cache_labels_for": {
112+
"pylsp.plugins.jedi_completion.cache_for": {
113113
"type": "array",
114114
"items": {
115115
"type": "string"
116116
},
117117
"default": ["pandas", "numpy", "tensorflow", "matplotlib"],
118-
"description": "Modules for which the labels should be cached."
118+
"description": "Modules for which labels and snippets should be cached."
119119
},
120120
"pylsp.plugins.jedi_definition.enabled": {
121121
"type": "boolean",

pylsp/plugins/_resolvers.py

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright 2017-2020 Palantir Technologies, Inc.
2+
# Copyright 2021- Python Language Server Contributors.
3+
4+
from collections import defaultdict
5+
import logging
6+
from time import time
7+
8+
from jedi.api.classes import Completion
9+
10+
from pylsp import lsp
11+
12+
13+
log = logging.getLogger(__name__)
14+
15+
16+
# ---- Base class
17+
# -----------------------------------------------------------------------------
18+
class Resolver:
19+
20+
def __init__(self, callback, resolve_on_error, time_to_live=60 * 30):
21+
self.callback = callback
22+
self.resolve_on_error = resolve_on_error
23+
self._cache = {}
24+
self._time_to_live = time_to_live
25+
self._cache_ttl = defaultdict(set)
26+
self._clear_every = 2
27+
# see https://github.com/davidhalter/jedi/blob/master/jedi/inference/helpers.py#L194-L202
28+
self._cached_modules = {'pandas', 'numpy', 'tensorflow', 'matplotlib'}
29+
30+
@property
31+
def cached_modules(self):
32+
return self._cached_modules
33+
34+
@cached_modules.setter
35+
def cached_modules(self, new_value):
36+
self._cached_modules = set(new_value)
37+
38+
def clear_outdated(self):
39+
now = self.time_key()
40+
to_clear = [
41+
timestamp
42+
for timestamp in self._cache_ttl
43+
if timestamp < now
44+
]
45+
for time_key in to_clear:
46+
for key in self._cache_ttl[time_key]:
47+
del self._cache[key]
48+
del self._cache_ttl[time_key]
49+
50+
def time_key(self):
51+
return int(time() / self._time_to_live)
52+
53+
def get_or_create(self, completion: Completion):
54+
if not completion.full_name:
55+
use_cache = False
56+
else:
57+
module_parts = completion.full_name.split('.')
58+
use_cache = module_parts and module_parts[0] in self._cached_modules
59+
60+
if use_cache:
61+
key = self._create_completion_id(completion)
62+
if key not in self._cache:
63+
if self.time_key() % self._clear_every == 0:
64+
self.clear_outdated()
65+
66+
self._cache[key] = self.resolve(completion)
67+
self._cache_ttl[self.time_key()].add(key)
68+
return self._cache[key]
69+
70+
return self.resolve(completion)
71+
72+
def _create_completion_id(self, completion: Completion):
73+
return (
74+
completion.full_name, completion.module_path,
75+
completion.line, completion.column,
76+
self.time_key()
77+
)
78+
79+
def resolve(self, completion):
80+
try:
81+
sig = completion.get_signatures()
82+
return self.callback(completion, sig)
83+
except Exception as e: # pylint: disable=broad-except
84+
log.warning(
85+
'Something went wrong when resolving label for {completion}: {e}',
86+
completion=completion, e=e
87+
)
88+
return self.resolve_on_error
89+
90+
91+
# ---- Label resolver
92+
# -----------------------------------------------------------------------------
93+
def format_label(completion, sig):
94+
if sig and completion.type in ('function', 'method'):
95+
params = ', '.join(param.name for param in sig[0].params)
96+
label = '{}({})'.format(completion.name, params)
97+
return label
98+
return completion.name
99+
100+
101+
LABEL_RESOLVER = Resolver(callback=format_label, resolve_on_error='')
102+
103+
104+
# ---- Snippets resolver
105+
# -----------------------------------------------------------------------------
106+
def format_snippet(completion, sig):
107+
if not sig:
108+
return {}
109+
110+
snippet_completion = {}
111+
112+
positional_args = [param for param in sig[0].params
113+
if '=' not in param.description and
114+
param.name not in {'/', '*'}]
115+
116+
if len(positional_args) > 1:
117+
# For completions with params, we can generate a snippet instead
118+
snippet_completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
119+
snippet = completion.name + '('
120+
for i, param in enumerate(positional_args):
121+
snippet += '${%s:%s}' % (i + 1, param.name)
122+
if i < len(positional_args) - 1:
123+
snippet += ', '
124+
snippet += ')$0'
125+
snippet_completion['insertText'] = snippet
126+
elif len(positional_args) == 1:
127+
snippet_completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
128+
snippet_completion['insertText'] = completion.name + '($0)'
129+
else:
130+
snippet_completion['insertText'] = completion.name + '()'
131+
132+
return snippet_completion
133+
134+
135+
SNIPPET_RESOLVER = Resolver(callback=format_snippet, resolve_on_error={})

pylsp/plugins/jedi_completion.py

+19-117
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33

44
import logging
55
import os.path as osp
6-
from collections import defaultdict
7-
from time import time
86

9-
from jedi.api.classes import Completion
107
import parso
118

129
from pylsp import _utils, hookimpl, lsp
10+
from pylsp.plugins._resolvers import LABEL_RESOLVER, SNIPPET_RESOLVER
1311

1412
log = logging.getLogger(__name__)
1513

@@ -57,10 +55,11 @@ def pylsp_completions(config, document, position):
5755
should_include_params = settings.get('include_params')
5856
should_include_class_objects = settings.get('include_class_objects', True)
5957

60-
max_labels_resolve = settings.get('resolve_at_most_labels', 25)
61-
modules_to_cache_labels_for = settings.get('cache_labels_for', None)
62-
if modules_to_cache_labels_for is not None:
63-
LABEL_RESOLVER.cached_modules = modules_to_cache_labels_for
58+
max_to_resolve = settings.get('resolve_at_most', 25)
59+
modules_to_cache_for = settings.get('cache_for', None)
60+
if modules_to_cache_for is not None:
61+
LABEL_RESOLVER.cached_modules = modules_to_cache_for
62+
SNIPPET_RESOLVER.cached_modules = modules_to_cache_for
6463

6564
include_params = snippet_support and should_include_params and use_snippets(document, position)
6665
include_class_objects = snippet_support and should_include_class_objects and use_snippets(document, position)
@@ -70,7 +69,7 @@ def pylsp_completions(config, document, position):
7069
c,
7170
include_params,
7271
resolve=resolve_eagerly,
73-
resolve_label=(i < max_labels_resolve)
72+
resolve_label_or_snippet=(i < max_to_resolve)
7473
)
7574
for i, c in enumerate(completions)
7675
]
@@ -83,7 +82,7 @@ def pylsp_completions(config, document, position):
8382
c,
8483
False,
8584
resolve=resolve_eagerly,
86-
resolve_label=(i < max_labels_resolve)
85+
resolve_label_or_snippet=(i < max_to_resolve)
8786
)
8887
completion_dict['kind'] = lsp.CompletionItemKind.TypeParameter
8988
completion_dict['label'] += ' object'
@@ -175,9 +174,9 @@ def _resolve_completion(completion, d):
175174
return completion
176175

177176

178-
def _format_completion(d, include_params=True, resolve=False, resolve_label=False):
177+
def _format_completion(d, include_params=True, resolve=False, resolve_label_or_snippet=False):
179178
completion = {
180-
'label': _label(d, resolve_label),
179+
'label': _label(d, resolve_label_or_snippet),
181180
'kind': _TYPE_MAP.get(d.type),
182181
'sortText': _sort_text(d),
183182
'insertText': d.name
@@ -193,29 +192,8 @@ def _format_completion(d, include_params=True, resolve=False, resolve_label=Fals
193192
completion['insertText'] = path
194193

195194
if include_params and not is_exception_class(d.name):
196-
sig = d.get_signatures()
197-
if not sig:
198-
return completion
199-
200-
positional_args = [param for param in sig[0].params
201-
if '=' not in param.description and
202-
param.name not in {'/', '*'}]
203-
204-
if len(positional_args) > 1:
205-
# For completions with params, we can generate a snippet instead
206-
completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
207-
snippet = d.name + '('
208-
for i, param in enumerate(positional_args):
209-
snippet += '${%s:%s}' % (i + 1, param.name)
210-
if i < len(positional_args) - 1:
211-
snippet += ', '
212-
snippet += ')$0'
213-
completion['insertText'] = snippet
214-
elif len(positional_args) == 1:
215-
completion['insertTextFormat'] = lsp.InsertTextFormat.Snippet
216-
completion['insertText'] = d.name + '($0)'
217-
else:
218-
completion['insertText'] = d.name + '()'
195+
snippet = _snippet(d, resolve_label_or_snippet)
196+
completion.update(snippet)
219197

220198
return completion
221199

@@ -229,6 +207,13 @@ def _label(definition, resolve=False):
229207
return definition.name
230208

231209

210+
def _snippet(definition, resolve=False):
211+
if not resolve:
212+
return {}
213+
snippet = SNIPPET_RESOLVER.get_or_create(definition)
214+
return snippet
215+
216+
232217
def _detail(definition):
233218
try:
234219
return definition.parent().full_name or ''
@@ -244,86 +229,3 @@ def _sort_text(definition):
244229
# If its 'hidden', put it next last
245230
prefix = 'z{}' if definition.name.startswith('_') else 'a{}'
246231
return prefix.format(definition.name)
247-
248-
249-
class LabelResolver:
250-
251-
def __init__(self, format_label_callback, time_to_live=60 * 30):
252-
self.format_label = format_label_callback
253-
self._cache = {}
254-
self._time_to_live = time_to_live
255-
self._cache_ttl = defaultdict(set)
256-
self._clear_every = 2
257-
# see https://github.com/davidhalter/jedi/blob/master/jedi/inference/helpers.py#L194-L202
258-
self._cached_modules = {'pandas', 'numpy', 'tensorflow', 'matplotlib'}
259-
260-
@property
261-
def cached_modules(self):
262-
return self._cached_modules
263-
264-
@cached_modules.setter
265-
def cached_modules(self, new_value):
266-
self._cached_modules = set(new_value)
267-
268-
def clear_outdated(self):
269-
now = self.time_key()
270-
to_clear = [
271-
timestamp
272-
for timestamp in self._cache_ttl
273-
if timestamp < now
274-
]
275-
for time_key in to_clear:
276-
for key in self._cache_ttl[time_key]:
277-
del self._cache[key]
278-
del self._cache_ttl[time_key]
279-
280-
def time_key(self):
281-
return int(time() / self._time_to_live)
282-
283-
def get_or_create(self, completion: Completion):
284-
if not completion.full_name:
285-
use_cache = False
286-
else:
287-
module_parts = completion.full_name.split('.')
288-
use_cache = module_parts and module_parts[0] in self._cached_modules
289-
290-
if use_cache:
291-
key = self._create_completion_id(completion)
292-
if key not in self._cache:
293-
if self.time_key() % self._clear_every == 0:
294-
self.clear_outdated()
295-
296-
self._cache[key] = self.resolve_label(completion)
297-
self._cache_ttl[self.time_key()].add(key)
298-
return self._cache[key]
299-
300-
return self.resolve_label(completion)
301-
302-
def _create_completion_id(self, completion: Completion):
303-
return (
304-
completion.full_name, completion.module_path,
305-
completion.line, completion.column,
306-
self.time_key()
307-
)
308-
309-
def resolve_label(self, completion):
310-
try:
311-
sig = completion.get_signatures()
312-
return self.format_label(completion, sig)
313-
except Exception as e: # pylint: disable=broad-except
314-
log.warning(
315-
'Something went wrong when resolving label for {completion}: {e}',
316-
completion=completion, e=e
317-
)
318-
return ''
319-
320-
321-
def format_label(completion, sig):
322-
if sig and completion.type in ('function', 'method'):
323-
params = ', '.join(param.name for param in sig[0].params)
324-
label = '{}({})'.format(completion.name, params)
325-
return label
326-
return completion.name
327-
328-
329-
LABEL_RESOLVER = LabelResolver(format_label)

0 commit comments

Comments
 (0)