Skip to content

Commit 65ba73b

Browse files
committed
Merge from 5.x: PR #20298
Fixes #20285
2 parents 4224b3c + 20ffa5c commit 65ba73b

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
@@ -590,9 +590,6 @@ def __init__(self, parent=None):
590590
self.patch = []
591591
self.leading_whitespaces = {}
592592

593-
# re-use parent of completion_widget (usually the main window)
594-
completion_parent = self.completion_widget.parent()
595-
596593
# Some events should not be triggered during undo/redo
597594
# such as line stripping
598595
self.is_undoing = False
@@ -1500,15 +1497,20 @@ def process_completion(self, params):
15001497

15011498
try:
15021499
completions = params['params']
1503-
completions = ([] if completions is None else
1504-
[completion for completion in completions
1505-
if completion.get('insertText')
1506-
or completion.get('textEdit', {}).get('newText')])
1507-
prefix = self.get_current_word(completion=True,
1508-
valid_python_variable=False)
1509-
if (len(completions) == 1
1510-
and completions[0].get('insertText') == prefix
1511-
and not completions[0].get('textEdit', {}).get('newText')):
1500+
completions = (
1501+
[] if completions is None else
1502+
[completion for completion in completions
1503+
if completion.get('insertText')
1504+
or completion.get('textEdit', {}).get('newText')]
1505+
)
1506+
prefix = self.get_current_word(
1507+
completion=True, valid_python_variable=False)
1508+
1509+
if (
1510+
len(completions) == 1 and
1511+
completions[0].get('insertText') == prefix and
1512+
not completions[0].get('textEdit', {}).get('newText')
1513+
):
15121514
completions.pop()
15131515

15141516
replace_end = self.textCursor().position()
@@ -1527,12 +1529,14 @@ def sort_key(completion):
15271529
text_insertion = completion['textEdit']['newText']
15281530
else:
15291531
text_insertion = completion['insertText']
1532+
15301533
first_insert_letter = text_insertion[0]
15311534
case_mismatch = (
15321535
(first_letter.isupper() and first_insert_letter.islower())
15331536
or
15341537
(first_letter.islower() and first_insert_letter.isupper())
15351538
)
1539+
15361540
# False < True, so case matches go first
15371541
return (case_mismatch, completion['sortText'])
15381542

