Skip to content

Commit 20ffa5c

Browse files
authored
Merge pull request #20298 from ccordoba12/issue-20285
PR: Fix automatic completions after a dot is written next to a module
2 parents b462b20 + 9de3241 commit 20ffa5c

File tree

4 files changed

+152
-38
lines changed

4 files changed

+152
-38
lines changed

spyder/plugins/editor/widgets/base.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,8 @@ def select_completion_list(self):
909909
self.completion_widget.item_selected()
910910

911911
def insert_completion(self, completion, completion_position):
912-
"""Insert a completion into the editor.
912+
"""
913+
Insert a completion into the editor.
913914
914915
completion_position is where the completion was generated.
915916
@@ -944,15 +945,18 @@ def insert_completion(self, completion, completion_position):
944945
text = completion['insertText']
945946
text = to_text_string(text)
946947

947-
# Get word on the left of the cursor.
948-
result = self.get_current_word_and_position(completion=True)
948+
# Get word to the left of the cursor.
949+
result = self.get_current_word_and_position(
950+
completion=True, valid_python_variable=False)
949951
if result is not None:
950952
current_text, start_position = result
951953
end_position = start_position + len(current_text)
954+
952955
# Check if the completion position is in the expected range
953956
if not start_position <= completion_position <= end_position:
954957
return
955958
cursor.setPosition(start_position)
959+
956960
# Remove the word under the cursor
957961
cursor.setPosition(end_position,
958962
QTextCursor.KeepAnchor)

spyder/plugins/editor/widgets/codeeditor.py

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -605,9 +605,6 @@ def __init__(self, parent=None):
605605
self.patch = []
606606
self.leading_whitespaces = {}
607607

608-
# re-use parent of completion_widget (usually the main window)
609-
completion_parent = self.completion_widget.parent()
610-
611608
# Some events should not be triggered during undo/redo
612609
# such as line stripping
613610
self.is_undoing = False
@@ -1523,15 +1520,20 @@ def process_completion(self, params):
15231520

15241521
try:
15251522
completions = params['params']
1526-
completions = ([] if completions is None else
1527-
[completion for completion in completions
1528-
if completion.get('insertText')
1529-
or completion.get('textEdit', {}).get('newText')])
1530-
prefix = self.get_current_word(completion=True,
1531-
valid_python_variable=False)
1532-
if (len(completions) == 1
1533-
and completions[0].get('insertText') == prefix
1534-
and not completions[0].get('textEdit', {}).get('newText')):
1523+
completions = (
1524+
[] if completions is None else
1525+
[completion for completion in completions
1526+
if completion.get('insertText')
1527+
or completion.get('textEdit', {}).get('newText')]
1528+
)
1529+
prefix = self.get_current_word(
1530+
completion=True, valid_python_variable=False)
1531+
1532+
if (
1533+
len(completions) == 1 and
1534+
completions[0].get('insertText') == prefix and
1535+
not completions[0].get('textEdit', {}).get('newText')
1536+
):
15351537
completions.pop()
15361538

15371539
replace_end = self.textCursor().position()
@@ -1550,12 +1552,14 @@ def sort_key(completion):
15501552
text_insertion = completion['textEdit']['newText']
15511553
else:
15521554
text_insertion = completion['insertText']
1555+
15531556
first_insert_letter = text_insertion[0]
15541557
case_mismatch = (
15551558
(first_letter.isupper() and first_insert_letter.islower())
15561559
or
15571560
(first_letter.islower() and first_insert_letter.isupper())
15581561
)
1562+
15591563
# False < True, so case matches go first
15601564
return (case_mismatch, completion['sortText'])
15611565

