Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ if [ "$USE_CONDA" = "true" ]; then

# Install dependencies per operating system
if [ "$OS" = "win" ]; then
# This is necessary for our tests related to conda envs to pass since
# the release of Mamba 1.1.0
mamba init
mamba env update --file requirements/windows.yml
elif [ "$OS" = "macos" ]; then
mamba env update --file requirements/macos.yml
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/test-win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ jobs:
run: |
conda info
conda list
# Notes:
# 1. This only works for conda, probably because it has the necessary
# MSYS2 packages to create the connection.
# 2. Check https://github.com/marketplace/actions/debugging-with-tmate for
# usage.
#- name: Setup remote ssh connection
# if: env.USE_CONDA == 'true'
# uses: mxschmitt/action-tmate@v3
- name: Run manifest checks
if: env.RUN_BUILD == 'true'
shell: bash -l {0}
Expand Down
4 changes: 2 additions & 2 deletions external-deps/python-lsp-server/.gitrepo

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions external-deps/python-lsp-server/pylsp/plugins/jedi_completion.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion external-deps/python-lsp-server/pylsp/plugins/pylint_lint.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions external-deps/python-lsp-server/pyproject.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions external-deps/python-lsp-server/test/plugins/test_completion.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions spyder/plugins/editor/extensions/closequotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""This module contains the close quotes editor extension."""

import re

# Third party imports
from qtpy.QtGui import QTextCursor

Expand All @@ -27,10 +30,14 @@ def unmatched_quotes_in_line(text):

Distributed under the terms of the BSD License.
"""
# Remove escaped quotes from counting, but only if they are not to the
# right of a backslash because in that case the backslash is the char that
# is escaped.
text = re.sub(r"(?<!\\)\\'", '', text)
text = re.sub(r'(?<!\\)\\"', '', text)

# We check " first, then ', so complex cases with nested quotes will
# get the " to take precedence.
text = text.replace("\\'", "")
text = text.replace('\\"', '')
if text.count('"') % 2:
return '"'
elif text.count("'") % 2:
Expand Down
36 changes: 24 additions & 12 deletions spyder/plugins/editor/extensions/tests/test_closequotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,44 @@
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
#

"""Tests for close quotes."""

# Third party imports
import pytest
from qtpy.QtCore import Qt
from qtpy.QtGui import QTextCursor
from qtpy.QtGui import QFont, QTextCursor

# Local imports
from spyder.utils.qthelpers import qapplication
from spyder.config.base import running_in_ci
from spyder.plugins.editor.widgets.codeeditor import CodeEditor
from spyder.plugins.editor.utils.editor import TextHelper
from spyder.plugins.editor.extensions.closequotes import (
CloseQuotesExtension)


# --- Fixtures
# ---- Fixtures
# -----------------------------------------------------------------------------
@pytest.fixture
def editor_close_quotes():
def editor_close_quotes(qtbot):
"""Set up Editor with close quotes activated."""
app = qapplication()
editor = CodeEditor(parent=None)
kwargs = {}
kwargs['language'] = 'Python'
kwargs['close_quotes'] = True
editor.setup_editor(**kwargs)
return editor
editor.setup_editor(
color_scheme='spyder/dark',
font=QFont("Courier New", 10),
language='Python',
close_quotes=True
)

# --- Tests
# -----------------------------------------------------------------------------
editor.resize(480, 360)
editor.show()
qtbot.addWidget(editor)

return editor


# ---- Tests
# -----------------------------------------------------------------------------
@pytest.mark.parametrize(
'text, expected_text, cursor_column',
[
Expand All @@ -48,13 +54,19 @@ def editor_close_quotes():
("''''", "''''''", 3),
('"some_string"', '"some_string"', 13), # Write a string
("'some_string'", "'some_string'", 13),
(r'"\""', r'"\""', 4), # Write escaped quotes
(r"'\''", r"'\''", 4),
(r'"\\"', r'"\\"', 4), # Don't enter escaped quote if the previous
(r"'\\'", r"'\\'", 4), # char is a backslash (for Windows paths)
])
def test_close_quotes(qtbot, editor_close_quotes, text, expected_text,
cursor_column):
"""Test insertion of extra quotes."""
editor = editor_close_quotes