@@ -1545,8 +1549,11 @@ def sort_key(completion):
15451549
if 'textEdit' in completion:
15461550
c_replace_start = completion['textEdit']['range']['start']
15471551
c_replace_end = completion['textEdit']['range']['end']
1548-
if (c_replace_start == replace_start
1549-
and c_replace_end == replace_end):
1552+
1553+
if (
1554+
c_replace_start == replace_start and
1555+
c_replace_end == replace_end
1556+
):
15501557
insert_text = completion['textEdit']['newText']
15511558
completion['filterText'] = insert_text
15521559
completion['insertText'] = insert_text
@@ -4715,8 +4722,11 @@ def keyPressEvent(self, event):
47154722
self.completion_widget.hide()
47164723
if key in (Qt.Key_Enter, Qt.Key_Return):
47174724
if not shift and not ctrl:
4718-
if (self.add_colons_enabled and self.is_python_like() and
4719-
self.autoinsert_colons()):
4725+
if (
4726+
self.add_colons_enabled and
4727+
self.is_python_like() and
4728+
self.autoinsert_colons()
4729+
):
47204730
self.textCursor().beginEditBlock()
47214731
self.insert_text(':' + self.get_line_separator())
47224732
if self.strip_trailing_spaces_on_modify:
@@ -4764,16 +4774,21 @@ def keyPressEvent(self, event):
47644774
trailing_spaces = leading_length - len(leading_text.rstrip())
47654775
trailing_text = self.get_text('cursor', 'eol')
47664776
matches = ('()', '[]', '{}', '\'\'', '""')
4767-
if (not leading_text.strip() and
4768-
(leading_length > len(self.indent_chars))):
4777+
if (
4778+
not leading_text.strip() and
4779+
(leading_length > len(self.indent_chars))
4780+
):
47694781
if leading_length % len(self.indent_chars) == 0:
47704782
self.unindent()
47714783
else:
47724784
self._handle_keypress_event(event)
47734785
elif trailing_spaces and not trailing_text.strip():
47744786
self.remove_suffix(leading_text[-trailing_spaces:])
4775-
elif (leading_text and trailing_text and
4776-
(leading_text[-1] + trailing_text[0] in matches)):
4787+
elif (
4788+
leading_text and
4789+
trailing_text and
4790+
(leading_text[-1] + trailing_text[0] in matches)
4791+
):
47774792
cursor = self.textCursor()
47784793
cursor.movePosition(QTextCursor.PreviousCharacter)
47794794
cursor.movePosition(QTextCursor.NextCharacter,
@@ -4788,27 +4803,36 @@ def keyPressEvent(self, event):
47884803
# redefine this basic action which should have been implemented
47894804
# natively
47904805
self.stdkey_end(shift, ctrl)
4791-
elif (text in self.auto_completion_characters and
4792-
self.automatic_completions):
4806+
elif (
4807+
text in self.auto_completion_characters and
4808+
self.automatic_completions
4809+
):
47934810
self.insert_text(text)
47944811
if text == ".":
47954812
if not self.in_comment_or_string():
47964813
text = self.get_text('sol', 'cursor')
47974814
last_obj = getobj(text)
47984815
prev_char = text[-2] if len(text) > 1 else ''
4799-
if (prev_char in {')', ']', '}'} or
4800-
(last_obj and not last_obj.isdigit())):
4816+
if (
4817+
prev_char in {')', ']', '}'} or
4818+
(last_obj and not last_obj.isdigit())
4819+
):
48014820
# Completions should be triggered immediately when
48024821
# an autocompletion character is introduced.
48034822
self.do_completion(automatic=True)
48044823
else:
48054824
self.do_completion(automatic=True)
4806-
elif (text in self.signature_completion_characters and
4807-
not self.has_selected_text()):
4825+
elif (
4826+
text in self.signature_completion_characters and
4827+
not self.has_selected_text()
4828+
):
48084829
self.insert_text(text)
48094830
self.request_signature()
4810-
elif (key == Qt.Key_Colon and not has_selection and
4811-
self.auto_unindent_enabled):
4831+
elif (
4832+
key == Qt.Key_Colon and
4833+
not has_selection and
4834+
self.auto_unindent_enabled
4835+
):
48124836
leading_text = self.get_text('sol', 'cursor')
48134837
if leading_text.lstrip() in ('else', 'finally'):
48144838
ind = lambda txt: len(txt) - len(txt.lstrip())
@@ -4819,8 +4843,13 @@ def keyPressEvent(self, event):
48194843
if ind(leading_text) == ind(prevtxt):
48204844
self.unindent(force=True)
48214845
self._handle_keypress_event(event)
4822-
elif (key == Qt.Key_Space and not shift and not ctrl and not
4823-
has_selection and self.auto_unindent_enabled):
4846+
elif (
4847+
key == Qt.Key_Space and
4848+
not shift and
4849+
not ctrl and
4850+
not has_selection and
4851+
self.auto_unindent_enabled
4852+
):
48244853
self.completion_widget.hide()
48254854
leading_text = self.get_text('sol', 'cursor')
48264855
if leading_text.lstrip() in ('elif', 'except'):
@@ -4905,13 +4934,20 @@ def do_automatic_completions(self):
49054934
is_backspace = (
49064935
self.is_completion_widget_visible() and key == Qt.Key_Backspace)
49074936

4908-
if (len(text) >= self.automatic_completions_after_chars
4909-
and self._last_key_pressed_text or is_backspace):
4937+
if (
4938+
(len(text) >= self.automatic_completions_after_chars) and
4939+
self._last_key_pressed_text or
4940+
is_backspace
4941+
):
49104942
# Perform completion on the fly
49114943
if not self.in_comment_or_string():
49124944
# Variables can include numbers and underscores
4913-
if (text.isalpha() or text.isalnum() or '_' in text
4914-
or '.' in text):
4945+
if (
4946+
text.isalpha() or
4947+
text.isalnum() or
4948+
'_' in text
4949+
or '.' in text
4950+
):
49154951
self.do_completion(automatic=True)
49164952
self._last_key_pressed_text = ''
49174953
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
@@ -1180,5 +1184,75 @@ def test_completions_environment(completions_codeeditor, qtbot, tmpdir):
11801184
completion_plugin.after_configuration_update([])
11811185

11821186

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