Skip to content

Commit 8ce602c

Browse files
authored
Merge pull request #20350 from ccordoba12/issue-20331
PR: Introduce completions correctly for autocompletion characters and improve file completions (Editor)
2 parents 60d53fe + e6cb35c commit 8ce602c

File tree

13 files changed

+287
-52
lines changed

13 files changed

+287
-52
lines changed

.github/scripts/install.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ if [ "$USE_CONDA" = "true" ]; then
1919

2020
# Install dependencies per operating system
2121
if [ "$OS" = "win" ]; then
22+
# This is necessary for our tests related to conda envs to pass since
23+
# the release of Mamba 1.1.0
24+
mamba init
2225
mamba env update --file requirements/windows.yml
2326
elif [ "$OS" = "macos" ]; then
2427
mamba env update --file requirements/macos.yml

.github/workflows/test-win.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ jobs:
110110
run: |
111111
conda info
112112
conda list
113+
# Notes:
114+
# 1. This only works for conda, probably because it has the necessary
115+
# MSYS2 packages to create the connection.
116+
# 2. Check https://github.com/marketplace/actions/debugging-with-tmate for
117+
# usage.
118+
#- name: Setup remote ssh connection
119+
# if: env.USE_CONDA == 'true'
120+
# uses: mxschmitt/action-tmate@v3
113121
- name: Run manifest checks
114122
if: env.RUN_BUILD == 'true'
115123
shell: bash -l {0}

external-deps/python-lsp-server/.gitrepo

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

external-deps/python-lsp-server/pyproject.toml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

external-deps/python-lsp-server/test/plugins/test_completion.py

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spyder/plugins/editor/extensions/closequotes.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
# Copyright © Spyder Project Contributors
44
# Licensed under the terms of the MIT License
55
# (see spyder/__init__.py for details)
6+
67
"""This module contains the close quotes editor extension."""
78

9+
import re
10+
811
# Third party imports
912
from qtpy.QtGui import QTextCursor
1013

