33from modulefinder import ModuleFinder
44
55from spyder .plugins .editor .widgets .codeeditor import CodeEditor
6- from PyQt5 .QtCore import pyqtSignal , QFileSystemWatcher , QTimer
7- from PyQt5 .QtWidgets import QAction , QFileDialog , QApplication
8- from PyQt5 .QtGui import QFontDatabase , QTextCursor
6+ from PyQt5 .QtCore import pyqtSignal , QFileSystemWatcher , QTimer , Qt , QEvent
7+ from PyQt5 .QtWidgets import (
8+ QAction ,
9+ QFileDialog ,
10+ QApplication ,
11+ QListWidget ,
12+ QListWidgetItem ,
13+ QShortcut ,
14+ )
15+ from PyQt5 .QtGui import QFontDatabase , QTextCursor , QKeyEvent
916from path import Path
1017
1118import sys
1219
20+ import jedi
21+
1322from pyqtgraph .parametertree import Parameter
1423
1524from ..mixins import ComponentMixin
@@ -26,6 +35,7 @@ class Editor(CodeEditor, ComponentMixin):
2635 # autoreload is enabled.
2736 triggerRerender = pyqtSignal (bool )
2837 sigFilenameChanged = pyqtSignal (str )
38+ statusChanged = pyqtSignal (str )
2939
3040 preferences = Parameter .create (
3141 name = "Preferences" ,
@@ -54,6 +64,9 @@ class Editor(CodeEditor, ComponentMixin):
5464 # Tracks whether or not the document was saved from the Spyder editor vs an external editor
5565 was_modified_by_self = False
5666
67+ # Helps display the completion list for the editor
68+ completion_list = None
69+
5770 def __init__ (self , parent = None ):
5871
5972 self ._watched_file = None
@@ -120,6 +133,46 @@ def __init__(self, parent=None):
120133
121134 self .updatePreferences ()
122135
136+ # Create a floating list widget for completions
137+ self .completion_list = QListWidget (self )
138+ self .completion_list .setWindowFlags (Qt .Popup )
139+ self .completion_list .setFocusPolicy (Qt .NoFocus )
140+ self .completion_list .hide ()
141+
142+ # Connect the completion list to the editor
143+ self .completion_list .itemClicked .connect (self .insert_completion )
144+
145+ # Ensure that when the escape key is pressed with the completion_list in focus, it will be hidden
146+ self .completion_list .installEventFilter (self )
147+
148+ def eventFilter (self , watched , event ):
149+ """
150+ Allows us to do things like escape and tab key press for the completion list.
151+ """
152+
153+ if watched == self .completion_list and event .type () == QEvent .KeyPress :
154+ key_event = QKeyEvent (event )
155+ # Handle the escape key press
156+ if key_event .key () == Qt .Key_Escape :
157+ if self .completion_list and self .completion_list .isVisible ():
158+ self .completion_list .hide ()
159+ return True # Event handled
160+ # Handle the tab key press
161+ elif key_event .key () == Qt .Key_Tab :
162+ if self .completion_list and self .completion_list .isVisible ():
163+ self .insert_completion (self .completion_list .currentItem ())
164+ return True # Event handled
165+ elif key_event .key () == Qt .Key_Return :
166+ if self .completion_list and self .completion_list .isVisible ():
167+ self .insert_completion (self .completion_list .currentItem ())
168+ return True # Event handled
169+
170+ # Let the event propagate to the editor
171+ return False
172+
173+ # Let the event propagate to the editor
174+ return False
175+
123176 def _fixContextMenu (self ):
124177
125178 menu = self .menu
@@ -260,6 +313,127 @@ def _watch_paths(self):
260313 if module_paths :
261314 self ._file_watcher .addPaths (module_paths )
262315
316+ def _trigger_autocomplete (self ):
317+ """
318+ Allows the user to ask for autocomplete suggestions.
319+ """
320+
321+ # Clear the status bar
322+ self .statusChanged .emit ("" )
323+
324+ # Track whether or not there are any completions to show
325+ completions_present = False
326+
327+ script = jedi .Script (self .toPlainText (), path = self .filename )
328+
329+ # Clear the completion list
330+ self .completion_list .clear ()
331+
332+ # Check to see if the character before the cursor is an open parenthesis
333+ cursor_pos = self .textCursor ().position ()
334+ text_before_cursor = self .toPlainText ()[:cursor_pos ]
335+ text_after_cursor = self .toPlainText ()[cursor_pos :]
336+ if text_before_cursor .endswith ("(" ):
337+ # If there is a trailing close parentheis after the cursor, remove it
338+ if text_after_cursor .startswith (")" ):
339+ self .textCursor ().deleteChar ()
340+
341+ # Update the script with the modified text
342+ script = jedi .Script (self .toPlainText (), path = self .filename )
343+
344+ # Check if there are any function signatures
345+ signatures = script .get_signatures ()
346+ if signatures :
347+ # Let the rest of the code know that there was a completion
348+ completions_present = True
349+
350+ # Load the signatures into the completion list
351+ for signature in signatures :
352+ # Build a human-readable signature
353+ i = 0
354+ cur_signature = f"{ signature .name } ("
355+ for param in signature .params :
356+ # Prevent trailing comma in parameter list
357+ param_ending = ","
358+ if i == len (signature .params ) - 1 :
359+ param_ending = ""
360+
361+ # If the parameter is optional, do not overload the user with it
362+ if "Optional" in param .description :
363+ i += 1
364+ continue
365+
366+ if "=" in param .description :
367+ cur_signature += f"{ param .name } ={ param .description .split ('=' )[1 ].strip ()} { param_ending } "
368+ else :
369+ cur_signature += f"{ param .name } { param_ending } "
370+ i += 1
371+ cur_signature += ")"
372+
373+ # Add the current signature to the list
374+ item = QListWidgetItem (cur_signature )
375+ self .completion_list .addItem (item )
376+ else :
377+ completions = script .complete ()
378+ if completions :
379+ # Let the rest of the code know that there was a completion
380+ completions_present = True
381+
382+ # Add completions to the list
383+ for completion in completions :
384+ item = QListWidgetItem (completion .name )
385+ self .completion_list .addItem (item )
386+
387+ # Only show the completions list if there were any
388+ if completions_present :
389+ # Position the list near the cursor
390+ cursor_rect = self .cursorRect ()
391+ global_pos = self .mapToGlobal (cursor_rect .bottomLeft ())
392+ self .completion_list .move (global_pos )
393+
394+ # Show the completion list
395+ self .completion_list .show ()
396+
397+ # Select the first item in the list
398+ self .completion_list .setCurrentRow (0 )
399+ else :
400+ # Let the user know that no completions are available
401+ self .statusChanged .emit ("No completions available" )
402+
403+ def insert_completion (self , item ):
404+ """
405+ Inserts the selected completion into the editor.
406+ """
407+
408+ # If there is an open parenthesis before the cursor, replace it with the completion
409+ if (
410+ self .textCursor ().position () > 0
411+ and self .toPlainText ()[self .textCursor ().position () - 1 ] == "("
412+ ):
413+ cursor = self .textCursor ()
414+ cursor .setPosition (cursor .position () - 1 )
415+ cursor .movePosition (QTextCursor .EndOfWord , QTextCursor .KeepAnchor )
416+ cursor .removeSelectedText ()
417+
418+ # Find the last period in the text
419+ text_before_cursor = self .toPlainText ()[: self .textCursor ().position ()]
420+ last_period_index = text_before_cursor .rfind ("." )
421+
422+ # Move the cursor to just after the last period position
423+ cursor = self .textCursor ()
424+ cursor .setPosition (last_period_index + 1 )
425+
426+ # Remove text after last period
427+ cursor .movePosition (QTextCursor .EndOfWord , QTextCursor .KeepAnchor )
428+ cursor .removeSelectedText ()
429+
430+ # Insert the completion text
431+ cursor .insertText (item .text ())
432+ self .setTextCursor (cursor )
433+
434+ # Hide the completion list
435+ self .completion_list .hide ()
436+
263437 # callback triggered by QFileSystemWatcher
264438 def _file_changed (self ):
265439 # neovim writes a file by removing it first so must re-add each time
0 commit comments