|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2017 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the Jedi client of eric7. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 import sys |
|
12 |
|
13 SuppressedException = Exception |
|
14 |
|
15 modulePath = sys.argv[-1] # it is always the last parameter |
|
16 sys.path.insert(1, modulePath) |
|
17 |
|
18 try: |
|
19 import jedi |
|
20 except ImportError: |
|
21 sys.exit(42) |
|
22 |
|
23 from eric7.EricNetwork.EricJsonClient import EricJsonClient |
|
24 |
|
25 |
|
26 class JediClient(EricJsonClient): |
|
27 """ |
|
28 Class implementing the Jedi client of eric7. |
|
29 """ |
|
30 def __init__(self, host, port, idString): |
|
31 """ |
|
32 Constructor |
|
33 |
|
34 @param host ip address the background service is listening |
|
35 @type str |
|
36 @param port port of the background service |
|
37 @type int |
|
38 @param idString assigned client id to be sent back to the server in |
|
39 order to identify the connection |
|
40 @type str |
|
41 """ |
|
42 super().__init__(host, port, idString) |
|
43 |
|
44 self.__methodMapping = { |
|
45 "openProject": self.__openProject, |
|
46 "closeProject": self.__closeProject, |
|
47 |
|
48 "getCompletions": self.__getCompletions, |
|
49 "getCallTips": self.__getCallTips, |
|
50 "getDocumentation": self.__getDocumentation, |
|
51 "hoverHelp": self.__getHoverHelp, |
|
52 "gotoDefinition": self.__getAssignment, |
|
53 "gotoReferences": self.__getReferences, |
|
54 |
|
55 "renameVariable": self.__renameVariable, |
|
56 "extractVariable": self.__extractVariable, |
|
57 "inlineVariable": self.__inlineVariable, |
|
58 "extractFunction": self.__extractFunction, |
|
59 "applyRefactoring": self.__applyRefactoring, |
|
60 "cancelRefactoring": self.__cancelRefactoring, |
|
61 } |
|
62 |
|
63 self.__id = idString |
|
64 |
|
65 self.__project = None |
|
66 |
|
67 self.__refactorings = {} |
|
68 |
|
69 def handleCall(self, method, params): |
|
70 """ |
|
71 Public method to handle a method call from the server. |
|
72 |
|
73 @param method requested method name |
|
74 @type str |
|
75 @param params dictionary with method specific parameters |
|
76 @type dict |
|
77 """ |
|
78 self.__methodMapping[method](params) |
|
79 |
|
80 def __handleError(self, err): |
|
81 """ |
|
82 Private method to process an error. |
|
83 |
|
84 @param err exception object |
|
85 @type Exception or Warning |
|
86 @return dictionary containing the error information |
|
87 @rtype dict |
|
88 """ |
|
89 error = str(type(err)).split()[-1] |
|
90 error = error[1:-2].split('.')[-1] |
|
91 errorDict = { |
|
92 "Error": error, |
|
93 "ErrorString": str(err), |
|
94 } |
|
95 |
|
96 return errorDict |
|
97 |
|
98 def __openProject(self, params): |
|
99 """ |
|
100 Private method to create a jedi project and load its saved data. |
|
101 |
|
102 @param params dictionary containing the method parameters |
|
103 @type dict |
|
104 """ |
|
105 projectPath = params["ProjectPath"] |
|
106 self.__project = jedi.Project(projectPath) |
|
107 |
|
108 def __closeProject(self, params): |
|
109 """ |
|
110 Private method to save a jedi project's data. |
|
111 |
|
112 @param params dictionary containing the method parameters |
|
113 @type dict |
|
114 """ |
|
115 if self.__project is not None: |
|
116 self.__project.save() |
|
117 |
|
118 def __completionType(self, completion): |
|
119 """ |
|
120 Private method to assemble the completion type depending on the |
|
121 visibility indicated by the completion name. |
|
122 |
|
123 @param completion reference to the completion object |
|
124 @type jedi.api.classes.Completion |
|
125 @return modified completion type |
|
126 @rtype str |
|
127 """ |
|
128 if completion.name.startswith('__'): |
|
129 compType = '__' + completion.type |
|
130 elif completion.name.startswith('_'): |
|
131 compType = '_' + completion.type |
|
132 else: |
|
133 compType = completion.type |
|
134 |
|
135 return compType |
|
136 |
|
137 def __completionFullName(self, completion): |
|
138 """ |
|
139 Private method to extract the full completion name. |
|
140 |
|
141 @param completion reference to the completion object |
|
142 @type jedi.api.classes.Completion |
|
143 @return full completion name |
|
144 @rtype str |
|
145 """ |
|
146 fullName = completion.full_name |
|
147 fullName = ( |
|
148 fullName.replace("__main__", completion.module_name) |
|
149 if fullName else |
|
150 completion.module_name |
|
151 ) |
|
152 |
|
153 return fullName |
|
154 |
|
155 def __getCompletions(self, params): |
|
156 """ |
|
157 Private method to calculate possible completions. |
|
158 |
|
159 @param params dictionary containing the method parameters |
|
160 @type dict |
|
161 """ |
|
162 filename = params["FileName"] |
|
163 source = params["Source"] |
|
164 line = params["Line"] |
|
165 index = params["Index"] |
|
166 fuzzy = params["Fuzzy"] |
|
167 |
|
168 errorDict = {} |
|
169 response = [] |
|
170 |
|
171 script = jedi.Script(source, path=filename, project=self.__project) |
|
172 |
|
173 try: |
|
174 completions = script.complete(line, index, fuzzy=fuzzy) |
|
175 response = [ |
|
176 { |
|
177 'ModulePath': str(completion.module_path), |
|
178 'Name': completion.name, |
|
179 'FullName': self.__completionFullName(completion), |
|
180 'CompletionType': self.__completionType(completion), |
|
181 } for completion in completions |
|
182 if not (completion.name.startswith("__") and |
|
183 completion.name.endswith("__")) |
|
184 ] |
|
185 except SuppressedException as err: |
|
186 errorDict = self.__handleError(err) |
|
187 |
|
188 result = { |
|
189 "Completions": response, |
|
190 "CompletionText": params["CompletionText"], |
|
191 "FileName": filename, |
|
192 } |
|
193 result.update(errorDict) |
|
194 |
|
195 self.sendJson("CompletionsResult", result) |
|
196 |
|
197 def __getCallTips(self, params): |
|
198 """ |
|
199 Private method to calculate possible calltips. |
|
200 |
|
201 @param params dictionary containing the method parameters |
|
202 @type dict |
|
203 """ |
|
204 filename = params["FileName"] |
|
205 source = params["Source"] |
|
206 line = params["Line"] |
|
207 index = params["Index"] |
|
208 |
|
209 errorDict = {} |
|
210 calltips = [] |
|
211 |
|
212 script = jedi.Script(source, path=filename, project=self.__project) |
|
213 |
|
214 try: |
|
215 signatures = script.get_signatures(line, index) |
|
216 for signature in signatures: |
|
217 name = signature.name |
|
218 params = self.__extractParameters(signature) |
|
219 calltips.append("{0}{1}".format(name, params)) |
|
220 except SuppressedException as err: |
|
221 errorDict = self.__handleError(err) |
|
222 |
|
223 result = { |
|
224 "CallTips": calltips, |
|
225 } |
|
226 result.update(errorDict) |
|
227 |
|
228 self.sendJson("CallTipsResult", result) |
|
229 |
|
230 def __extractParameters(self, signature): |
|
231 """ |
|
232 Private method to extract the call parameter descriptions. |
|
233 |
|
234 @param signature a jedi signature object |
|
235 @type object |
|
236 @return a string with comma seperated parameter names and default |
|
237 values |
|
238 @rtype str |
|
239 """ |
|
240 try: |
|
241 params = ", ".join([param.description.split('param ', 1)[-1] |
|
242 for param in signature.params]) |
|
243 return "({0})".format(params) |
|
244 except AttributeError: |
|
245 # Empty strings as argspec suppress display of "definition" |
|
246 return ' ' |
|
247 |
|
248 def __getDocumentation(self, params): |
|
249 """ |
|
250 Private method to get some source code documentation. |
|
251 |
|
252 @param params dictionary containing the method parameters |
|
253 @type dict |
|
254 """ |
|
255 filename = params["FileName"] |
|
256 source = params["Source"] |
|
257 line = params["Line"] |
|
258 index = params["Index"] |
|
259 |
|
260 errorDict = {} |
|
261 docu = {} |
|
262 |
|
263 script = jedi.Script(source, path=filename, project=self.__project) |
|
264 |
|
265 try: |
|
266 definitions = script.infer(line, index) |
|
267 definition = definitions[0] # use the first one only |
|
268 docu = { |
|
269 "name": definition.full_name, |
|
270 "module": definition.module_name, |
|
271 "argspec": self.__extractParameters(definition), |
|
272 "docstring": definition.docstring(), |
|
273 } |
|
274 except SuppressedException as err: |
|
275 errorDict = self.__handleError(err) |
|
276 |
|
277 result = { |
|
278 "DocumentationDict": docu, |
|
279 } |
|
280 result.update(errorDict) |
|
281 |
|
282 self.sendJson("DocumentationResult", result) |
|
283 |
|
284 def __getHoverHelp(self, params): |
|
285 """ |
|
286 Private method to get some source code documentation. |
|
287 |
|
288 @param params dictionary containing the method parameters |
|
289 @type dict |
|
290 """ |
|
291 filename = params["FileName"] |
|
292 source = params["Source"] |
|
293 line = params["Line"] |
|
294 index = params["Index"] |
|
295 uid = params["Uuid"] |
|
296 |
|
297 script = jedi.Script(source, path=filename, project=self.__project) |
|
298 |
|
299 errorDict = {} |
|
300 helpText = "" |
|
301 |
|
302 try: |
|
303 helpText = script.help(line, index)[0].docstring() |
|
304 except SuppressedException as err: |
|
305 errorDict = self.__handleError(err) |
|
306 |
|
307 result = { |
|
308 "Line": line, |
|
309 "Index": index, |
|
310 "HoverHelp": helpText, |
|
311 "Uuid": uid, |
|
312 } |
|
313 result.update(errorDict) |
|
314 |
|
315 self.sendJson("HoverHelpResult", result) |
|
316 |
|
317 def __getAssignment(self, params): |
|
318 """ |
|
319 Private method to get the place a parameter is defined. |
|
320 |
|
321 @param params dictionary containing the method parameters |
|
322 @type dict |
|
323 """ |
|
324 filename = params["FileName"] |
|
325 source = params["Source"] |
|
326 line = params["Line"] |
|
327 index = params["Index"] |
|
328 uid = params["Uuid"] |
|
329 |
|
330 errorDict = {} |
|
331 gotoDefinition = {} |
|
332 |
|
333 script = jedi.Script(source, path=filename, project=self.__project) |
|
334 |
|
335 try: |
|
336 assignments = script.goto( |
|
337 line, index, follow_imports=True, follow_builtin_imports=True) |
|
338 for assignment in assignments: |
|
339 if bool(assignment.module_path): |
|
340 gotoDefinition = { |
|
341 'ModulePath': str(assignment.module_path), |
|
342 'Line': (0 if assignment.line is None else |
|
343 assignment.line), |
|
344 'Column': assignment.column, |
|
345 } |
|
346 |
|
347 if ( |
|
348 gotoDefinition["ModulePath"] == filename and |
|
349 gotoDefinition["Line"] == line |
|
350 ): |
|
351 # user called for the definition itself |
|
352 # => send the references instead |
|
353 self.__getReferences(params) |
|
354 return |
|
355 break |
|
356 except SuppressedException as err: |
|
357 errorDict = self.__handleError(err) |
|
358 |
|
359 result = { |
|
360 "GotoDefinitionDict": gotoDefinition, |
|
361 "Uuid": uid, |
|
362 } |
|
363 result.update(errorDict) |
|
364 |
|
365 self.sendJson("GotoDefinitionResult", result) |
|
366 |
|
367 def __getReferences(self, params): |
|
368 """ |
|
369 Private method to get the places a parameter is referenced. |
|
370 |
|
371 @param params dictionary containing the method parameters |
|
372 @type dict |
|
373 """ |
|
374 filename = params["FileName"] |
|
375 source = params["Source"] |
|
376 line = params["Line"] |
|
377 index = params["Index"] |
|
378 uid = params["Uuid"] |
|
379 |
|
380 errorDict = {} |
|
381 gotoReferences = [] |
|
382 |
|
383 script = jedi.Script(source, path=filename, project=self.__project) |
|
384 |
|
385 try: |
|
386 references = script.get_references(line, index, |
|
387 include_builtins=False) |
|
388 for reference in references: |
|
389 if bool(reference.module_path): |
|
390 if ( |
|
391 reference.line == line and |
|
392 str(reference.module_path) == filename |
|
393 ): |
|
394 continue |
|
395 gotoReferences.append({ |
|
396 'ModulePath': str(reference.module_path), |
|
397 'Line': (0 if reference.line is None else |
|
398 reference.line), |
|
399 'Column': reference.column, |
|
400 'Code': reference.get_line_code(), |
|
401 }) |
|
402 except SuppressedException as err: |
|
403 errorDict = self.__handleError(err) |
|
404 |
|
405 result = { |
|
406 "GotoReferencesList": gotoReferences, |
|
407 "Uuid": uid, |
|
408 } |
|
409 result.update(errorDict) |
|
410 |
|
411 self.sendJson("GotoReferencesResult", result) |
|
412 |
|
413 def __renameVariable(self, params): |
|
414 """ |
|
415 Private method to rename the variable under the cursor. |
|
416 |
|
417 @param params dictionary containing the method parameters |
|
418 @type dict |
|
419 """ |
|
420 filename = params["FileName"] |
|
421 source = params["Source"] |
|
422 line = params["Line"] |
|
423 index = params["Index"] |
|
424 uid = params["Uuid"] |
|
425 newName = params["NewName"] |
|
426 |
|
427 errorDict = {} |
|
428 diff = "" |
|
429 |
|
430 script = jedi.Script(source, path=filename, project=self.__project) |
|
431 |
|
432 try: |
|
433 refactoring = script.rename(line, index, new_name=newName) |
|
434 self.__refactorings[uid] = refactoring |
|
435 diff = refactoring.get_diff() |
|
436 except SuppressedException as err: |
|
437 errorDict = self.__handleError(err) |
|
438 |
|
439 result = { |
|
440 "Diff": diff, |
|
441 "Uuid": uid, |
|
442 } |
|
443 result.update(errorDict) |
|
444 |
|
445 self.sendJson("RefactoringDiff", result) |
|
446 |
|
447 def __extractVariable(self, params): |
|
448 """ |
|
449 Private method to extract a statement to a new variable. |
|
450 |
|
451 @param params dictionary containing the method parameters |
|
452 @type dict |
|
453 """ |
|
454 filename = params["FileName"] |
|
455 source = params["Source"] |
|
456 line = params["Line"] |
|
457 index = params["Index"] |
|
458 endLine = params["EndLine"] |
|
459 endIndex = params["EndIndex"] |
|
460 uid = params["Uuid"] |
|
461 newName = params["NewName"] |
|
462 |
|
463 errorDict = {} |
|
464 diff = "" |
|
465 |
|
466 script = jedi.Script(source, path=filename, project=self.__project) |
|
467 |
|
468 try: |
|
469 refactoring = script.extract_variable( |
|
470 line, index, new_name=newName, |
|
471 until_line=endLine, until_column=endIndex |
|
472 ) |
|
473 self.__refactorings[uid] = refactoring |
|
474 diff = refactoring.get_diff() |
|
475 except SuppressedException as err: |
|
476 errorDict = self.__handleError(err) |
|
477 |
|
478 result = { |
|
479 "Diff": diff, |
|
480 "Uuid": uid, |
|
481 } |
|
482 result.update(errorDict) |
|
483 |
|
484 self.sendJson("RefactoringDiff", result) |
|
485 |
|
486 def __inlineVariable(self, params): |
|
487 """ |
|
488 Private method to inline a variable statement. |
|
489 |
|
490 @param params dictionary containing the method parameters |
|
491 @type dict |
|
492 """ |
|
493 filename = params["FileName"] |
|
494 source = params["Source"] |
|
495 line = params["Line"] |
|
496 index = params["Index"] |
|
497 uid = params["Uuid"] |
|
498 |
|
499 errorDict = {} |
|
500 diff = "" |
|
501 |
|
502 script = jedi.Script(source, path=filename, project=self.__project) |
|
503 |
|
504 try: |
|
505 refactoring = script.inline(line, index) |
|
506 self.__refactorings[uid] = refactoring |
|
507 diff = refactoring.get_diff() |
|
508 except SuppressedException as err: |
|
509 errorDict = self.__handleError(err) |
|
510 |
|
511 result = { |
|
512 "Diff": diff, |
|
513 "Uuid": uid, |
|
514 } |
|
515 result.update(errorDict) |
|
516 |
|
517 self.sendJson("RefactoringDiff", result) |
|
518 |
|
519 def __extractFunction(self, params): |
|
520 """ |
|
521 Private method to extract an expression to a new function. |
|
522 |
|
523 @param params dictionary containing the method parameters |
|
524 @type dict |
|
525 """ |
|
526 filename = params["FileName"] |
|
527 source = params["Source"] |
|
528 line = params["Line"] |
|
529 index = params["Index"] |
|
530 endLine = params["EndLine"] |
|
531 endIndex = params["EndIndex"] |
|
532 uid = params["Uuid"] |
|
533 newName = params["NewName"] |
|
534 |
|
535 errorDict = {} |
|
536 diff = "" |
|
537 |
|
538 script = jedi.Script(source, path=filename, project=self.__project) |
|
539 |
|
540 try: |
|
541 refactoring = script.extract_function( |
|
542 line, index, new_name=newName, |
|
543 until_line=endLine, until_column=endIndex |
|
544 ) |
|
545 self.__refactorings[uid] = refactoring |
|
546 diff = refactoring.get_diff() |
|
547 except SuppressedException as err: |
|
548 errorDict = self.__handleError(err) |
|
549 |
|
550 result = { |
|
551 "Diff": diff, |
|
552 "Uuid": uid, |
|
553 } |
|
554 result.update(errorDict) |
|
555 |
|
556 self.sendJson("RefactoringDiff", result) |
|
557 |
|
558 def __applyRefactoring(self, params): |
|
559 """ |
|
560 Private method to apply a refactoring. |
|
561 |
|
562 @param params dictionary containing the method parameters |
|
563 @type dict |
|
564 """ |
|
565 uid = params["Uuid"] |
|
566 |
|
567 errorDict = {} |
|
568 |
|
569 try: |
|
570 refactoring = self.__refactorings[uid] |
|
571 refactoring.apply() |
|
572 ok = True |
|
573 except KeyError: |
|
574 ok = False |
|
575 except SuppressedException as err: |
|
576 errorDict = self.__handleError(err) |
|
577 |
|
578 result = { |
|
579 "result": ok, |
|
580 } |
|
581 result.update(errorDict) |
|
582 |
|
583 self.sendJson("RefactoringApplyResult", result) |
|
584 |
|
585 with contextlib.suppress(KeyError): |
|
586 del self.__refactorings[uid] |
|
587 |
|
588 def __cancelRefactoring(self, params): |
|
589 """ |
|
590 Private method to cancel a refactoring. |
|
591 |
|
592 @param params dictionary containing the method parameters |
|
593 @type dict |
|
594 """ |
|
595 uid = params["Uuid"] |
|
596 with contextlib.suppress(KeyError): |
|
597 del self.__refactorings[uid] |
|
598 |
|
599 |
|
600 if __name__ == '__main__': |
|
601 if len(sys.argv) != 5: |
|
602 print('Host, port, id and module path parameters are missing.' |
|
603 ' Abort.') |
|
604 sys.exit(1) |
|
605 |
|
606 host, port, idString = sys.argv[1:-1] |
|
607 |
|
608 client = JediClient(host, int(port), idString) |
|
609 # Start the main loop |
|
610 client.run() |
|
611 |
|
612 sys.exit(0) |
|
613 |
|
614 # |
|
615 # eflag: noqa = M801 |