@@ -27,10 +30,14 @@ def unmatched_quotes_in_line(text):
2730
2831
Distributed under the terms of the BSD License.
2932
"""
33+
# Remove escaped quotes from counting, but only if they are not to the
34+
# right of a backslash because in that case the backslash is the char that
35+
# is escaped.
36+
text = re.sub(r"(?<!\\)\\'", '', text)
37+
text = re.sub(r'(?<!\\)\\"', '', text)
38+
3039
# We check " first, then ', so complex cases with nested quotes will
3140
# get the " to take precedence.
32-
text = text.replace("\\'", "")
33-
text = text.replace('\\"', '')
3441
if text.count('"') % 2:
3542
return '"'
3643
elif text.count("'") % 2:

spyder/plugins/editor/extensions/tests/test_closequotes.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,44 @@
33
# Copyright © Spyder Project Contributors
44
# Licensed under the terms of the MIT License
55
#
6+
67
"""Tests for close quotes."""
78

89
# Third party imports
910
import pytest
1011
from qtpy.QtCore import Qt
11-
from qtpy.QtGui import QTextCursor
12+
from qtpy.QtGui import QFont, QTextCursor
1213

1314
# Local imports
14-
from spyder.utils.qthelpers import qapplication
15+
from spyder.config.base import running_in_ci
1516
from spyder.plugins.editor.widgets.codeeditor import CodeEditor
1617
from spyder.plugins.editor.utils.editor import TextHelper
1718
from spyder.plugins.editor.extensions.closequotes import (
1819
CloseQuotesExtension)
1920

2021

21-
# --- Fixtures
22+
# ---- Fixtures
2223
# -----------------------------------------------------------------------------
2324
@pytest.fixture
24-
def editor_close_quotes():
25+
def editor_close_quotes(qtbot):
2526
"""Set up Editor with close quotes activated."""
26-
app = qapplication()
2727
editor = CodeEditor(parent=None)
28-
kwargs = {}
29-
kwargs['language'] = 'Python'
30-
kwargs['close_quotes'] = True
31-
editor.setup_editor(**kwargs)
32-
return editor
28+
editor.setup_editor(
29+
color_scheme='spyder/dark',
30+
font=QFont("Courier New", 10),
31+
language='Python',
32+
close_quotes=True
33+
)
3334

34-
# --- Tests
35-
# -----------------------------------------------------------------------------
35+
editor.resize(480, 360)
36+
editor.show()
37+
qtbot.addWidget(editor)
38+
39+
return editor
3640

3741

42+
# ---- Tests
43+
# -----------------------------------------------------------------------------
3844
@pytest.mark.parametrize(
3945
'text, expected_text, cursor_column',
4046
[
@@ -48,13 +54,19 @@ def editor_close_quotes():
4854
("''''", "''''''", 3),
4955
('"some_string"', '"some_string"', 13), # Write a string
5056
("'some_string'", "'some_string'", 13),
57+
(r'"\""', r'"\""', 4), # Write escaped quotes
58+
(r"'\''", r"'\''", 4),
59+
(r'"\\"', r'"\\"', 4), # Don't enter escaped quote if the previous
60+
(r"'\\'", r"'\\'", 4), # char is a backslash (for Windows paths)
5161
])
5262
def test_close_quotes(qtbot, editor_close_quotes, text, expected_text,
5363
cursor_column):
5464
"""Test insertion of extra quotes."""
5565
editor = editor_close_quotes
5666

5767
qtbot.keyClicks(editor, text)
68+
if not running_in_ci():
69+
qtbot.wait(1000)
5870
assert editor.toPlainText() == expected_text
5971

6072
assert cursor_column == TextHelper(editor).current_column_nbr()

spyder/plugins/editor/widgets/base.py

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@
2525
# Local imports
2626
from spyder.config.gui import get_font
2727
from spyder.config.manager import CONF
28-
from spyder.py3compat import PY3, to_text_string
29-
from spyder.widgets.calltip import CallTipWidget, ToolTipWidget
30-
from spyder.widgets.mixins import BaseEditMixin
3128
from spyder.plugins.editor.api.decoration import TextDecoration, DRAW_ORDERS
3229
from spyder.plugins.editor.utils.decoration import TextDecorationsManager
3330
from spyder.plugins.editor.widgets.completion import CompletionWidget
31+
from spyder.plugins.completion.api import CompletionItemKind
3432
from spyder.plugins.outlineexplorer.api import is_cell_header, document_cells
33+
from spyder.py3compat import PY3, to_text_string
3534
from spyder.utils.palette import SpyderPalette
35+
from spyder.widgets.calltip import CallTipWidget, ToolTipWidget
36+
from spyder.widgets.mixins import BaseEditMixin
37+
3638

3739
class TextEditBaseWidget(QPlainTextEdit, BaseEditMixin):
3840
"""Text edit base widget"""
@@ -938,12 +940,14 @@ def insert_completion(self, completion, completion_position):
938940
end = self.get_position_line_number(end_line, end_col)
939941
cursor.setPosition(start)
940942
cursor.setPosition(end, QTextCursor.KeepAnchor)
941-
text = to_text_string(completion['textEdit']['newText'])
943+
text = str(completion['textEdit']['newText'])
942944
else:
943945
text = completion
946+
kind = None
944947
if isinstance(completion, dict):
945948
text = completion['insertText']
946-
text = to_text_string(text)
949+
kind = completion['kind']
950+
text = str(text)
947951

948952
# Get word to the left of the cursor.
949953
result = self.get_current_word_and_position(
@@ -952,14 +956,70 @@ def insert_completion(self, completion, completion_position):
952956
current_text, start_position = result
953957
end_position = start_position + len(current_text)
954958

955-
# Check if the completion position is in the expected range
956-
if not start_position <= completion_position <= end_position:
957-
return
958-
cursor.setPosition(start_position)
959-
960-
# Remove the word under the cursor
961-
cursor.setPosition(end_position,
962-
QTextCursor.KeepAnchor)
959+
# Remove text under cursor only if it's not an autocompletion
960+
# character
961+
is_auto_completion_character = False
962+
if self.objectName() == 'console':
963+
if current_text == '.':
964+
is_auto_completion_character = True
965+
else:
966+
if (
967+
kind != CompletionItemKind.FILE and
968+
current_text in self.auto_completion_characters
969+
):
970+
is_auto_completion_character = True
971+
972+
# Adjustments for file completions
973+
if kind == CompletionItemKind.FILE:
974+
special_chars = ['"', "'", '/', '\\']
975+
976+
if any(
977+
[current_text.endswith(c) for c in special_chars]
978+
):
979+
# This is necessary when completions are requested next
980+
# to special characters.
981+
start_position = end_position
982+
elif current_text.endswith('.') and len(current_text) > 1:
983+
# This inserts completions for files or directories
984+
# that start with a dot
985+
start_position = end_position - 1
986+
elif current_text == '.':
987+
# This is needed if users are asking for file
988+
# completions to the right of a dot when some of its
989+
# name is part of the completed text
990+
cursor_1 = self.textCursor()
991+
found_start = False
992+
993+
# Select text backwards until we find where the file
994+
# name starts
995+
while not found_start:
996+
cursor_1.movePosition(
997+
QTextCursor.PreviousCharacter,
998+
QTextCursor.KeepAnchor,
999+
)
1000+
1001+
selection = str(cursor_1.selectedText())
1002+
if text.startswith(selection):
1003+
found_start = True
1004+
1005+
current_text = str(cursor_1.selectedText())
1006+
start_position = cursor_1.selectionStart()
1007+
end_position = cursor_1.selectionEnd()
1008+
1009+
if not is_auto_completion_character:
1010+
# Check if the completion position is in the expected range
1011+
if not (
1012+
start_position <= completion_position <= end_position
1013+
):
1014+
return
1015+
cursor.setPosition(start_position)
1016+
1017+
# Remove the word under the cursor
1018+
cursor.setPosition(end_position, QTextCursor.KeepAnchor)
1019+
else:
1020+
# Check if we are in the correct position
1021+
if cursor.position() != completion_position:
1022+
return
9631023
else:
9641024
# Check if we are in the correct position
9651025
if cursor.position() != completion_position:

0 commit comments

Comments
 (0)