eric7/JediInterface/JediServer.py

branch
eric7
changeset 8593
1d66b6af60ed
child 8606
dd9bf9841c50
equal deleted inserted replaced
8592:ab8580937d4b 8593:1d66b6af60ed
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2015 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the autocompletion interface to jedi.
8 """
9
10 import contextlib
11 import os
12 import uuid
13
14 from PyQt6.QtCore import pyqtSlot, QCoreApplication, QTimer
15
16 from EricWidgets.EricApplication import ericApp
17
18 from EricNetwork.EricJsonServer import EricJsonServer
19
20 from QScintilla.Editor import Editor
21
22 import Preferences
23 import Globals
24
25
26 class JediServer(EricJsonServer):
27 """
28 Class implementing the interface to the jedi library.
29 """
30 IdProject = "Project"
31
32 PictureIDs = {
33 "class": "?{0}".format(Editor.ClassID),
34 "_class": "?{0}".format(Editor.ClassProtectedID),
35 "__class": "?{0}".format(Editor.ClassPrivateID),
36 "instance": "?{0}".format(Editor.ClassID),
37 "_instance": "?{0}".format(Editor.ClassProtectedID),
38 "__instance": "?{0}".format(Editor.ClassPrivateID),
39 "function": "?{0}".format(Editor.MethodID),
40 "_function": "?{0}".format(Editor.MethodProtectedID),
41 "__function": "?{0}".format(Editor.MethodPrivateID),
42 "module": "?{0}".format(Editor.ModuleID),
43 "_module": "?{0}".format(Editor.ModuleID),
44 "__module": "?{0}".format(Editor.ModuleID),
45 "param": "?{0}".format(Editor.AttributeID),
46 "_param": "?{0}".format(Editor.AttributeProtectedID),
47 "__param": "?{0}".format(Editor.AttributePrivateID),
48 "statement": "?{0}".format(Editor.AttributeID),
49 "_statement": "?{0}".format(Editor.AttributeProtectedID),
50 "__statement": "?{0}".format(Editor.AttributePrivateID),
51 "import": "",
52 "None": "",
53 }
54
55 def __init__(self, viewManager, project, ui):
56 """
57 Constructor
58
59 @param viewManager reference to the viewmanager object
60 @type ViewManager
61 @param project reference to the project object
62 @type Project
63 @param ui reference to the user interface
64 @type UserInterface
65 """
66 super().__init__(
67 "JediServer", multiplex=True, parent=ui)
68
69 self.__ui = ui
70 self.__vm = viewManager
71 self.__ericProject = project
72
73 self.__editorLanguageMapping = {}
74
75 self.__documentationViewer = None
76
77 # attributes to store the resuls of the client side
78 self.__completions = None
79 self.__calltips = None
80
81 self.__methodMapping = {
82 "CompletionsResult": self.__processCompletionsResult,
83 "CallTipsResult": self.__processCallTipsResult,
84 "DocumentationResult": self.__processDocumentationResult,
85 "HoverHelpResult": self.__processHoverHelpResult,
86 "GotoDefinitionResult": self.__processGotoDefinitionResult,
87 "GotoReferencesResult": self.__processGotoReferencesResult,
88
89 "ClientException": self.__processClientException,
90 }
91
92 # temporary store for editor references indexed by Uuid
93 self.__editors = {}
94
95 # Python 3
96 self.__ensureActive("Python3")
97
98 def __updateEditorLanguageMapping(self):
99 """
100 Private method to update the editor language to connection mapping.
101 """
102 self.__editorLanguageMapping = {}
103 for name in self.connectionNames():
104 if name == "Python3":
105 self.__editorLanguageMapping.update({
106 "Python3": "Python3",
107 "MicroPython": "Python3",
108 "Pygments|Python": "Python3",
109 "Pygments|Python 2.x": "Python3",
110 "Cython": "Python3",
111 })
112
113 def isSupportedLanguage(self, language):
114 """
115 Public method to check, if the given language is supported.
116
117 @param language editor programming language to check
118 @type str
119 @return flag indicating the support status
120 @rtype bool
121 """
122 return language in self.__editorLanguageMapping
123
124 def __idString(self, editor):
125 """
126 Private method to determine the ID string for the back-end.
127
128 @param editor reference to the editor to determine the ID string for
129 @type Editor
130 @return ID string
131 @rtype str
132 """
133 idString = ""
134
135 language = editor.getLanguage()
136 if (
137 self.__ericProject.isOpen() and
138 self.__ericProject.getProjectLanguage() == language
139 ):
140 filename = editor.getFileName()
141 if self.__ericProject.isProjectSource(filename):
142 idString = JediServer.IdProject
143
144 if not idString and language in self.__editorLanguageMapping:
145 idString = self.__editorLanguageMapping[language]
146
147 return idString
148
149 def __prepareData(self, editor):
150 """
151 Private method to gather data about current cursor position.
152
153 @param editor reference to the editor object, that called this method
154 @type Editor
155 @return tuple of filename, line, index, source
156 @rtype tuple (str, int, int, str)
157 """
158 filename = editor.getFileName()
159 line, index = editor.getCursorPosition()
160 line += 1 # jedi line numbers are 1 based
161 source = editor.text()
162 return filename, line, index, source
163
164 def requestCompletions(self, editor, context, acText):
165 """
166 Public method to request a list of possible completions.
167
168 @param editor reference to the editor object, that called this method
169 @type Editor
170 @param context flag indicating to autocomplete a context
171 @type bool
172 @param acText text to be completed
173 @type str
174 """
175 if not Preferences.getJedi("JediCompletionsEnabled"):
176 return
177
178 idString = self.__idString(editor)
179 if not idString:
180 return
181
182 filename, line, index, source = self.__prepareData(editor)
183 fuzzy = Preferences.getJedi("JediFuzzyCompletionsEnabled")
184
185 self.__ensureActive(idString)
186
187 self.sendJson("getCompletions", {
188 "FileName": filename,
189 "Source": source,
190 "Line": line,
191 "Index": index,
192 "Fuzzy": fuzzy,
193 "CompletionText": acText,
194 }, idString=idString)
195
196 def __processCompletionsResult(self, result):
197 """
198 Private method to process the completions sent by the client.
199
200 @param result dictionary containing the result sent by the client
201 @type dict
202 """
203 names = []
204 for completion in result["Completions"]:
205 name = completion['Name']
206 context = completion['FullName']
207 if context.endswith(".{0}".format(name)):
208 context = context.rsplit(".", 1)[0]
209 if context:
210 name = "{0} ({1})".format(name, context)
211
212 name += JediServer.PictureIDs.get(completion['CompletionType'], '')
213 names.append(name)
214
215 if "Error" not in result:
216 editor = self.__vm.getOpenEditor(result["FileName"])
217 if editor is not None:
218 editor.completionsListReady(names,
219 result["CompletionText"])
220
221 def getCallTips(self, editor, pos, commas):
222 """
223 Public method to calculate calltips.
224
225 @param editor reference to the editor object, that called this method
226 @type Editor
227 @param pos position in the text for the calltip
228 @type int
229 @param commas minimum number of commas contained in the calltip
230 @type int
231 @return list of possible calltips
232 @rtype list of str
233 """
234 if not Preferences.getJedi("JediCalltipsEnabled"):
235 return []
236
237 # reset the calltips buffer
238 self.__calltips = None
239
240 idString = self.__idString(editor)
241 if not idString:
242 return []
243
244 filename, line, index, source = self.__prepareData(editor)
245
246 self.__ensureActive(idString)
247 self.sendJson("getCallTips", {
248 "FileName": filename,
249 "Source": source,
250 "Line": line,
251 "Index": index,
252 }, idString=idString)
253
254 # emulate the synchronous behaviour
255 timer = QTimer()
256 timer.setSingleShot(True)
257 timer.start(5000) # 5s timeout
258 while self.__calltips is None and timer.isActive():
259 QCoreApplication.processEvents()
260
261 return [] if self.__calltips is None else self.__calltips
262
263 def __processCallTipsResult(self, result):
264 """
265 Private method to process the calltips sent by the client.
266
267 @param result dictionary containing the result sent by the client
268 @type dict
269 """
270 if "Error" in result:
271 self.__calltips = []
272 else:
273 self.__calltips = result["CallTips"]
274
275 def requestCodeDocumentation(self, editor):
276 """
277 Public method to request source code documentation for the given
278 editor.
279
280 @param editor reference to the editor to get source code documentation
281 for
282 @type Editor
283 """
284 if self.__documentationViewer is None:
285 return
286
287 idString = self.__idString(editor)
288
289 if not idString:
290 language = editor.getLanguage()
291 warning = (
292 self.tr("Language <b>{0}</b> is not supported.")
293 .format(language)
294 )
295 self.__documentationViewer.documentationReady(
296 warning, isWarning=True)
297 return
298
299 filename, line, index, source = self.__prepareData(editor)
300 sourceLines = source.splitlines()
301 # Correct index if cursor is standing after an opening bracket
302 if line > 0 and index > 0 and sourceLines[line - 1][index - 1] == '(':
303 index -= 1
304
305 self.__ensureActive(idString)
306 self.sendJson("getDocumentation", {
307 "FileName": filename,
308 "Source": source,
309 "Line": line,
310 "Index": index,
311 }, idString=idString)
312
313 def __processDocumentationResult(self, result):
314 """
315 Private method to process the documentation sent by the client.
316
317 @param result dictionary containing the result sent by the client
318 @type dict with keys 'name', 'module', 'argspec', 'docstring'
319 """
320 if self.__documentationViewer is None:
321 return
322
323 docu = None
324
325 if "Error" not in result:
326 docu = result["DocumentationDict"]
327 docu["note"] = (
328 self.tr("Present in <i>{0}</i> module")
329 .format(docu["module"]))
330
331 if docu is None:
332 msg = self.tr("No documentation available.")
333 self.__documentationViewer.documentationReady(
334 msg, isDocWarning=True)
335 else:
336 self.__documentationViewer.documentationReady(docu)
337
338 def gotoDefinition(self, editor):
339 """
340 Public slot to find the definition for the word at the cursor position
341 and go to it.
342
343 Note: This is executed upon a mouse click sequence.
344
345 @param editor reference to the calling editor
346 @type Editor
347 """
348 if not Preferences.getJedi("MouseClickEnabled"):
349 return
350
351 idString = self.__idString(editor)
352 if not idString:
353 return
354
355 filename, line, index, source = self.__prepareData(editor)
356
357 self.__ensureActive(idString)
358
359 euuid = str(uuid.uuid4())
360 self.__editors[euuid] = editor
361
362 self.sendJson("gotoDefinition", {
363 "FileName": filename,
364 "Source": source,
365 "Line": line,
366 "Index": index,
367 "Uuid": euuid,
368 }, idString=idString)
369
370 def __processGotoDefinitionResult(self, result):
371 """
372 Private method callback for the goto definition result.
373
374 @param result dictionary containing the result data
375 @type dict
376 """
377 euuid = result["Uuid"]
378 if "Error" not in result:
379 # ignore errors silently
380 location = result["GotoDefinitionDict"]
381 if location:
382 try:
383 editor = self.__editors[euuid]
384 except KeyError:
385 editor = None
386
387 if (
388 editor is not None and
389 editor.getFileName() == location["ModulePath"] and
390 editor.getCursorPosition()[0] + 1 == location["Line"]
391 ):
392 # this was a click onto the definition line
393 # -> get references
394 idString = self.__idString(editor)
395 filename, line, index, source = self.__prepareData(editor)
396 self.sendJson("gotoReferences", {
397 "FileName": filename,
398 "Source": source,
399 "Line": line,
400 "Index": index,
401 "Uuid": euuid,
402 }, idString=idString)
403 return
404
405 self.__vm.openSourceFile(location["ModulePath"],
406 location["Line"],
407 addNext=True)
408 else:
409 ericApp().getObject("UserInterface").statusBar().showMessage(
410 self.tr('Jedi: No definition found'), 5000)
411
412 with contextlib.suppress(KeyError):
413 del self.__editors[euuid]
414
415 def __processGotoReferencesResult(self, result):
416 """
417 Private method callback for the goto references result.
418
419 @param result dictionary containing the result data
420 @type dict
421 """
422 euuid = result["Uuid"]
423 with contextlib.suppress(ImportError):
424 from QScintilla.Editor import ReferenceItem
425
426 if "Error" not in result:
427 # ignore errors silently
428 references = result["GotoReferencesList"]
429 if references:
430 try:
431 editor = self.__editors[euuid]
432 except KeyError:
433 editor = None
434 if editor is not None:
435 referenceItemsList = [
436 ReferenceItem(
437 modulePath=ref["ModulePath"],
438 codeLine=ref["Code"],
439 line=ref["Line"],
440 column=ref["Column"],
441 ) for ref in references
442 ]
443 editor.gotoReferenceHandler(referenceItemsList)
444
445 with contextlib.suppress(KeyError):
446 del self.__editors[euuid]
447
448 def hoverHelp(self, editor, line, index):
449 """
450 Public method to initiate the display of mouse hover help.
451
452 @param editor reference to the calling editor
453 @type Editor
454 @param line line number (zero based)
455 @type int
456 @param index index within the line (zero based)
457 @type int
458 """
459 idString = self.__idString(editor)
460 if not idString:
461 return
462
463 filename = editor.getFileName()
464 line += 1 # jedi line numbers are 1 based
465 source = editor.text()
466
467 self.__ensureActive(idString)
468
469 euuid = str(uuid.uuid4())
470 self.__editors[euuid] = editor
471
472 self.sendJson("hoverHelp", {
473 "FileName": filename,
474 "Source": source,
475 "Line": line,
476 "Index": index,
477 "Uuid": euuid,
478 }, idString=idString)
479
480 def __processHoverHelpResult(self, result):
481 """
482 Private method callback for the goto definition result.
483
484 @param result dictionary containing the result data
485 @type dict
486 """
487 euuid = result["Uuid"]
488 if "Error" not in result:
489 # ignore errors silently
490 helpText = result["HoverHelp"]
491 if helpText:
492 with contextlib.suppress(KeyError):
493 self.__editors[euuid].showMouseHoverHelpData(
494 result["Line"] - 1,
495 result["Index"],
496 helpText
497 )
498 else:
499 ericApp().getObject("UserInterface").statusBar().showMessage(
500 self.tr('Jedi: No mouse hover help found'), 5000)
501
502 with contextlib.suppress(KeyError):
503 del self.__editors[euuid]
504
505 #######################################################################
506 ## Methods below handle the network connection
507 #######################################################################
508
509 def handleCall(self, method, params):
510 """
511 Public method to handle a method call from the client.
512
513 @param method requested method name
514 @type str
515 @param params dictionary with method specific parameters
516 @type dict
517 """
518 self.__methodMapping[method](params)
519
520 def __processClientException(self, params):
521 """
522 Private method to handle exceptions of the refactoring client.
523
524 @param params dictionary containing the exception data
525 @type dict
526 """
527 if params["ExceptionType"] == "ProtocolError":
528 self.__ui.appendToStderr(
529 self.tr("The data received from the Jedi server could not be"
530 " decoded. Please report this issue with the received"
531 " data to the eric bugs email address.\n"
532 "Error: {0}\n"
533 "Data:\n{1}\n").format(
534 params["ExceptionValue"],
535 params["ProtocolData"]))
536 else:
537 self.__ui.appendToStderr(
538 self.tr("An exception happened in the Jedi client. Please"
539 " report it to the eric bugs email address.\n"
540 "Exception: {0}\n"
541 "Value: {1}\n"
542 "Traceback: {2}\n").format(
543 params["ExceptionType"],
544 params["ExceptionValue"],
545 params["Traceback"]))
546
547 def __startJediClient(self, interpreter, idString, clientEnv):
548 """
549 Private method to start the Jedi client with the given interpreter.
550
551 @param interpreter interpreter to be used for the Jedi client
552 @type str
553 @param idString id of the client to be started
554 @type str
555 @param clientEnv dictionary with environment variables to run the
556 interpreter with
557 @type dict
558 @return flag indicating a successful start of the client
559 @rtype bool
560 """
561 ok = False
562
563 if interpreter:
564 client = os.path.join(os.path.dirname(__file__),
565 "JediClient.py")
566 ok, exitCode = self.startClient(
567 interpreter, client,
568 [Globals.getPythonLibraryDirectory()],
569 idString=idString, environment=clientEnv)
570 if not ok:
571 if exitCode == 42:
572 self.__ui.appendToStderr("JediServer: " + self.tr(
573 "The jedi and/or parso library is not installed.\n"
574 ))
575 else:
576 self.__ui.appendToStderr("JediServer: " + self.tr(
577 "'{0}' is not supported because the configured"
578 " interpreter could not be started.\n"
579 ).format(idString))
580 else:
581 self.__ui.appendToStderr("JediServer: " + self.tr(
582 "'{0}' is not supported because no suitable interpreter is"
583 " configured.\n"
584 ).format(idString))
585
586 return ok
587
588 def __ensureActive(self, idString):
589 """
590 Private method to ensure, that the requested client is active.
591
592 A non-active client will be started.
593
594 @param idString id of the client to be checked
595 @type str
596 @return flag indicating an active client
597 @rtype bool
598 """
599 ok = idString in self.connectionNames()
600 if not ok:
601 # client is not running
602 if idString == JediServer.IdProject:
603 interpreter, clientEnv = self.__interpreterForProject()
604 else:
605 interpreter = ""
606 venvName = ""
607 clientEnv = os.environ.copy()
608 if "PATH" in clientEnv:
609 clientEnv["PATH"] = self.__ui.getOriginalPathString()
610 # new code using virtual environments
611 venvManager = ericApp().getObject("VirtualEnvManager")
612 if idString == "Python3":
613 venvName = Preferences.getDebugger("Python3VirtualEnv")
614 if not venvName:
615 venvName, _ = venvManager.getDefaultEnvironment()
616 if venvName:
617 interpreter = venvManager.getVirtualenvInterpreter(
618 venvName)
619 execPath = venvManager.getVirtualenvExecPath(venvName)
620
621 # build a suitable environment
622 if execPath:
623 if "PATH" in clientEnv:
624 clientEnv["PATH"] = os.pathsep.join(
625 [execPath, clientEnv["PATH"]])
626 else:
627 clientEnv["PATH"] = execPath
628 if interpreter:
629 ok = self.__startJediClient(interpreter, idString, clientEnv)
630 else:
631 ok = False
632 return ok
633
634 def __interpreterForProject(self):
635 """
636 Private method to determine the interpreter for the current project and
637 the environment to run it.
638
639 @return tuple containing the interpreter of the current project and the
640 environment variables
641 @rtype tuple of (str, dict)
642 """
643 projectLanguage = self.__ericProject.getProjectLanguage()
644 interpreter = ""
645 clientEnv = os.environ.copy()
646 if "PATH" in clientEnv:
647 clientEnv["PATH"] = self.__ui.getOriginalPathString()
648
649 if (projectLanguage.startswith("Python") or
650 projectLanguage == "MicroPython"):
651 # new code using virtual environments
652 venvManager = ericApp().getObject("VirtualEnvManager")
653
654 # get virtual environment from project first
655 venvName = self.__ericProject.getDebugProperty("VIRTUALENV")
656 if not venvName:
657 # get it from debugger settings next
658 if projectLanguage in ("Python3", "MicroPython", "Cython"):
659 venvName = Preferences.getDebugger("Python3VirtualEnv")
660 if not venvName:
661 venvName, _ = venvManager.getDefaultEnvironment()
662 else:
663 venvName = ""
664 if venvName:
665 interpreter = venvManager.getVirtualenvInterpreter(
666 venvName)
667 execPath = venvManager.getVirtualenvExecPath(venvName)
668
669 # build a suitable environment
670 if execPath:
671 if "PATH" in clientEnv:
672 clientEnv["PATH"] = os.pathsep.join(
673 [execPath, clientEnv["PATH"]])
674 else:
675 clientEnv["PATH"] = execPath
676
677 return interpreter, clientEnv
678
679 @pyqtSlot()
680 def handleNewConnection(self):
681 """
682 Public slot for new incoming connections from a client.
683 """
684 super().handleNewConnection()
685
686 self.__updateEditorLanguageMapping()
687
688 def activate(self):
689 """
690 Public method to activate the Jedi server.
691 """
692 self.__documentationViewer = self.__ui.documentationViewer()
693 if self.__documentationViewer is not None:
694 self.__documentationViewer.registerProvider(
695 "jedi", self.tr("Jedi"), self.requestCodeDocumentation,
696 self.isSupportedLanguage)
697
698 self.__ericProject.projectOpened.connect(self.__projectOpened)
699 self.__ericProject.projectClosed.connect(self.__projectClosed)
700
701 def deactivate(self):
702 """
703 Public method to deactivate the code assist server.
704 """
705 """
706 Public method to shut down the code assist server.
707 """
708 if self.__documentationViewer is not None:
709 self.__documentationViewer.unregisterProvider("jedi")
710
711 with contextlib.suppress(TypeError):
712 self.__ericProject.projectOpened.disconnect(self.__projectOpened)
713 self.__ericProject.projectClosed.disconnect(self.__projectClosed)
714
715 self.stopAllClients()
716
717 @pyqtSlot()
718 def __projectOpened(self):
719 """
720 Private slot to handle the projectOpened signal.
721 """
722 self.__ensureActive(JediServer.IdProject)
723 self.sendJson("openProject", {
724 "ProjectPath": self.__ericProject.getProjectPath(),
725 }, idString=JediServer.IdProject)
726
727 @pyqtSlot()
728 def __projectClosed(self):
729 """
730 Private slot to handle the projectClosed signal.
731 """
732 self.__ensureActive(JediServer.IdProject)
733 self.sendJson("closeProject", {}, idString=JediServer.IdProject)
734
735 self.stopClient(idString=JediServer.IdProject)

eric ide

mercurial