13 class RedirectStdout(object):
14 def __init__(self, new_stdout=None):
15 """If stdout is None, redirect to /dev/null"""
16 self._new_stdout = new_stdout or open(os.devnull, "w")
20 self.oldstdout_fno = os.dup(sys.stdout.fileno())
21 os.dup2(self._new_stdout.fileno(), 1)
23 def __exit__(self, exc_type, exc_value, traceback):
24 self._new_stdout.flush()
25 os.dup2(self.oldstdout_fno, 1)
26 os.close(self.oldstdout_fno)
29 class JediCompletion(object):
32 "instance": "variable",
38 self.default_sys_path = sys.path
39 self.environment = jedi.api.environment.create_environment(
40 sys.executable, safe=False
42 self._input = io.open(sys.stdin.fileno(), encoding="utf-8")
43 if (os.path.sep == "/") and (platform.uname()[2].find("Microsoft") > -1):
44 # WSL; does not support UNC paths
45 self.drive_mount = "/mnt/"
46 elif sys.platform == "cygwin":
48 self.drive_mount = "/cygdrive/"
50 # Do no normalization, e.g. Windows build of Python.
51 # Could add additional test: ((os.path.sep == '/') and os.path.isdir('/mnt/c'))
52 # However, this may have more false positives trying to identify Windows/*nix hybrids
55 def _get_definition_type(self, definition):
56 # if definition.type not in ['import', 'keyword'] and is_built_in():
59 if definition.type in ["statement"] and definition.name.isupper():
61 return self.basic_types.get(definition.type, definition.type)
65 def _additional_info(self, completion):
66 """Provide additional information about the completion object."""
67 if not hasattr(completion, "_definition") or completion._definition is None:
69 if completion.type == "statement":
70 nodes_to_display = ["InstanceElement", "String", "Node", "Lambda", "Number"]
73 for c in completion._definition.children
74 if type(c).__name__ in nodes_to_display
79 def _get_top_level_module(cls, path):
80 """Recursively walk through directories looking for top level module.
82 Jedi will use current filepath to look for another modules at same
83 path, but it will not be able to see modules **above**, so our goal
84 is to find the higher python module available from filepath.
86 _path, _ = os.path.split(path)
87 if os.path.isfile(os.path.join(_path, "__init__.py")):
88 return cls._get_top_level_module(_path)
91 def _generate_signature(self, completion):
92 """Generate signature with function arguments.
94 if completion.type in ["module"] or not hasattr(completion, "params"):
98 ", ".join(p.description[6:] for p in completion.params if p),
101 def _get_call_signatures(self, script, line, column):
102 """Extract call signatures from jedi.api.Script object in failsafe way.
105 Tuple with original signature object, name and value.
109 call_signatures = script.get_signatures(line, column)
114 for signature in call_signatures:
115 for pos, param in enumerate(signature.params):
119 name = self._get_param_name(param)
120 if param.name == "self" and pos == 0:
122 if name.startswith("*"):
125 value = self._get_param_value(param)
126 _signatures.append((signature, name, value))
129 def _get_param_name(self, p):
130 if p.name.startswith("param "):
131 return p.name[6:] # drop leading 'param '
134 def _get_param_value(self, p):
135 pair = p.description.split("=")
140 def _get_call_signatures_with_args(self, script, line, column):
141 """Extract call signatures from jedi.api.Script object in failsafe way.
144 Array with dictionary
148 call_signatures = script.get_signatures(line, column)
151 for signature in call_signatures:
160 sig["description"] = signature.description
162 sig["docstring"] = signature.docstring()
163 sig["raw_docstring"] = signature.docstring(raw=True)
165 sig["docstring"] = ""
166 sig["raw_docstring"] = ""
168 sig["name"] = signature.name
169 sig["paramindex"] = signature.index
170 sig["bracketstart"].append(signature.index)
172 _signatures.append(sig)
173 for pos, param in enumerate(signature.params):
177 name = self._get_param_name(param)
178 if param.name == "self" and pos == 0:
181 value = self._get_param_value(param)
184 paramDocstring = param.docstring()
188 sig["params"].append(
192 "docstring": paramDocstring,
193 "description": param.description,
198 def _serialize_completions(self, script, line, column, identifier=None, prefix=""):
199 """Serialize response to be read from VSCode.
202 script: Instance of jedi.api.Script object.
203 identifier: Unique completion identifier to pass back to VSCode.
204 prefix: String with prefix to filter function arguments.
205 Used only when fuzzy matcher turned off.
208 Serialized string to send to VSCode.
212 for signature, name, value in self._get_call_signatures(script, line, column):
213 if not self.fuzzy_matcher and not name.lower().startswith(prefix.lower()):
218 "rightLabel": self._additional_info(signature),
220 _completion["description"] = ""
221 _completion["raw_docstring"] = ""
223 # we pass 'text' here only for fuzzy matcher
225 _completion["snippet"] = "%s=${1:%s}$0" % (name, value)
226 _completion["text"] = "%s=" % (name)
228 _completion["snippet"] = "%s=$1$0" % name
229 _completion["text"] = name
230 _completion["displayText"] = name
231 _completions.append(_completion)
234 completions = script.complete(line, column)
239 for completion in completions:
242 "text": completion.name,
243 "type": self._get_definition_type(completion),
244 "raw_type": completion.type,
245 "rightLabel": self._additional_info(completion),
250 for c in _completions:
251 if c["text"] == _completion["text"]:
252 c["type"] = _completion["type"]
253 c["raw_type"] = _completion["raw_type"]
256 [c["text"].split("=")[0] == _completion["text"] for c in _completions]
258 # ignore function arguments we already have
260 _completions.append(_completion)
261 return json.dumps({"id": identifier, "results": _completions})
263 def _serialize_methods(self, script, line, column, identifier=None, prefix=""):
266 completions = script.complete(line, column)
270 for completion in completions:
271 if completion.name == "__autocomplete_python":
272 instance = completion.parent().name
275 instance = "self.__class__"
277 for completion in completions:
279 if hasattr(completion, "params"):
280 params = [p.description for p in completion.params if p]
281 if completion.parent().type == "class":
284 "parent": completion.parent().name,
285 "instance": instance,
286 "name": completion.name,
288 "moduleName": completion.module_name,
289 "fileName": completion.module_path,
290 "line": completion.line,
291 "column": completion.column,
294 return json.dumps({"id": identifier, "results": _methods})
296 def _serialize_arguments(self, script, line, column, identifier=None):
297 """Serialize response to be read from VSCode.
300 script: Instance of jedi.api.Script object.
301 identifier: Unique completion identifier to pass back to VSCode.
304 Serialized string to send to VSCode.
309 "results": self._get_call_signatures_with_args(script, line, column),
313 def _top_definition(self, definition):
314 for d in definition.goto_assignments():
317 if d.type == "import":
318 return self._top_definition(d)
323 def _extract_range_jedi_0_11_1(self, definition):
324 from parso.utils import split_lines
326 # get the scope range
328 if definition.type in ["class", "function"]:
329 tree_name = definition._name.tree_name
330 scope = tree_name.get_definition()
331 start_line = scope.start_pos[0] - 1
332 start_column = scope.start_pos[1]
334 code = scope.get_code(include_prefix=False)
335 lines = split_lines(code)
337 lines = "\n".join(lines).rstrip().split("\n")
338 end_line = start_line + len(lines) - 1
339 end_column = len(lines[-1]) - 1
341 symbol = definition._name.tree_name
342 start_line = symbol.start_pos[0] - 1
343 start_column = symbol.start_pos[1]
344 end_line = symbol.end_pos[0] - 1
345 end_column = symbol.end_pos[1]
347 "start_line": start_line,
348 "start_column": start_column,
349 "end_line": end_line,
350 "end_column": end_column,
352 except Exception as e:
354 "start_line": definition.line - 1,
355 "start_column": definition.column,
356 "end_line": definition.line - 1,
357 "end_column": definition.column,
360 def _extract_range(self, definition):
361 """Provides the definition range of a given definition
363 For regular symbols it returns the start and end location of the
364 characters making up the symbol.
366 For scoped containers it will return the entire definition of the
369 The scope that jedi provides ends with the first character of the next
370 scope so it's not ideal. For vscode we need the scope to end with the
371 last character of actual code. That's why we extract the lines that
372 make up our scope and trim the trailing whitespace.
374 return self._extract_range_jedi_0_11_1(definition)
376 def _get_definitionsx(self, definitions, identifier=None, ignoreNoModulePath=False):
377 """Serialize response to be read from VSCode.
380 definitions: List of jedi.api.classes.Definition objects.
381 identifier: Unique completion identifier to pass back to VSCode.
384 Serialized string to send to VSCode.
387 for definition in definitions:
389 if definition.type == "import":
390 definition = self._top_definition(definition)
398 if hasattr(definition, "module_path") and definition.module_path:
399 module_path = definition.module_path
400 definitionRange = self._extract_range(definition)
402 if not ignoreNoModulePath:
405 parent = definition.parent()
406 container = parent.name if parent.type != "module" else ""
411 docstring = definition.docstring()
412 rawdocstring = definition.docstring(raw=True)
417 "text": definition.name,
418 "type": self._get_definition_type(definition),
419 "raw_type": definition.type,
420 "fileName": str(module_path),
421 "container": container,
422 "range": definitionRange,
423 "description": definition.description,
424 "docstring": docstring,
425 "raw_docstring": rawdocstring,
426 "signature": self._generate_signature(definition),
428 _definitions.append(_definition)
429 except Exception as e:
433 def _serialize_definitions(self, definitions, identifier=None):
434 """Serialize response to be read from VSCode.
437 definitions: List of jedi.api.classes.Definition objects.
438 identifier: Unique completion identifier to pass back to VSCode.
441 Serialized string to send to VSCode.
444 for definition in definitions:
446 if definition.module_path:
447 if definition.type == "import":
448 definition = self._top_definition(definition)
449 if not definition.module_path:
452 parent = definition.parent()
453 container = parent.name if parent.type != "module" else ""
458 docstring = definition.docstring()
459 rawdocstring = definition.docstring(raw=True)
464 "text": definition.name,
465 "type": self._get_definition_type(definition),
466 "raw_type": definition.type,
467 "fileName": definition.module_path,
468 "container": container,
469 "range": self._extract_range(definition),
470 "description": definition.description,
471 "docstring": docstring,
472 "raw_docstring": rawdocstring,
474 _definitions.append(_definition)
475 except Exception as e:
477 return json.dumps({"id": identifier, "results": _definitions})
479 def _serialize_tooltip(self, definitions, identifier=None):
481 for definition in definitions:
482 signature = definition.name
484 if definition.type in ["class", "function"]:
485 signature = self._generate_signature(definition)
487 description = definition.docstring(raw=True).strip()
490 if not description and not hasattr(definition, "get_line_code"):
491 # jedi returns an empty string for compiled objects
492 description = definition.docstring().strip()
493 if definition.type == "module":
494 signature = definition.full_name
496 description = definition.docstring(raw=True).strip()
499 if not description and hasattr(definition, "get_line_code"):
500 # jedi returns an empty string for compiled objects
501 description = definition.docstring().strip()
503 "type": self._get_definition_type(definition),
504 "text": definition.name,
505 "description": description,
506 "docstring": description,
507 "signature": signature,
509 _definitions.append(_definition)
510 return json.dumps({"id": identifier, "results": _definitions})
512 def _serialize_usages(self, usages, identifier=None):
518 "moduleName": usage.module_name,
519 "fileName": usage.module_path,
521 "column": usage.column,
524 return json.dumps({"id": identifier, "results": _usages})
526 def _deserialize(self, request):
527 """Deserialize request from VSCode.
530 request: String with raw request from VSCode.
533 Python dictionary with request data.
535 return json.loads(request)
537 def _set_request_config(self, config):
538 """Sets config values for current request.
540 This includes sys.path modifications which is getting restored to
541 default value on each request so each project should be isolated
545 config: Dictionary with config values.
547 sys.path = self.default_sys_path
548 self.use_snippets = config.get("useSnippets")
549 self.show_doc_strings = config.get("showDescriptions", True)
550 self.fuzzy_matcher = config.get("fuzzyMatcher", False)
551 jedi.settings.case_insensitive_completion = config.get(
552 "caseInsensitiveCompletion", True
554 for path in config.get("extraPaths", []):
555 if path and path not in sys.path:
556 sys.path.insert(0, path)
558 def _normalize_request_path(self, request):
559 """Normalize any Windows paths received by a *nix build of
560 Python. Does not alter the reverse os.path.sep=='\\',
561 i.e. *nix paths received by a Windows build of Python.
563 if "path" in request:
564 if not self.drive_mount:
566 newPath = request["path"].replace("\\", "/")
567 if newPath[0:1] == "/":
568 # is absolute path with no drive letter
569 request["path"] = newPath
570 elif newPath[1:2] == ":":
571 # is path with drive letter, only absolute can be mapped
572 request["path"] = self.drive_mount + newPath[0:1].lower() + newPath[2:]
575 request["path"] = newPath
577 def _process_request(self, request):
578 """Accept serialized request from VSCode and write response.
580 request = self._deserialize(request)
582 self._set_request_config(request.get("config", {}))
584 self._normalize_request_path(request)
585 path = self._get_top_level_module(request.get("path", ""))
586 if len(path) > 0 and path not in sys.path:
587 sys.path.insert(0, path)
588 lookup = request.get("lookup", "completions")
590 project = jedi.Project(os.path.dirname(path), sys_path=sys.path)
592 if lookup == "names":
593 return self._serialize_definitions(
595 code=request.get("source", None),
596 path=request.get("path", ""),
598 environment=self.environment,
599 ).get_names(all_scopes=True),
603 line = request["line"] + 1
604 column = request["column"]
605 script = jedi.Script(
606 code=request.get("source", None),
607 path=request.get("path", ""),
609 environment=self.environment,
612 if lookup == "definitions":
613 defs = self._get_definitionsx(
614 script.goto(line, column, follow_imports=True), request["id"]
616 return json.dumps({"id": request["id"], "results": defs})
617 if lookup == "tooltip":
621 defs = self._get_definitionsx(
622 script.infer(line, column), request["id"], True
628 defs = self._get_definitionsx(
629 script.goto(line, column), request["id"], True
633 return json.dumps({"id": request["id"], "results": defs})
636 return self._serialize_tooltip(
637 script.infer(line, column), request["id"]
640 return json.dumps({"id": request["id"], "results": []})
641 elif lookup == "arguments":
642 return self._serialize_arguments(script, line, column, request["id"])
643 elif lookup == "usages":
644 return self._serialize_usages(
645 script.get_references(line, column), request["id"]
647 elif lookup == "methods":
648 return self._serialize_methods(
649 script, line, column, request["id"], request.get("prefix", "")
652 return self._serialize_completions(
653 script, line, column, request["id"], request.get("prefix", "")
656 def _write_response(self, response):
657 sys.stdout.write(response + "\n")
663 rq = self._input.readline()
665 # Reached EOF - indication our parent process is gone.
667 "Received EOF from the standard input,exiting" + "\n"
671 with RedirectStdout():
672 response = self._process_request(rq)
673 self._write_response(response)
676 sys.stderr.write(traceback.format_exc() + "\n")
680 if __name__ == "__main__":
683 if len(sys.argv) > 2 and sys.argv[1] == "custom":
684 jediPath = sys.argv[2]
686 cachePrefix = "custom_v"
687 if len(sys.argv) > 3:
688 modulesToLoad = sys.argv[3]
691 jediPath = os.path.join(os.path.dirname(__file__), "lib", "python")
692 if len(sys.argv) > 1:
693 modulesToLoad = sys.argv[1]
695 sys.path.insert(0, jediPath)
698 digits = jedi.__version__.split(".")
699 if int(digits[0]) == 0 and int(digits[1]) < 17:
700 raise RuntimeError("Jedi version %s too old, requires >= 0.17.0" % (jedi.__version__))
703 jedi.settings.cache_directory = os.path.join(
704 jedi.settings.cache_directory,
705 cachePrefix + jedi.__version__.replace(".", ""),
707 # remove jedi from path after we import it so it will not be completed
709 if len(modulesToLoad) > 0:
710 jedi.preload_module(*modulesToLoad.split(","))
711 JediCompletion().watch()