3
3
from modulefinder import ModuleFinder
4
4
5
5
from 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
9
16
from path import Path
10
17
11
18
import sys
12
19
20
+ import jedi
21
+
13
22
from pyqtgraph .parametertree import Parameter
14
23
15
24
from ..mixins import ComponentMixin
@@ -26,6 +35,7 @@ class Editor(CodeEditor, ComponentMixin):
26
35
# autoreload is enabled.
27
36
triggerRerender = pyqtSignal (bool )
28
37
sigFilenameChanged = pyqtSignal (str )
38
+ statusChanged = pyqtSignal (str )
29
39
30
40
preferences = Parameter .create (
31
41
name = "Preferences" ,
@@ -54,6 +64,9 @@ class Editor(CodeEditor, ComponentMixin):
54
64
# Tracks whether or not the document was saved from the Spyder editor vs an external editor
55
65
was_modified_by_self = False
56
66
67
+ # Helps display the completion list for the editor
68
+ completion_list = None
69
+
57
70
def __init__ (self , parent = None ):
58
71
59
72
self ._watched_file = None
@@ -120,6 +133,46 @@ def __init__(self, parent=None):
120
133
121
134
self .updatePreferences ()
122
135
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
+
123
176
def _fixContextMenu (self ):
124
177
125
178
menu = self .menu
@@ -260,6 +313,127 @@ def _watch_paths(self):
260
313
if module_paths :
261
314
self ._file_watcher .addPaths (module_paths )
262
315
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
+
263
437
# callback triggered by QFileSystemWatcher
264
438
def _file_changed (self ):
265
439
# neovim writes a file by removing it first so must re-add each time
0 commit comments