@@ -1568,8 +1572,11 @@ def sort_key(completion):
15681572
if 'textEdit' in completion:
15691573
c_replace_start = completion['textEdit']['range']['start']
15701574
c_replace_end = completion['textEdit']['range']['end']
1571-
if (c_replace_start == replace_start
1572-
and c_replace_end == replace_end):
1575+
1576+
if (
1577+
c_replace_start == replace_start and
1578+
c_replace_end == replace_end
1579+
):
15731580
insert_text = completion['textEdit']['newText']
15741581
completion['filterText'] = insert_text
15751582
completion['insertText'] = insert_text
@@ -4766,8 +4773,11 @@ def keyPressEvent(self, event):
47664773
self.completion_widget.hide()
47674774
if key in (Qt.Key_Enter, Qt.Key_Return):
47684775
if not shift and not ctrl:
4769-
if (self.add_colons_enabled and self.is_python_like() and
4770-
self.autoinsert_colons()):
4776+
if (
4777+
self.add_colons_enabled and
4778+
self.is_python_like() and
4779+
self.autoinsert_colons()
4780+
):
47714781
self.textCursor().beginEditBlock()
47724782
self.insert_text(':' + self.get_line_separator())
47734783
if self.strip_trailing_spaces_on_modify:
@@ -4815,16 +4825,21 @@ def keyPressEvent(self, event):
48154825
trailing_spaces = leading_length - len(leading_text.rstrip())
48164826
trailing_text = self.get_text('cursor', 'eol')
48174827
matches = ('()', '[]', '{}', '\'\'', '""')
4818-
if (not leading_text.strip() and
4819-
(leading_length > len(self.indent_chars))):
4828+
if (
4829+
not leading_text.strip() and
4830+
(leading_length > len(self.indent_chars))
4831+
):
48204832
if leading_length % len(self.indent_chars) == 0:
48214833
self.unindent()
48224834
else:
48234835
self._handle_keypress_event(event)
48244836
elif trailing_spaces and not trailing_text.strip():
48254837
self.remove_suffix(leading_text[-trailing_spaces:])
4826-
elif (leading_text and trailing_text and
4827-
(leading_text[-1] + trailing_text[0] in matches)):
4838+
elif (
4839+
leading_text and
4840+
trailing_text and
4841+
(leading_text[-1] + trailing_text[0] in matches)
4842+
):
48284843
cursor = self.textCursor()
48294844
cursor.movePosition(QTextCursor.PreviousCharacter)
48304845
cursor.movePosition(QTextCursor.NextCharacter,
@@ -4839,27 +4854,36 @@ def keyPressEvent(self, event):
48394854
# redefine this basic action which should have been implemented
48404855
# natively
48414856
self.stdkey_end(shift, ctrl)
4842-
elif (text in self.auto_completion_characters and
4843-
self.automatic_completions):
4857+
elif (
4858+
text in self.auto_completion_characters and
4859+
self.automatic_completions
4860+
):
48444861
self.insert_text(text)
48454862
if text == ".":
48464863
if not self.in_comment_or_string():
48474864
text = self.get_text('sol', 'cursor')
48484865
last_obj = getobj(text)
48494866
prev_char = text[-2] if len(text) > 1 else ''
4850-
if (prev_char in {')', ']', '}'} or
4851-
(last_obj and not last_obj.isdigit())):
4867+
if (
4868+
prev_char in {')', ']', '}'} or
4869+
(last_obj and not last_obj.isdigit())
4870+
):
48524871
# Completions should be triggered immediately when
48534872
# an autocompletion character is introduced.
48544873
self.do_completion(automatic=True)
48554874
else:
48564875
self.do_completion(automatic=True)
4857-
elif (text in self.signature_completion_characters and
4858-
not self.has_selected_text()):
4876+
elif (
4877+
text in self.signature_completion_characters and
4878+
not self.has_selected_text()
4879+
):
48594880
self.insert_text(text)
48604881
self.request_signature()
4861-
elif (key == Qt.Key_Colon and not has_selection and
4862-
self.auto_unindent_enabled):
4882+
elif (
4883+
key == Qt.Key_Colon and
4884+
not has_selection and
4885+
self.auto_unindent_enabled
4886+
):
48634887
leading_text = self.get_text('sol', 'cursor')
48644888
if leading_text.lstrip() in ('else', 'finally'):
48654889
ind = lambda txt: len(txt) - len(txt.lstrip())
@@ -4870,8 +4894,13 @@ def keyPressEvent(self, event):
48704894
if ind(leading_text) == ind(prevtxt):
48714895
self.unindent(force=True)
48724896
self._handle_keypress_event(event)
4873-
elif (key == Qt.Key_Space and not shift and not ctrl and not
4874-
has_selection and self.auto_unindent_enabled):
4897+
elif (
4898+
key == Qt.Key_Space and
4899+
not shift and
4900+
not ctrl and
4901+
not has_selection and
4902+
self.auto_unindent_enabled
4903+
):
48754904
self.completion_widget.hide()
48764905
leading_text = self.get_text('sol', 'cursor')
48774906
if leading_text.lstrip() in ('elif', 'except'):
@@ -4956,13 +4985,20 @@ def do_automatic_completions(self):
49564985
is_backspace = (
49574986
self.is_completion_widget_visible() and key == Qt.Key_Backspace)
49584987

4959-
if (len(text) >= self.automatic_completions_after_chars
4960-
and self._last_key_pressed_text or is_backspace):
4988+
if (
4989+
(len(text) >= self.automatic_completions_after_chars) and
4990+
self._last_key_pressed_text or
4991+
is_backspace
4992+
):
49614993
# Perform completion on the fly
49624994
if not self.in_comment_or_string():
49634995
# Variables can include numbers and underscores
4964-
if (text.isalpha() or text.isalnum() or '_' in text
4965-
or '.' in text):
4996+
if (
4997+
text.isalpha() or
4998+
text.isalnum() or
4999+
'_' in text
5000+
or '.' in text
5001+
):
49665002
self.do_completion(automatic=True)
49675003
self._last_key_pressed_text = ''
49685004
self._last_pressed_key = None

spyder/plugins/editor/widgets/tests/test_introspection.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
# Local imports
2424
from spyder.config.base import running_in_ci, running_in_ci_with_conda
2525
from spyder.config.utils import is_anaconda
26+
from spyder.plugins.completion.api import (
27+
CompletionRequestTypes, CompletionItemKind)
28+
from spyder.plugins.completion.providers.languageserver.providers.utils import (
29+
path_as_uri)
2630
from spyder.plugins.completion.providers.kite.utils.status import (
2731
check_if_kite_installed, check_if_kite_running)
2832
from spyder.py3compat import PY2
@@ -1178,5 +1182,75 @@ def test_completions_environment(completions_codeeditor, qtbot, tmpdir):
11781182
completion_plugin.after_configuration_update([])
11791183

11801184

1185+
@pytest.mark.slow
1186+
@pytest.mark.order(1)
1187+
@flaky(max_runs=5)
1188+
def test_dot_completions(completions_codeeditor, qtbot):
1189+
"""
1190+
Test that completions after a dot are working as expected.
1191+
1192+
This is a regression test for issue spyder-ide/spyder#20285
1193+
"""
1194+
code_editor, _ = completions_codeeditor
1195+
completion = code_editor.completion_widget
1196+
1197+
# Import module and check completions are shown for it after writing a dot
1198+
# after it
1199+
qtbot.keyClicks(code_editor, "import math")
1200+
qtbot.keyPress(code_editor, Qt.Key_Enter)
1201+
1202+
qtbot.wait(500)
1203+
assert not completion.isVisible()
1204+
1205+
with qtbot.waitSignal(completion.sig_show_completions, timeout=10000):
1206+
qtbot.keyClicks(code_editor, "math.")
1207+
1208+
qtbot.wait(500)
1209+
assert completion.isVisible()
1210+
1211+
1212+
@pytest.mark.slow
1213+
@pytest.mark.order(1)
1214+
def test_completions_for_files_that_start_with_numbers(
1215+
mock_completions_codeeditor, qtbot):
1216+
"""
1217+
Test that completions for files that start with numbers are handled as
1218+
expected.
1219+
1220+
This is a regression test for issue spyder-ide/spyder#20156
1221+
"""
1222+
code_editor, mock_response = mock_completions_codeeditor
1223+
completion = code_editor.completion_widget
1224+
file_name = '000_testing.txt'
1225+
1226+
# Set text to complete and move cursor to the position we want to ask for
1227+
# completions.
1228+
qtbot.keyClicks(code_editor, "'0'")
1229+
code_editor.moveCursor(QTextCursor.PreviousCharacter)
1230+
qtbot.wait(500)
1231+
1232+
# Complete '0' -> '000_testing.txt'
1233+
mock_response.side_effect = lambda lang, method, params: {'params': [{
1234+
'label': f'{file_name}',
1235+
'kind': CompletionItemKind.FILE,
1236+
'sortText': (0, f'a{file_name}'),
1237+
'insertText': f'{file_name}',
1238+
'data': {'doc_uri': path_as_uri(__file__)},
1239+
'detail': '',
1240+
'documentation': '',
1241+
'filterText': f'{file_name}',
1242+
'insertTextFormat': 1,
1243+
'provider': 'LSP',
1244+
'resolve': True
1245+
}]} if method == CompletionRequestTypes.DOCUMENT_COMPLETION else None
1246+
1247+
with qtbot.waitSignal(completion.sig_show_completions,
1248+
timeout=10000):
1249+
qtbot.keyPress(code_editor, Qt.Key_Tab, delay=300)
1250+
1251+
qtbot.wait(500)
1252+
assert code_editor.get_text_with_eol() == f"'{file_name}'"
1253+
1254+
11811255
if __name__ == '__main__':
11821256
pytest.main(['test_introspection.py', '--run-slow'])

spyder/widgets/mixins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,7 @@ def is_special_character(move):
10831083
startpos = cursor.selectionStart()
10841084

10851085
# Find a valid Python variable name
1086-
if valid_python_variable and not completion:
1086+
if valid_python_variable:
10871087
match = re.findall(r'([^\d\W]\w*)', text, re.UNICODE)
10881088
if not match:
10891089
# This is assumed in several places of our codebase,

0 commit comments

Comments
 (0)