qtbot.keyClicks(editor, text)
if not running_in_ci():
qtbot.wait(1000)
assert editor.toPlainText() == expected_text

assert cursor_column == TextHelper(editor).current_column_nbr()
Expand Down
86 changes: 73 additions & 13 deletions spyder/plugins/editor/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@
# Local imports
from spyder.config.gui import get_font
from spyder.config.manager import CONF
from spyder.py3compat import PY3, to_text_string
from spyder.widgets.calltip import CallTipWidget, ToolTipWidget
from spyder.widgets.mixins import BaseEditMixin
from spyder.plugins.editor.api.decoration import TextDecoration, DRAW_ORDERS
from spyder.plugins.editor.utils.decoration import TextDecorationsManager
from spyder.plugins.editor.widgets.completion import CompletionWidget
from spyder.plugins.completion.api import CompletionItemKind
from spyder.plugins.outlineexplorer.api import is_cell_header, document_cells
from spyder.py3compat import PY3, to_text_string
from spyder.utils.palette import SpyderPalette
from spyder.widgets.calltip import CallTipWidget, ToolTipWidget
from spyder.widgets.mixins import BaseEditMixin


class TextEditBaseWidget(QPlainTextEdit, BaseEditMixin):
"""Text edit base widget"""
Expand Down Expand Up @@ -938,12 +940,14 @@ def insert_completion(self, completion, completion_position):
end = self.get_position_line_number(end_line, end_col)
cursor.setPosition(start)
cursor.setPosition(end, QTextCursor.KeepAnchor)
text = to_text_string(completion['textEdit']['newText'])
text = str(completion['textEdit']['newText'])
else:
text = completion
kind = None
if isinstance(completion, dict):
text = completion['insertText']
text = to_text_string(text)
kind = completion['kind']
text = str(text)

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

# Check if the completion position is in the expected range
if not start_position <= completion_position <= end_position:
return
cursor.setPosition(start_position)

# Remove the word under the cursor
cursor.setPosition(end_position,
QTextCursor.KeepAnchor)
# Remove text under cursor only if it's not an autocompletion
# character
is_auto_completion_character = False
if self.objectName() == 'console':
if current_text == '.':
is_auto_completion_character = True
else:
if (
kind != CompletionItemKind.FILE and
current_text in self.auto_completion_characters
):
is_auto_completion_character = True

# Adjustments for file completions
if kind == CompletionItemKind.FILE:
special_chars = ['"', "'", '/', '\\']

if any(
[current_text.endswith(c) for c in special_chars]
):
# This is necessary when completions are requested next
# to special characters.
start_position = end_position
elif current_text.endswith('.') and len(current_text) > 1:
# This inserts completions for files or directories
# that start with a dot
start_position = end_position - 1
elif current_text == '.':
# This is needed if users are asking for file
# completions to the right of a dot when some of its
# name is part of the completed text
cursor_1 = self.textCursor()
found_start = False

# Select text backwards until we find where the file
# name starts
while not found_start:
cursor_1.movePosition(
QTextCursor.PreviousCharacter,
QTextCursor.KeepAnchor,
)

selection = str(cursor_1.selectedText())
if text.startswith(selection):
found_start = True

current_text = str(cursor_1.selectedText())
start_position = cursor_1.selectionStart()
end_position = cursor_1.selectionEnd()

if not is_auto_completion_character:
# Check if the completion position is in the expected range
if not (
start_position <= completion_position <= end_position
):
return
cursor.setPosition(start_position)

# Remove the word under the cursor
cursor.setPosition(end_position, QTextCursor.KeepAnchor)
else:
# Check if we are in the correct position
if cursor.position() != completion_position:
return
else:
# Check if we are in the correct position
if cursor.position() != completion_position:
Expand Down
Loading