QCodeSitter 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qcodesitter-0.2.0/.github/workflows/publish.yml +22 -0
- qcodesitter-0.2.0/.gitignore +11 -0
- {qcodesitter-0.1.0/src/QCodeSitter.egg-info → qcodesitter-0.2.0}/PKG-INFO +1 -1
- qcodesitter-0.2.0/build_venv_and_kickoff.sh +47 -0
- qcodesitter-0.2.0/examples/demo.py +26 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/pyproject.toml +4 -2
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/highlight_matching_selection.py +9 -2
- qcodesitter-0.2.0/src/QCodeSitter/behaviors/line_commands.py +120 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/multi_cursor_paint.py +1 -1
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/syntax_highlighting.py +6 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/cursors/multi.py +25 -4
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/defaults.py +2 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/gui_colors.py +1 -1
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/themes.py +5 -4
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/tree_manager.py +26 -20
- {qcodesitter-0.1.0 → qcodesitter-0.2.0/src/QCodeSitter.egg-info}/PKG-INFO +1 -1
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter.egg-info/SOURCES.txt +40 -1
- qcodesitter-0.2.0/src/QCodeSitter.egg-info/scm_file_list.json +87 -0
- qcodesitter-0.2.0/src/QCodeSitter.egg-info/scm_version.json +8 -0
- qcodesitter-0.2.0/tests/__init__.py +1 -0
- qcodesitter-0.2.0/tests/behaviors/__init__.py +1 -0
- qcodesitter-0.2.0/tests/behaviors/test_behaviors.py +425 -0
- qcodesitter-0.2.0/tests/behaviors/test_code_folding.py +327 -0
- qcodesitter-0.2.0/tests/behaviors/test_comment_toggle.py +226 -0
- qcodesitter-0.2.0/tests/behaviors/test_gutters.py +99 -0
- qcodesitter-0.2.0/tests/behaviors/test_highlight_matching.py +109 -0
- qcodesitter-0.2.0/tests/behaviors/test_tab_completion.py +325 -0
- qcodesitter-0.2.0/tests/conftest.py +122 -0
- qcodesitter-0.2.0/tests/editor/__init__.py +1 -0
- qcodesitter-0.2.0/tests/editor/test_clipboard.py +127 -0
- qcodesitter-0.2.0/tests/editor/test_cursors.py +276 -0
- qcodesitter-0.2.0/tests/editor/test_editing_keys.py +77 -0
- qcodesitter-0.2.0/tests/editor/test_editor_core.py +497 -0
- qcodesitter-0.2.0/tests/editor/test_line_edits.py +124 -0
- qcodesitter-0.2.0/tests/editor/test_multi_cursor.py +387 -0
- qcodesitter-0.2.0/tests/editor/test_options.py +310 -0
- qcodesitter-0.2.0/tests/editor/test_shortcuts.py +61 -0
- qcodesitter-0.2.0/tests/editor/test_utils.py +73 -0
- qcodesitter-0.2.0/tests/fuzz/__init__.py +0 -0
- qcodesitter-0.2.0/tests/fuzz/test_fuzz.py +410 -0
- qcodesitter-0.2.0/tests/helpers.py +130 -0
- qcodesitter-0.2.0/tests/integration/__init__.py +0 -0
- qcodesitter-0.2.0/tests/integration/test_demo.py +26 -0
- qcodesitter-0.2.0/tests/integration/test_preditor_interface.py +263 -0
- qcodesitter-0.2.0/tests/syntax/__init__.py +1 -0
- qcodesitter-0.2.0/tests/syntax/test_incremental.py +182 -0
- qcodesitter-0.2.0/tests/syntax/test_languages.py +125 -0
- qcodesitter-0.2.0/tests/syntax/test_queries.py +138 -0
- qcodesitter-0.2.0/tests/syntax/test_syntax.py +387 -0
- qcodesitter-0.2.0/tests/syntax/test_themes.py +78 -0
- qcodesitter-0.2.0/tox.ini +73 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/LICENSE.txt +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/README.md +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/setup.cfg +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/__init__.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/__init__.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/auto_bracket.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/code_folding.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/comment_toggle.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/highlight_matching_brackets.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/line_numbers.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/overscroll.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/smart_indent.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/tab_completion/__init__.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/tab_completion/popup.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/tab_completion/providers.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/tab_completion/types.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/code_editor.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/cursors/__init__.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/cursors/clipboard.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/cursors/iterator.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/cursors/line_edits.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/cursors/state.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/cursors/tracked.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/editing_keys.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/editor_options.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/gutters.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/integrations/__init__.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/integrations/preditor_interface.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/languages/__init__.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/languages/base.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/languages/python.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/languages/python_highlights.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/py.typed +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/queries/__init__.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/queries/predicates.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/syntax_specs.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/tracked_document.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/utils.py +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter.egg-info/dependency_links.txt +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter.egg-info/entry_points.txt +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter.egg-info/requires.txt +0 -0
- {qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # PyPI trusted publishing, no API token needed
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v7
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0 # setuptools-scm needs the tags to compute the version
|
|
17
|
+
- uses: actions/setup-python@v7
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.13"
|
|
20
|
+
- run: python -m pip install build
|
|
21
|
+
- run: python -m build
|
|
22
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Resolve the directory this script lives in
|
|
5
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
VENV_DIR="$SCRIPT_DIR/.venv"
|
|
7
|
+
|
|
8
|
+
# Pick a Python executable. Default to the py launcher
|
|
9
|
+
if command -v py >/dev/null 2>&1; then
|
|
10
|
+
PYTHON="py -3.13"
|
|
11
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
12
|
+
PYTHON=python3
|
|
13
|
+
elif command -v python >/dev/null 2>&1; then
|
|
14
|
+
PYTHON=python
|
|
15
|
+
else
|
|
16
|
+
echo "Error: Python not found on PATH"
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
activate_venv() {
|
|
21
|
+
if [[ -f "$VENV_DIR/bin/activate" ]]; then
|
|
22
|
+
# macOS / Linux
|
|
23
|
+
source "$VENV_DIR/bin/activate"
|
|
24
|
+
elif [[ -f "$VENV_DIR/Scripts/activate" ]]; then
|
|
25
|
+
# Windows (Git Bash / MSYS)
|
|
26
|
+
source "$VENV_DIR/Scripts/activate"
|
|
27
|
+
else
|
|
28
|
+
echo "Error: Could not find virtualenv activate script"
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Create virtual environment if it doesn't exist
|
|
34
|
+
if [[ ! -d "$VENV_DIR" ]]; then
|
|
35
|
+
echo "Building Virtualenv..."
|
|
36
|
+
$PYTHON -m venv "$VENV_DIR"
|
|
37
|
+
|
|
38
|
+
activate_venv
|
|
39
|
+
# Upgrade pip and install PySide6
|
|
40
|
+
python -m pip install --upgrade pip
|
|
41
|
+
pip install PySide6 Preditor[cli]
|
|
42
|
+
pip install -e "${SCRIPT_DIR}[preditor]"
|
|
43
|
+
else
|
|
44
|
+
activate_venv
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
python "${SCRIPT_DIR}/examples/demo.py"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Open a window with a fully featured editor. Run with `python examples/demo.py`"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from Qt import QtWidgets
|
|
6
|
+
|
|
7
|
+
from QCodeSitter import CodeEditor, default_options, install_default_behaviors
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_window() -> QtWidgets.QMainWindow:
|
|
11
|
+
win = QtWidgets.QMainWindow()
|
|
12
|
+
editor = CodeEditor(default_options(), parent=win)
|
|
13
|
+
install_default_behaviors(editor, optional=True)
|
|
14
|
+
win.setCentralWidget(editor)
|
|
15
|
+
return win
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
app = QtWidgets.QApplication(sys.argv)
|
|
20
|
+
win = build_window()
|
|
21
|
+
win.show()
|
|
22
|
+
app.exec()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=77"]
|
|
2
|
+
requires = ["setuptools>=77", "setuptools-scm>=8"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "QCodeSitter"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"] # from the git tag, via setuptools-scm
|
|
8
8
|
description = "A tree-sitter powered code editor widget for Qt"
|
|
9
9
|
requires-python = ">=3.10"
|
|
10
10
|
# Qt.py needs a Qt binding: install one yourself, or use the pyside6 extra.
|
|
@@ -45,6 +45,8 @@ where = ["src"]
|
|
|
45
45
|
[tool.setuptools.package-data]
|
|
46
46
|
QCodeSitter = ["py.typed"]
|
|
47
47
|
|
|
48
|
+
[tool.setuptools_scm]
|
|
49
|
+
|
|
48
50
|
[tool.mypy]
|
|
49
51
|
python_version = "3.10"
|
|
50
52
|
|
{qcodesitter-0.1.0 → qcodesitter-0.2.0}/src/QCodeSitter/behaviors/highlight_matching_selection.py
RENAMED
|
@@ -22,6 +22,8 @@ class HighlightMatchingSelection(Behavior):
|
|
|
22
22
|
super().__init__(editor)
|
|
23
23
|
self.watch_options({"colors": self._set_colors})
|
|
24
24
|
self.connect(self.editor.selectionChanged, self.highlight_occurrences)
|
|
25
|
+
# Ctrl+D adds cursors without changing the primary selection
|
|
26
|
+
self.connect(self.editor.cursors.changed, self.highlight_occurrences)
|
|
25
27
|
|
|
26
28
|
def _set_colors(self, colors: GuiColors) -> None:
|
|
27
29
|
self._match_hl = gui_color(colors, "match_hl")
|
|
@@ -41,6 +43,12 @@ class HighlightMatchingSelection(Behavior):
|
|
|
41
43
|
fmt.setBackground(self._match_hl)
|
|
42
44
|
doc = self.editor.document()
|
|
43
45
|
search_cursor = QtGui.QTextCursor(doc)
|
|
46
|
+
# Qt or MultiCursorPaint already draws the cursors' own selections, and
|
|
47
|
+
# a highlight over them would hide which occurrences are selected
|
|
48
|
+
selected = {
|
|
49
|
+
(state.selectionStart(), state.selectionEnd())
|
|
50
|
+
for state in (cursor, *self.editor.cursors.secondary)
|
|
51
|
+
}
|
|
44
52
|
case_sensitive = QtGui.QTextDocument.FindFlag.FindCaseSensitively
|
|
45
53
|
|
|
46
54
|
while True:
|
|
@@ -48,9 +56,8 @@ class HighlightMatchingSelection(Behavior):
|
|
|
48
56
|
if search_cursor.isNull():
|
|
49
57
|
break
|
|
50
58
|
|
|
51
|
-
# Qt already highlights the selection itself
|
|
52
59
|
found = (search_cursor.selectionStart(), search_cursor.selectionEnd())
|
|
53
|
-
if found
|
|
60
|
+
if found not in selected:
|
|
54
61
|
selection = QtWidgets.QTextEdit.ExtraSelection()
|
|
55
62
|
selection.cursor = search_cursor
|
|
56
63
|
selection.format = fmt
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ..cursors.state import CursorState, primary_first
|
|
6
|
+
from ..tracked_document import len16
|
|
7
|
+
from ..utils import MM, add_menu_action, make_shortcut_action
|
|
8
|
+
from . import Behavior
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from Qt import QtCore, QtGui, QtWidgets
|
|
12
|
+
|
|
13
|
+
MOVE_UP = ("Move Lines Up", "Ctrl+Shift+Up")
|
|
14
|
+
MOVE_DOWN = ("Move Lines Down", "Ctrl+Shift+Down")
|
|
15
|
+
DUPLICATE = ("Duplicate", "Ctrl+Shift+D")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LineCommands(Behavior):
|
|
19
|
+
"""Move the lines under the cursors up or down, and duplicate the selection
|
|
20
|
+
or the current line, like PrEditor's QScintilla keys
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def _commands(self):
|
|
24
|
+
return (
|
|
25
|
+
(*MOVE_UP, lambda: self.move_lines(up=True)),
|
|
26
|
+
(*MOVE_DOWN, lambda: self.move_lines(up=False)),
|
|
27
|
+
(*DUPLICATE, self.duplicate),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def get_hotkeys(self) -> list[QtGui.QAction]:
|
|
31
|
+
return [
|
|
32
|
+
make_shortcut_action(self.editor, shortcut, slot, label)
|
|
33
|
+
for label, shortcut, slot in self._commands()
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def add_menu_actions(
|
|
37
|
+
self, menu: QtWidgets.QMenu, pos: QtCore.QPoint | None
|
|
38
|
+
) -> None:
|
|
39
|
+
for label, shortcut, slot in self._commands():
|
|
40
|
+
add_menu_action(menu, label, slot, shortcut)
|
|
41
|
+
|
|
42
|
+
def duplicate(self) -> None:
|
|
43
|
+
"""Copy each selection after itself and select the copy, or copy the
|
|
44
|
+
line of a cursor without a selection below it
|
|
45
|
+
"""
|
|
46
|
+
with self.editor.edit_session():
|
|
47
|
+
for cursor in self.editor.citer.iterate_cursors(complete_all=True):
|
|
48
|
+
if cursor.hasSelection():
|
|
49
|
+
text = cursor.selection().toPlainText()
|
|
50
|
+
end = cursor.selectionEnd()
|
|
51
|
+
cursor.setPosition(end)
|
|
52
|
+
cursor.insertText(text)
|
|
53
|
+
cursor.setPosition(end)
|
|
54
|
+
cursor.setPosition(end + len16(text), MM.KeepAnchor)
|
|
55
|
+
else:
|
|
56
|
+
block = cursor.block()
|
|
57
|
+
column = cursor.positionInBlock()
|
|
58
|
+
cursor.setPosition(block.position() + len16(block.text()))
|
|
59
|
+
cursor.insertText("\n" + block.text())
|
|
60
|
+
cursor.setPosition(block.next().position() + column)
|
|
61
|
+
|
|
62
|
+
def move_lines(self, up: bool) -> None:
|
|
63
|
+
"""Swap the lines under the cursors with the line above or below them
|
|
64
|
+
|
|
65
|
+
Nothing moves if any of the lines is already at the edge of the document
|
|
66
|
+
"""
|
|
67
|
+
doc = self.editor.document()
|
|
68
|
+
states, primary_index = self.editor.cursors.in_document_order()
|
|
69
|
+
|
|
70
|
+
# Group the cursors whose lines touch, as (first line, last line, states)
|
|
71
|
+
groups: list[tuple[int, int, list[CursorState]]] = []
|
|
72
|
+
for state in states:
|
|
73
|
+
first = doc.findBlock(state.selectionStart()).blockNumber()
|
|
74
|
+
end_block = doc.findBlock(state.selectionEnd())
|
|
75
|
+
last = end_block.blockNumber()
|
|
76
|
+
# A selection ending at the start of a line doesn't take that line
|
|
77
|
+
if last > first and state.selectionEnd() == end_block.position():
|
|
78
|
+
last -= 1
|
|
79
|
+
if groups and first <= groups[-1][1] + 1:
|
|
80
|
+
groups[-1] = (groups[-1][0], max(groups[-1][1], last), groups[-1][2])
|
|
81
|
+
groups[-1][2].append(state)
|
|
82
|
+
else:
|
|
83
|
+
groups.append((first, last, [state]))
|
|
84
|
+
|
|
85
|
+
if up and groups[0][0] == 0:
|
|
86
|
+
return
|
|
87
|
+
if not up and groups[-1][1] == doc.blockCount() - 1:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
moved: list[CursorState] = []
|
|
91
|
+
with self.editor.edit_session():
|
|
92
|
+
cursor = self.editor.textCursor()
|
|
93
|
+
cursor.beginEditBlock()
|
|
94
|
+
try:
|
|
95
|
+
for first, last, group in groups:
|
|
96
|
+
span_first, span_last = (
|
|
97
|
+
(first - 1, last) if up else (first, last + 1)
|
|
98
|
+
)
|
|
99
|
+
start_block = doc.findBlockByNumber(span_first)
|
|
100
|
+
end_block = doc.findBlockByNumber(span_last)
|
|
101
|
+
cursor.setPosition(start_block.position())
|
|
102
|
+
cursor.setPosition(
|
|
103
|
+
end_block.position() + len16(end_block.text()), MM.KeepAnchor
|
|
104
|
+
)
|
|
105
|
+
lines = cursor.selection().toPlainText().split("\n")
|
|
106
|
+
# The swapped line's length plus its newline is how far the others move
|
|
107
|
+
if up:
|
|
108
|
+
lines.append(lines.pop(0))
|
|
109
|
+
shift = -len16(lines[-1]) - 1
|
|
110
|
+
else:
|
|
111
|
+
lines.insert(0, lines.pop())
|
|
112
|
+
shift = len16(lines[0]) + 1
|
|
113
|
+
# The text keeps its length, so the later groups stay put
|
|
114
|
+
cursor.insertText("\n".join(lines))
|
|
115
|
+
moved += [
|
|
116
|
+
CursorState(s.anchor + shift, s.position + shift) for s in group
|
|
117
|
+
]
|
|
118
|
+
finally:
|
|
119
|
+
cursor.endEditBlock()
|
|
120
|
+
self.editor.cursors.replace_all(primary_first(moved, primary_index))
|
|
@@ -67,7 +67,7 @@ class MultiCursorPaint(Behavior):
|
|
|
67
67
|
states = [*self.editor.cursors.secondary, self.editor.cursors.primary()]
|
|
68
68
|
|
|
69
69
|
fmt = QtGui.QTextCharFormat()
|
|
70
|
-
fmt.setBackground(self.secondary_selection_color
|
|
70
|
+
fmt.setBackground(self.secondary_selection_color)
|
|
71
71
|
doc = self.editor.document()
|
|
72
72
|
selections = []
|
|
73
73
|
for state in states:
|
|
@@ -81,6 +81,12 @@ class TreeSitterHighlighter(QtGui.QSyntaxHighlighter):
|
|
|
81
81
|
block = self.currentBlock()
|
|
82
82
|
if tree is None or not block.isValid():
|
|
83
83
|
return
|
|
84
|
+
if self.tree_manager.reparse_pending:
|
|
85
|
+
# Qt highlights every block between the first and last edit of an
|
|
86
|
+
# edit block. Keep their formats, the reparse redoes the edited rows
|
|
87
|
+
for fmt_range in block.layout().formats():
|
|
88
|
+
self.setFormat(fmt_range.start, fmt_range.length, fmt_range.format)
|
|
89
|
+
return
|
|
84
90
|
|
|
85
91
|
# Qt positions are UTF-16 code units, but python's len counts code points
|
|
86
92
|
# so they differ when the text contains characters outside the BMP
|
|
@@ -126,13 +126,34 @@ class MultiCursor(QtCore.QObject):
|
|
|
126
126
|
search_text = cursor.selectedText()
|
|
127
127
|
all_cursors = self._primary_first()
|
|
128
128
|
next_pos = self._find(search_text, max(c.selectionEnd() for c in all_cursors))
|
|
129
|
+
length = len16(search_text)
|
|
129
130
|
if next_pos < 0:
|
|
130
|
-
# Wrap around to the start,
|
|
131
|
+
# Wrap around to the start, skipping occurrences that have a cursor
|
|
132
|
+
taken = {c.selectionStart() for c in all_cursors}
|
|
131
133
|
next_pos = self._find(search_text, 0)
|
|
132
|
-
|
|
134
|
+
while next_pos in taken:
|
|
135
|
+
next_pos = self._find(search_text, next_pos + length)
|
|
136
|
+
if next_pos < 0:
|
|
133
137
|
return False
|
|
134
|
-
end = next_pos +
|
|
135
|
-
|
|
138
|
+
end = next_pos + length
|
|
139
|
+
added = self._add([*all_cursors, CursorState(next_pos, end)])
|
|
140
|
+
if added:
|
|
141
|
+
self._scroll_to(end)
|
|
142
|
+
return added
|
|
143
|
+
|
|
144
|
+
def _scroll_to(self, position: int) -> None:
|
|
145
|
+
"""Scroll to show a position, leaving the primary cursor where it is"""
|
|
146
|
+
# Qt only scrolls to the primary cursor, so borrow it to find the scroll
|
|
147
|
+
editor = self.editor
|
|
148
|
+
primary = editor.textCursor()
|
|
149
|
+
bars = (editor.horizontalScrollBar(), editor.verticalScrollBar())
|
|
150
|
+
editor.setTextCursor(
|
|
151
|
+
CursorState(position, position).build_cursor(editor.document())
|
|
152
|
+
)
|
|
153
|
+
values = [bar.value() for bar in bars]
|
|
154
|
+
editor.setTextCursor(primary)
|
|
155
|
+
for bar, value in zip(bars, values):
|
|
156
|
+
bar.setValue(value)
|
|
136
157
|
|
|
137
158
|
def _find(self, search_text: str, start_pos: int) -> int:
|
|
138
159
|
"""Find the position of search_text after start_pos, or -1 if there is none
|
|
@@ -12,6 +12,7 @@ from .behaviors.code_folding import CodeFolding
|
|
|
12
12
|
from .behaviors.comment_toggle import CommentToggle
|
|
13
13
|
from .behaviors.highlight_matching_brackets import HighlightMatchingBrackets
|
|
14
14
|
from .behaviors.highlight_matching_selection import HighlightMatchingSelection
|
|
15
|
+
from .behaviors.line_commands import LineCommands
|
|
15
16
|
from .behaviors.line_numbers import LineNumbers
|
|
16
17
|
from .behaviors.multi_cursor_paint import MultiCursorPaint
|
|
17
18
|
from .behaviors.overscroll import Overscroll
|
|
@@ -38,6 +39,7 @@ DEFAULT_BEHAVIORS: tuple[type[Behavior], ...] = (
|
|
|
38
39
|
MultiCursorPaint,
|
|
39
40
|
CodeFolding,
|
|
40
41
|
CommentToggle,
|
|
42
|
+
LineCommands,
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
# The built-in behaviors that are off by default
|
|
@@ -31,7 +31,7 @@ GUI_COLOR_DEFAULTS: GuiColors = {
|
|
|
31
31
|
"primary_cursor": "#FFFFFFFF",
|
|
32
32
|
"secondary_cursor": "#B4B4B4",
|
|
33
33
|
"selection": "#094771",
|
|
34
|
-
"secondary_selection": "#
|
|
34
|
+
"secondary_selection": "#264F78",
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
# Gui color name -> its default, made from the other colors
|
|
@@ -36,6 +36,7 @@ DARK_THEME: Theme = {
|
|
|
36
36
|
"red": "#FF5555",
|
|
37
37
|
"orange": "#FFB86C",
|
|
38
38
|
"yellow": "#F1FA8C",
|
|
39
|
+
"faint_yellow": "#40F1FA8C",
|
|
39
40
|
"green": "#50fa7b",
|
|
40
41
|
"purple": "#BD93F9",
|
|
41
42
|
"cyan": "#8BE9FD",
|
|
@@ -72,14 +73,14 @@ DARK_THEME: Theme = {
|
|
|
72
73
|
"gutter_fg": "xlight_slate",
|
|
73
74
|
"hl_dim": "mid_light_slate",
|
|
74
75
|
"manual_fold": "orange",
|
|
75
|
-
"match_hl": "
|
|
76
|
+
"match_hl": "faint_yellow",
|
|
76
77
|
"menu": "dark_slate",
|
|
77
78
|
"outside_bg": "dark_slate",
|
|
78
79
|
"pair_hl": "mid_gray",
|
|
79
80
|
"primary_cursor": "bright_white",
|
|
80
81
|
"secondary_cursor": "half_mid_gray",
|
|
81
82
|
"selection": "mid_light_slate",
|
|
82
|
-
"secondary_selection": "
|
|
83
|
+
"secondary_selection": "light_slate",
|
|
83
84
|
},
|
|
84
85
|
|
|
85
86
|
"syntax" : {
|
|
@@ -173,14 +174,14 @@ LIGHT_THEME: Theme = {
|
|
|
173
174
|
"gutter_fg": "fg",
|
|
174
175
|
"hl_dim": "filled_blue",
|
|
175
176
|
"manual_fold": "yellow",
|
|
176
|
-
"match_hl": "
|
|
177
|
+
"match_hl": "bg_green",
|
|
177
178
|
"menu": "bg4",
|
|
178
179
|
"outside_bg": "bg_grey",
|
|
179
180
|
"pair_hl": "bg_yellow",
|
|
180
181
|
"primary_cursor": "true_black",
|
|
181
182
|
"secondary_cursor": "true_black",
|
|
182
183
|
"selection": "filled_blue",
|
|
183
|
-
"secondary_selection": "
|
|
184
|
+
"secondary_selection": "filled_blue",
|
|
184
185
|
},
|
|
185
186
|
|
|
186
187
|
"syntax" : {
|
|
@@ -26,8 +26,8 @@ class TreeManager(QtCore.QObject):
|
|
|
26
26
|
# Whether an edit was made while paused, so unpausing has to reparse
|
|
27
27
|
self._reparse_pending = False
|
|
28
28
|
|
|
29
|
-
# The (first, last) rows
|
|
30
|
-
self._edit_rows: tuple[int, int]
|
|
29
|
+
# The (first, last) rows of each edit since the last parse
|
|
30
|
+
self._edit_rows: list[tuple[int, int]] = []
|
|
31
31
|
# The (first, last) row ranges that changed in the last parse
|
|
32
32
|
# None means the whole document changed
|
|
33
33
|
self.changed_rows: list[tuple[int, int]] | None = None
|
|
@@ -42,6 +42,11 @@ class TreeManager(QtCore.QObject):
|
|
|
42
42
|
def is_paused(self) -> bool:
|
|
43
43
|
return self._pause_depth > 0
|
|
44
44
|
|
|
45
|
+
@property
|
|
46
|
+
def reparse_pending(self) -> bool:
|
|
47
|
+
"""Whether edits are held by a pause, and the tree is out of date"""
|
|
48
|
+
return self._pause_depth > 0 and self._reparse_pending
|
|
49
|
+
|
|
45
50
|
def set_language(self, language: Language | None) -> None:
|
|
46
51
|
"""Change the language, and parse the document again"""
|
|
47
52
|
self._set_parser(language)
|
|
@@ -51,7 +56,7 @@ class TreeManager(QtCore.QObject):
|
|
|
51
56
|
def _set_parser(self, language: Language | None) -> None:
|
|
52
57
|
self.parser = Parser(language) if language is not None else None
|
|
53
58
|
self.tree = None
|
|
54
|
-
self._edit_rows =
|
|
59
|
+
self._edit_rows = []
|
|
55
60
|
self.changed_rows = None
|
|
56
61
|
|
|
57
62
|
def treesitter_source_callback(self, _byte_offset: int, ts_point: Point) -> bytes:
|
|
@@ -73,7 +78,7 @@ class TreeManager(QtCore.QObject):
|
|
|
73
78
|
def parse_from_scratch(self) -> None:
|
|
74
79
|
"""Parse the whole document from scratch. Without a language there is no tree"""
|
|
75
80
|
self.tree = self._parse(self.parser) if self.parser is not None else None
|
|
76
|
-
self._edit_rows =
|
|
81
|
+
self._edit_rows = []
|
|
77
82
|
self.changed_rows = None
|
|
78
83
|
self.reparsed.emit()
|
|
79
84
|
|
|
@@ -97,20 +102,22 @@ class TreeManager(QtCore.QObject):
|
|
|
97
102
|
def _track_edit_rows(
|
|
98
103
|
self, start_row: int, old_end_row: int, new_end_row: int
|
|
99
104
|
) -> None:
|
|
100
|
-
"""
|
|
105
|
+
"""Add this edit's rows to the edited spans
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
Spans are kept separate, so edits far apart (ie: multi-cursor) don't
|
|
108
|
+
mark every row between them. They're kept in the current document's
|
|
109
|
+
rows, so earlier spans shift by the lines this edit added or removed
|
|
104
110
|
"""
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
self._edit_rows = (
|
|
111
|
+
delta = new_end_row - old_end_row
|
|
112
|
+
|
|
113
|
+
def shift(row: int) -> int:
|
|
114
|
+
if row > old_end_row:
|
|
115
|
+
return row + delta
|
|
116
|
+
# A row inside a removed range collapses to the edit's end
|
|
117
|
+
return min(row, new_end_row) if row > start_row else row
|
|
118
|
+
|
|
119
|
+
self._edit_rows = [(shift(lo), shift(hi)) for lo, hi in self._edit_rows]
|
|
120
|
+
self._edit_rows.append((start_row, new_end_row))
|
|
114
121
|
|
|
115
122
|
def apply_edit(
|
|
116
123
|
self,
|
|
@@ -145,7 +152,7 @@ class TreeManager(QtCore.QObject):
|
|
|
145
152
|
return
|
|
146
153
|
|
|
147
154
|
if self.parser is None:
|
|
148
|
-
self._edit_rows =
|
|
155
|
+
self._edit_rows = []
|
|
149
156
|
return
|
|
150
157
|
|
|
151
158
|
old_tree = self.tree
|
|
@@ -159,10 +166,9 @@ class TreeManager(QtCore.QObject):
|
|
|
159
166
|
(r.start_point.row, r.end_point.row)
|
|
160
167
|
for r in old_tree.changed_ranges(self.tree)
|
|
161
168
|
]
|
|
162
|
-
|
|
163
|
-
rows.append(self._edit_rows)
|
|
169
|
+
rows.extend(self._edit_rows)
|
|
164
170
|
self.changed_rows = rows
|
|
165
|
-
self._edit_rows =
|
|
171
|
+
self._edit_rows = []
|
|
166
172
|
self.reparsed.emit()
|
|
167
173
|
|
|
168
174
|
def node_at_byte(self, byte_offset: int) -> Node | None:
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
.gitignore
|
|
1
2
|
LICENSE.txt
|
|
2
3
|
README.md
|
|
4
|
+
build_venv_and_kickoff.sh
|
|
3
5
|
pyproject.toml
|
|
6
|
+
tox.ini
|
|
7
|
+
.github/workflows/publish.yml
|
|
8
|
+
examples/demo.py
|
|
4
9
|
src/QCodeSitter/__init__.py
|
|
5
10
|
src/QCodeSitter/code_editor.py
|
|
6
11
|
src/QCodeSitter/defaults.py
|
|
@@ -19,6 +24,8 @@ src/QCodeSitter.egg-info/SOURCES.txt
|
|
|
19
24
|
src/QCodeSitter.egg-info/dependency_links.txt
|
|
20
25
|
src/QCodeSitter.egg-info/entry_points.txt
|
|
21
26
|
src/QCodeSitter.egg-info/requires.txt
|
|
27
|
+
src/QCodeSitter.egg-info/scm_file_list.json
|
|
28
|
+
src/QCodeSitter.egg-info/scm_version.json
|
|
22
29
|
src/QCodeSitter.egg-info/top_level.txt
|
|
23
30
|
src/QCodeSitter/behaviors/__init__.py
|
|
24
31
|
src/QCodeSitter/behaviors/auto_bracket.py
|
|
@@ -26,6 +33,7 @@ src/QCodeSitter/behaviors/code_folding.py
|
|
|
26
33
|
src/QCodeSitter/behaviors/comment_toggle.py
|
|
27
34
|
src/QCodeSitter/behaviors/highlight_matching_brackets.py
|
|
28
35
|
src/QCodeSitter/behaviors/highlight_matching_selection.py
|
|
36
|
+
src/QCodeSitter/behaviors/line_commands.py
|
|
29
37
|
src/QCodeSitter/behaviors/line_numbers.py
|
|
30
38
|
src/QCodeSitter/behaviors/multi_cursor_paint.py
|
|
31
39
|
src/QCodeSitter/behaviors/overscroll.py
|
|
@@ -49,4 +57,35 @@ src/QCodeSitter/languages/base.py
|
|
|
49
57
|
src/QCodeSitter/languages/python.py
|
|
50
58
|
src/QCodeSitter/languages/python_highlights.py
|
|
51
59
|
src/QCodeSitter/queries/__init__.py
|
|
52
|
-
src/QCodeSitter/queries/predicates.py
|
|
60
|
+
src/QCodeSitter/queries/predicates.py
|
|
61
|
+
tests/__init__.py
|
|
62
|
+
tests/conftest.py
|
|
63
|
+
tests/helpers.py
|
|
64
|
+
tests/behaviors/__init__.py
|
|
65
|
+
tests/behaviors/test_behaviors.py
|
|
66
|
+
tests/behaviors/test_code_folding.py
|
|
67
|
+
tests/behaviors/test_comment_toggle.py
|
|
68
|
+
tests/behaviors/test_gutters.py
|
|
69
|
+
tests/behaviors/test_highlight_matching.py
|
|
70
|
+
tests/behaviors/test_tab_completion.py
|
|
71
|
+
tests/editor/__init__.py
|
|
72
|
+
tests/editor/test_clipboard.py
|
|
73
|
+
tests/editor/test_cursors.py
|
|
74
|
+
tests/editor/test_editing_keys.py
|
|
75
|
+
tests/editor/test_editor_core.py
|
|
76
|
+
tests/editor/test_line_edits.py
|
|
77
|
+
tests/editor/test_multi_cursor.py
|
|
78
|
+
tests/editor/test_options.py
|
|
79
|
+
tests/editor/test_shortcuts.py
|
|
80
|
+
tests/editor/test_utils.py
|
|
81
|
+
tests/fuzz/__init__.py
|
|
82
|
+
tests/fuzz/test_fuzz.py
|
|
83
|
+
tests/integration/__init__.py
|
|
84
|
+
tests/integration/test_demo.py
|
|
85
|
+
tests/integration/test_preditor_interface.py
|
|
86
|
+
tests/syntax/__init__.py
|
|
87
|
+
tests/syntax/test_incremental.py
|
|
88
|
+
tests/syntax/test_languages.py
|
|
89
|
+
tests/syntax/test_queries.py
|
|
90
|
+
tests/syntax/test_syntax.py
|
|
91
|
+
tests/syntax/test_themes.py
|