QCodeSitter 0.1.0__py3-none-any.whl
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/__init__.py +38 -0
- QCodeSitter/behaviors/__init__.py +115 -0
- QCodeSitter/behaviors/auto_bracket.py +141 -0
- QCodeSitter/behaviors/code_folding.py +530 -0
- QCodeSitter/behaviors/comment_toggle.py +71 -0
- QCodeSitter/behaviors/highlight_matching_brackets.py +181 -0
- QCodeSitter/behaviors/highlight_matching_selection.py +59 -0
- QCodeSitter/behaviors/line_numbers.py +49 -0
- QCodeSitter/behaviors/multi_cursor_paint.py +131 -0
- QCodeSitter/behaviors/overscroll.py +104 -0
- QCodeSitter/behaviors/smart_indent.py +191 -0
- QCodeSitter/behaviors/syntax_highlighting.py +181 -0
- QCodeSitter/behaviors/tab_completion/__init__.py +257 -0
- QCodeSitter/behaviors/tab_completion/popup.py +123 -0
- QCodeSitter/behaviors/tab_completion/providers.py +66 -0
- QCodeSitter/behaviors/tab_completion/types.py +55 -0
- QCodeSitter/code_editor.py +540 -0
- QCodeSitter/cursors/__init__.py +8 -0
- QCodeSitter/cursors/clipboard.py +51 -0
- QCodeSitter/cursors/iterator.py +141 -0
- QCodeSitter/cursors/line_edits.py +128 -0
- QCodeSitter/cursors/multi.py +240 -0
- QCodeSitter/cursors/state.py +55 -0
- QCodeSitter/cursors/tracked.py +97 -0
- QCodeSitter/defaults.py +93 -0
- QCodeSitter/editing_keys.py +150 -0
- QCodeSitter/editor_options.py +246 -0
- QCodeSitter/gui_colors.py +51 -0
- QCodeSitter/gutters.py +141 -0
- QCodeSitter/integrations/__init__.py +1 -0
- QCodeSitter/integrations/preditor_interface.py +390 -0
- QCodeSitter/languages/__init__.py +99 -0
- QCodeSitter/languages/base.py +60 -0
- QCodeSitter/languages/python.py +146 -0
- QCodeSitter/languages/python_highlights.py +444 -0
- QCodeSitter/py.typed +0 -0
- QCodeSitter/queries/__init__.py +73 -0
- QCodeSitter/queries/predicates.py +172 -0
- QCodeSitter/syntax_specs.py +65 -0
- QCodeSitter/themes.py +271 -0
- QCodeSitter/tracked_document.py +107 -0
- QCodeSitter/tree_manager.py +180 -0
- QCodeSitter/utils.py +84 -0
- qcodesitter-0.1.0.dist-info/METADATA +255 -0
- qcodesitter-0.1.0.dist-info/RECORD +49 -0
- qcodesitter-0.1.0.dist-info/WHEEL +5 -0
- qcodesitter-0.1.0.dist-info/entry_points.txt +2 -0
- qcodesitter-0.1.0.dist-info/licenses/LICENSE.txt +7 -0
- qcodesitter-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Text predicates for tree-sitter queries
|
|
2
|
+
|
|
3
|
+
Tree-sitter's built-in text predicates (#eq?, #match?, #any-of?, etc) compare the
|
|
4
|
+
node's source text against the strings in the query. Our trees are parsed as UTF-16,
|
|
5
|
+
so tree-sitter compares UTF-16 bytes against UTF-8 strings, and they never match.
|
|
6
|
+
|
|
7
|
+
To get around this, `prepare_query_source` renames the text predicates so tree-sitter
|
|
8
|
+
treats them as custom predicates, and `QueryPredicates` evaluates them against the
|
|
9
|
+
node text it is given, which TrackedDocument.node_text reads from the document.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import functools
|
|
15
|
+
import re
|
|
16
|
+
import warnings
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from tree_sitter import Node
|
|
22
|
+
|
|
23
|
+
from . import Captures, PredicateArgs
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PREFIX = "qcs-"
|
|
27
|
+
_TEXT_PREDICATE_RE = re.compile(r"#((?:not-)?(?:eq|match|lua-match|any-of))\?")
|
|
28
|
+
|
|
29
|
+
# Lua pattern character classes (nvim queries use these) as regex set contents
|
|
30
|
+
_LUA_CLASSES = {
|
|
31
|
+
"a": r"A-Za-z",
|
|
32
|
+
"c": r"\x00-\x1f\x7f",
|
|
33
|
+
"d": r"0-9",
|
|
34
|
+
"l": r"a-z",
|
|
35
|
+
"p": r"!-/:-@\[-`{-~",
|
|
36
|
+
"s": r"\s",
|
|
37
|
+
"u": r"A-Z",
|
|
38
|
+
"w": r"A-Za-z0-9",
|
|
39
|
+
"x": r"A-Fa-f0-9",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def prepare_query_source(source: str) -> str:
|
|
44
|
+
"""Rename the text predicates in a query so we can evaluate them ourselves"""
|
|
45
|
+
return _TEXT_PREDICATE_RE.sub(lambda m: f"#{PREFIX}{m.group(1)}?", source)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _lua_class(nxt: str, in_set: bool) -> str:
|
|
49
|
+
"""Convert the character after a % into regex"""
|
|
50
|
+
cls = _LUA_CLASSES.get(nxt.lower())
|
|
51
|
+
if cls is None:
|
|
52
|
+
# %% or %. etc are escaped literals
|
|
53
|
+
return re.escape(nxt)
|
|
54
|
+
if not nxt.isupper():
|
|
55
|
+
return cls if in_set else f"[{cls}]"
|
|
56
|
+
# Uppercase classes are the complement
|
|
57
|
+
if in_set:
|
|
58
|
+
raise re.error(f"%{nxt} is not supported inside a set")
|
|
59
|
+
return f"[^{cls}]"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def lua_to_regex(pattern: str) -> str:
|
|
63
|
+
"""Convert the lua pattern character classes (ie %u, %l) into a python regex
|
|
64
|
+
|
|
65
|
+
Only handles the character classes, which is what the nvim queries use.
|
|
66
|
+
Normal regex syntax passes through unchanged. The complement classes (ie %U)
|
|
67
|
+
aren't supported inside of a [set]
|
|
68
|
+
"""
|
|
69
|
+
out = []
|
|
70
|
+
in_set = False
|
|
71
|
+
i = 0
|
|
72
|
+
while i < len(pattern):
|
|
73
|
+
ch = pattern[i]
|
|
74
|
+
if ch in "%\\" and i + 1 < len(pattern):
|
|
75
|
+
pair = pattern[i : i + 2]
|
|
76
|
+
out.append(_lua_class(pair[1], in_set) if ch == "%" else pair)
|
|
77
|
+
i += 2
|
|
78
|
+
continue
|
|
79
|
+
if ch == "[" and not in_set:
|
|
80
|
+
in_set = True
|
|
81
|
+
elif ch == "]" and in_set:
|
|
82
|
+
in_set = False
|
|
83
|
+
out.append(ch)
|
|
84
|
+
i += 1
|
|
85
|
+
return "".join(out)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@functools.lru_cache(maxsize=256)
|
|
89
|
+
def _compile_pattern(pattern: str) -> re.Pattern | None:
|
|
90
|
+
"""Compile a #match? pattern once, or warn once and give None if it's invalid"""
|
|
91
|
+
try:
|
|
92
|
+
return re.compile(lua_to_regex(pattern))
|
|
93
|
+
except re.error as err:
|
|
94
|
+
warnings.warn(
|
|
95
|
+
f"Invalid #match? pattern {pattern!r} in a tree-sitter query: {err}",
|
|
96
|
+
stacklevel=2,
|
|
97
|
+
)
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class QueryPredicates:
|
|
102
|
+
"""A predicate callback for QueryCursor.captures that evaluates the renamed
|
|
103
|
+
text predicates using the text from a document
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
get_text: A function that returns the source text of a node
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, get_text: Callable[[Node], str]) -> None:
|
|
110
|
+
self.get_text = get_text
|
|
111
|
+
|
|
112
|
+
def _eq_check(
|
|
113
|
+
self, rest: PredicateArgs, captures: Captures
|
|
114
|
+
) -> Callable[[str], bool] | None:
|
|
115
|
+
value, kind = rest[0]
|
|
116
|
+
if kind != "capture":
|
|
117
|
+
return lambda text: text == value
|
|
118
|
+
others = captures.get(value, [])
|
|
119
|
+
if not others:
|
|
120
|
+
return None
|
|
121
|
+
other = self.get_text(others[0])
|
|
122
|
+
return lambda text: text == other
|
|
123
|
+
|
|
124
|
+
def _match_check(self, rest: PredicateArgs) -> Callable[[str], bool]:
|
|
125
|
+
regex = _compile_pattern(rest[0][0])
|
|
126
|
+
if regex is None:
|
|
127
|
+
# An invalid pattern can't match anything
|
|
128
|
+
return lambda _text: False
|
|
129
|
+
return lambda text: regex.search(text) is not None
|
|
130
|
+
|
|
131
|
+
def _make_check(
|
|
132
|
+
self, name: str, rest: PredicateArgs, captures: Captures
|
|
133
|
+
) -> Callable[[str], bool] | None:
|
|
134
|
+
"""Build the text test for a predicate, or None if it can't filter"""
|
|
135
|
+
if name == "any-of":
|
|
136
|
+
values = {value for value, _kind in rest}
|
|
137
|
+
return lambda text: text in values
|
|
138
|
+
if not rest:
|
|
139
|
+
return None
|
|
140
|
+
if name == "eq":
|
|
141
|
+
return self._eq_check(rest, captures)
|
|
142
|
+
# The nvim queries use lua classes in #match? too, so treat them the same
|
|
143
|
+
if name in ("match", "lua-match"):
|
|
144
|
+
return self._match_check(rest)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def __call__(
|
|
148
|
+
self,
|
|
149
|
+
predicate: str,
|
|
150
|
+
args: PredicateArgs,
|
|
151
|
+
pattern_index: int,
|
|
152
|
+
captures: Captures,
|
|
153
|
+
) -> bool:
|
|
154
|
+
# The names match tree-sitter's QueryPredicate protocol
|
|
155
|
+
name = predicate
|
|
156
|
+
if not name.startswith(PREFIX):
|
|
157
|
+
# Not one of ours. Don't filter anything out
|
|
158
|
+
return True
|
|
159
|
+
name = name[len(PREFIX) : -1]
|
|
160
|
+
negate = name.startswith("not-")
|
|
161
|
+
if negate:
|
|
162
|
+
name = name[4:]
|
|
163
|
+
|
|
164
|
+
if not args or args[0][1] != "capture":
|
|
165
|
+
return True
|
|
166
|
+
check = self._make_check(name, args[1:], captures)
|
|
167
|
+
if check is None:
|
|
168
|
+
return True
|
|
169
|
+
return all(
|
|
170
|
+
check(self.get_text(node)) != negate
|
|
171
|
+
for node in captures.get(args[0][0], [])
|
|
172
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""The "syntax" option: text formats keyed by tree-sitter capture name, and how
|
|
2
|
+
dotted capture names fall back to their parents
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import TypedDict, TypeVar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SyntaxSpec(TypedDict, total=False):
|
|
12
|
+
color: str
|
|
13
|
+
italic: bool
|
|
14
|
+
bold: bool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Tree-sitter capture name -> format
|
|
18
|
+
SyntaxSpecs = dict[str, SyntaxSpec]
|
|
19
|
+
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_capture(name: str, specs: Mapping[str, T]) -> T | None:
|
|
24
|
+
"""Look up a treesitter capture name, falling back to its parent groups
|
|
25
|
+
|
|
26
|
+
For example "function.method.call" will check "function.method.call", then
|
|
27
|
+
"function.method", then "function". This mirrors how nvim resolves highlight groups
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
name: The dotted treesitter capture name
|
|
31
|
+
specs: A dictionary keyed by capture names
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The value of the most specific matching key, or None if nothing matches
|
|
35
|
+
"""
|
|
36
|
+
parts = name.split(".")
|
|
37
|
+
while parts:
|
|
38
|
+
val = specs.get(".".join(parts))
|
|
39
|
+
if val is not None:
|
|
40
|
+
return val
|
|
41
|
+
parts.pop()
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def overlay_syntax(base: SyntaxSpecs, overrides: SyntaxSpecs) -> SyntaxSpecs:
|
|
46
|
+
"""Overlay syntax specs onto a base set of syntax specs
|
|
47
|
+
|
|
48
|
+
Overrides are resolved hierarchically, so an override for "keyword" will replace
|
|
49
|
+
the base "keyword.return" unless there is also an override for "keyword.return".
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
base: A syntax spec dictionary as returned by read_theme
|
|
53
|
+
overrides: A partial syntax spec dictionary
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A new syntax spec dictionary
|
|
57
|
+
"""
|
|
58
|
+
names = set(base) | set(overrides)
|
|
59
|
+
out: SyntaxSpecs = {}
|
|
60
|
+
for name in names:
|
|
61
|
+
spec = resolve_capture(name, overrides)
|
|
62
|
+
if spec is None:
|
|
63
|
+
spec = base[name]
|
|
64
|
+
out[name] = spec.copy()
|
|
65
|
+
return out
|
QCodeSitter/themes.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Built-in color themes, and helpers to turn a theme into editor options
|
|
2
|
+
|
|
3
|
+
A theme has an optional "palette" of named colors, "gui" colors for the editor
|
|
4
|
+
and its behaviors (see gui_colors.py), and "syntax" specs keyed by tree-sitter
|
|
5
|
+
capture name (see syntax_specs.py for their format). Gui and syntax colors can
|
|
6
|
+
be hex colors or palette names.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import TypedDict
|
|
14
|
+
|
|
15
|
+
from Qt import QtGui
|
|
16
|
+
|
|
17
|
+
from .gui_colors import GuiColors
|
|
18
|
+
from .syntax_specs import SyntaxSpecs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _RequiredThemeSections(TypedDict):
|
|
22
|
+
gui: dict[str, str]
|
|
23
|
+
syntax: SyntaxSpecs
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Theme(_RequiredThemeSections, total=False):
|
|
27
|
+
palette: dict[str, str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# fmt: off
|
|
31
|
+
|
|
32
|
+
# This colorscheme is pulled from the nvim Dracula theme
|
|
33
|
+
# https://github.com/Mofiqul/dracula.nvim
|
|
34
|
+
DARK_THEME: Theme = {
|
|
35
|
+
"palette": {
|
|
36
|
+
"red": "#FF5555",
|
|
37
|
+
"orange": "#FFB86C",
|
|
38
|
+
"yellow": "#F1FA8C",
|
|
39
|
+
"green": "#50fa7b",
|
|
40
|
+
"purple": "#BD93F9",
|
|
41
|
+
"cyan": "#8BE9FD",
|
|
42
|
+
"pink": "#FF79C6",
|
|
43
|
+
"bright_red": "#FF6E6E",
|
|
44
|
+
"bright_green": "#69FF94",
|
|
45
|
+
"bright_yellow": "#FFFFA5",
|
|
46
|
+
"bright_blue": "#D6ACFF",
|
|
47
|
+
"bright_magenta": "#FF92DF",
|
|
48
|
+
"bright_cyan": "#A4FFFF",
|
|
49
|
+
"bright_white": "#FFFFFF",
|
|
50
|
+
"white": "#ABB2BF",
|
|
51
|
+
"half_mid_gray": "#B4B4B4",
|
|
52
|
+
"mid_gray": "#C8C8C8",
|
|
53
|
+
"gray": "#787878",
|
|
54
|
+
"dark_gray": "#101010",
|
|
55
|
+
"black": "#191A21",
|
|
56
|
+
"cream": "#F8F8F2",
|
|
57
|
+
"xlight_slate": "#8E9CBC",
|
|
58
|
+
"light_slate": "#6272A4",
|
|
59
|
+
"mid_light_slate": "#4C4C5C",
|
|
60
|
+
"slate": "#3E4452",
|
|
61
|
+
"slate2": "#3C3C4C",
|
|
62
|
+
"mid_dark_slate": "#282A36",
|
|
63
|
+
"dark_slate": "#21222C",
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
"gui" : {
|
|
67
|
+
"bg": "mid_dark_slate",
|
|
68
|
+
"bg_dim": "slate2",
|
|
69
|
+
"fg": "cream",
|
|
70
|
+
"fg_dim": "mid_gray",
|
|
71
|
+
"gutter": "dark_gray",
|
|
72
|
+
"gutter_fg": "xlight_slate",
|
|
73
|
+
"hl_dim": "mid_light_slate",
|
|
74
|
+
"manual_fold": "orange",
|
|
75
|
+
"match_hl": "bright_yellow",
|
|
76
|
+
"menu": "dark_slate",
|
|
77
|
+
"outside_bg": "dark_slate",
|
|
78
|
+
"pair_hl": "mid_gray",
|
|
79
|
+
"primary_cursor": "bright_white",
|
|
80
|
+
"secondary_cursor": "half_mid_gray",
|
|
81
|
+
"selection": "mid_light_slate",
|
|
82
|
+
"secondary_selection": "half_mid_gray",
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
"syntax" : {
|
|
86
|
+
"attribute": {"color": "cyan"},
|
|
87
|
+
"attribute.builtin": {"color": "cyan"},
|
|
88
|
+
"boolean": {"color": "purple"},
|
|
89
|
+
"character.special": {"color": "green"},
|
|
90
|
+
"comment": {"color": "light_slate"},
|
|
91
|
+
"constructor": {"color": "cyan"},
|
|
92
|
+
"function": {"color": "green"},
|
|
93
|
+
"function.builtin": {"color": "cyan"},
|
|
94
|
+
"function.call": {"color": "green"},
|
|
95
|
+
"function.macro": {"color": "green"},
|
|
96
|
+
"function.method": {"color": "green"},
|
|
97
|
+
"function.method.call": {"color": "green"},
|
|
98
|
+
"keyword": {"color": "pink"},
|
|
99
|
+
"keyword.conditional": {"color": "pink"},
|
|
100
|
+
"keyword.coroutine": {"color": "pink"},
|
|
101
|
+
"keyword.directive": {"color": "pink"},
|
|
102
|
+
"keyword.exception": {"color": "purple"},
|
|
103
|
+
"keyword.function": {"color": "cyan"},
|
|
104
|
+
"keyword.import": {"color": "pink"},
|
|
105
|
+
"keyword.operator": {"color": "pink"},
|
|
106
|
+
"keyword.repeat": {"color": "pink"},
|
|
107
|
+
"keyword.return": {"color": "pink"},
|
|
108
|
+
"keyword.type": {"color": "pink"},
|
|
109
|
+
"module": {"color": "orange"},
|
|
110
|
+
"module.builtin": {"color": "orange"},
|
|
111
|
+
"none": {"color": "purple"},
|
|
112
|
+
"number": {"color": "purple"},
|
|
113
|
+
"number.float": {"color": "green"},
|
|
114
|
+
"operator": {"color": "pink"},
|
|
115
|
+
"punctuation.bracket": {"color": "cream"},
|
|
116
|
+
"punctuation.delimiter": {"color": "cream"},
|
|
117
|
+
"string": {"color": "yellow"},
|
|
118
|
+
"string.documentation": {"color": "cyan"},
|
|
119
|
+
"string.escape": {"color": "cyan"},
|
|
120
|
+
"string.regexp": {"color": "red"},
|
|
121
|
+
"type": {"color": "bright_cyan"},
|
|
122
|
+
"type.builtin": {"color": "cyan", "italic": True},
|
|
123
|
+
"type.definition": {"color": "bright_cyan"},
|
|
124
|
+
"variable": {"color": "cream"},
|
|
125
|
+
"variable.builtin": {"color": "purple"},
|
|
126
|
+
"variable.member": {"color": "orange"},
|
|
127
|
+
"variable.parameter": {"color": "orange"},
|
|
128
|
+
"constant": {"color": "purple"},
|
|
129
|
+
"constant.builtin": {"color": "purple"},
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# This colorscheme is pulled from the nvim Edge theme
|
|
134
|
+
# https://github.com/sainnhe/edge
|
|
135
|
+
LIGHT_THEME: Theme = {
|
|
136
|
+
"palette" : {
|
|
137
|
+
'true_black': '#000000',
|
|
138
|
+
'true_white': '#FFFFFF',
|
|
139
|
+
'black': '#DDE2E7',
|
|
140
|
+
'bg_dim': '#E8EBF0',
|
|
141
|
+
'bg0': '#FAFAFA',
|
|
142
|
+
'bg1': '#EEF1F4',
|
|
143
|
+
'bg2': '#E8EBF0',
|
|
144
|
+
'bg3': '#E8EBF0',
|
|
145
|
+
'bg4': '#DDE2E7',
|
|
146
|
+
'bg_grey': '#BCC5CF',
|
|
147
|
+
'bg_red': '#F6E4E4',
|
|
148
|
+
'bg_yellow': '#F0ECE2',
|
|
149
|
+
'bg_green': '#E5EEE4',
|
|
150
|
+
'bg_blue': '#E3EAF6',
|
|
151
|
+
'bg_purple': '#F4E9F8',
|
|
152
|
+
'filled_red': '#E17373',
|
|
153
|
+
'filled_green': '#76AF6F',
|
|
154
|
+
'filled_blue': '#6996E0',
|
|
155
|
+
'filled_purple': '#BF75D6',
|
|
156
|
+
'fg': '#4B505B',
|
|
157
|
+
'red': '#D05858',
|
|
158
|
+
'yellow': '#BE7E05',
|
|
159
|
+
'green': '#608E32',
|
|
160
|
+
'cyan': '#3A8B84',
|
|
161
|
+
'blue': '#5079BE',
|
|
162
|
+
'purple': '#B05CCC',
|
|
163
|
+
'grey': '#8790A0',
|
|
164
|
+
'grey_dim': '#BAC3CB',
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
"gui" : {
|
|
168
|
+
"bg": "true_white",
|
|
169
|
+
"bg_dim": "bg_dim",
|
|
170
|
+
"fg": "fg",
|
|
171
|
+
"fg_dim": "grey",
|
|
172
|
+
"gutter": "bg2",
|
|
173
|
+
"gutter_fg": "fg",
|
|
174
|
+
"hl_dim": "filled_blue",
|
|
175
|
+
"manual_fold": "yellow",
|
|
176
|
+
"match_hl": "filled_green",
|
|
177
|
+
"menu": "bg4",
|
|
178
|
+
"outside_bg": "bg_grey",
|
|
179
|
+
"pair_hl": "bg_yellow",
|
|
180
|
+
"primary_cursor": "true_black",
|
|
181
|
+
"secondary_cursor": "true_black",
|
|
182
|
+
"selection": "filled_blue",
|
|
183
|
+
"secondary_selection": "blue",
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
"syntax" : {
|
|
187
|
+
"attribute": {"color": "yellow"},
|
|
188
|
+
"attribute.builtin": {"color": "blue"},
|
|
189
|
+
"boolean": {"color": "green"},
|
|
190
|
+
"character.special": {"color": "yellow"},
|
|
191
|
+
"comment": {"color": "grey"},
|
|
192
|
+
"constructor": {"color": "blue"},
|
|
193
|
+
"function": {"color": "blue"},
|
|
194
|
+
"function.builtin": {"color": "blue"},
|
|
195
|
+
"function.call": {"color": "blue"},
|
|
196
|
+
"function.macro": {"color": "blue"},
|
|
197
|
+
"function.method": {"color": "blue"},
|
|
198
|
+
"function.method.call": {"color": "blue"},
|
|
199
|
+
"keyword": {"color": "purple"},
|
|
200
|
+
"keyword.conditional": {"color": "purple"},
|
|
201
|
+
"keyword.coroutine": {"color": "purple"},
|
|
202
|
+
"keyword.directive": {"color": "purple"},
|
|
203
|
+
"keyword.exception": {"color": "purple"},
|
|
204
|
+
"keyword.function": {"color": "purple"},
|
|
205
|
+
"keyword.import": {"color": "purple"},
|
|
206
|
+
"keyword.operator": {"color": "purple"},
|
|
207
|
+
"keyword.repeat": {"color": "purple"},
|
|
208
|
+
"keyword.return": {"color": "purple"},
|
|
209
|
+
"keyword.type": {"color": "yellow"},
|
|
210
|
+
"module": {"color": "yellow"},
|
|
211
|
+
"module.builtin": {"color": "yellow"},
|
|
212
|
+
"none": {"color": "fg"},
|
|
213
|
+
"number": {"color": "green"},
|
|
214
|
+
"number.float": {"color": "green"},
|
|
215
|
+
"operator": {"color": "purple"},
|
|
216
|
+
"punctuation.bracket": {"color": "grey"},
|
|
217
|
+
"punctuation.delimiter": {"color": "grey"},
|
|
218
|
+
"string": {"color": "green"},
|
|
219
|
+
"string.documentation": {"color": "green"},
|
|
220
|
+
"string.escape": {"color": "yellow"},
|
|
221
|
+
"string.regexp": {"color": "yellow"},
|
|
222
|
+
"type": {"color": "yellow"},
|
|
223
|
+
"type.builtin": {"color": "yellow"},
|
|
224
|
+
"type.definition": {"color": "yellow"},
|
|
225
|
+
"variable": {"color": "red", "italic": True},
|
|
226
|
+
"variable.builtin": {"color": "cyan", "italic": True},
|
|
227
|
+
"variable.member": {"color": "cyan"},
|
|
228
|
+
"variable.parameter": {"color": "red", "italic": True},
|
|
229
|
+
"constant": {"color": "red", "italic": True},
|
|
230
|
+
"constant.builtin": {"color": "red", "italic": True},
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
# fmt: on
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def read_theme(theme: Theme) -> tuple[GuiColors, SyntaxSpecs]:
|
|
237
|
+
"""Resolve a theme's palette names into gui colors and syntax specs
|
|
238
|
+
|
|
239
|
+
See the module docstring for the theme format.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
The gui colors, which also include the palette's names unless a gui
|
|
243
|
+
color has the same name, and the syntax specs
|
|
244
|
+
"""
|
|
245
|
+
pal = theme.get("palette", {})
|
|
246
|
+
|
|
247
|
+
gui = {
|
|
248
|
+
**pal,
|
|
249
|
+
**{k: _resolve_color(f"gui {k!r}", v, pal) for k, v in theme["gui"].items()},
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
newsyn: SyntaxSpecs = {}
|
|
253
|
+
for k, vdata_orig in theme["syntax"].items():
|
|
254
|
+
vdata = vdata_orig.copy()
|
|
255
|
+
if "color" in vdata:
|
|
256
|
+
vdata["color"] = _resolve_color(f"syntax {k!r}", vdata["color"], gui)
|
|
257
|
+
newsyn[k] = vdata
|
|
258
|
+
|
|
259
|
+
return gui, newsyn
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _resolve_color(where: str, value: str, names: Mapping[str, str]) -> str:
|
|
263
|
+
"""Look up a color name, warning if it's neither a known name nor a color"""
|
|
264
|
+
if value in names:
|
|
265
|
+
return names[value]
|
|
266
|
+
if not QtGui.QColor(value).isValid():
|
|
267
|
+
warnings.warn(
|
|
268
|
+
f"Theme {where} color {value!r} is not a palette name or a color",
|
|
269
|
+
stacklevel=3,
|
|
270
|
+
)
|
|
271
|
+
return value
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from Qt import QtCore, QtGui, QtWidgets
|
|
6
|
+
from tree_sitter import Point
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from tree_sitter import Node
|
|
10
|
+
|
|
11
|
+
# Qt positions count UTF-16 code units, and tree-sitter parses the document as
|
|
12
|
+
# UTF-16LE bytes. Python strings count code points, so they differ for
|
|
13
|
+
# characters outside the BMP, which take two code units
|
|
14
|
+
ENC = "utf-16-le"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def len16(val: str) -> int:
|
|
18
|
+
"""Get the number of UTF-16 code units, where surrogate pairs count as 2"""
|
|
19
|
+
return len(val.encode(ENC)) // 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def utf16_to_index(text: str, col: int) -> int:
|
|
23
|
+
"""Convert a UTF-16 column in text (ie: positionInBlock) to a str index
|
|
24
|
+
|
|
25
|
+
A column inside a surrogate pair rounds down to the start of the pair
|
|
26
|
+
"""
|
|
27
|
+
return len(text.encode(ENC)[: col * 2].decode(ENC, errors="ignore"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TrackedDocument(QtGui.QTextDocument):
|
|
31
|
+
"""A QTextDocument that reports its edits in the form tree-sitter expects
|
|
32
|
+
|
|
33
|
+
Qt positions are UTF-16 code units. Tree-sitter parses the document as
|
|
34
|
+
UTF-16LE, so its offsets and Point columns are in bytes, which is always
|
|
35
|
+
2 bytes per code unit. Every edit made through a TrackedCursor emits
|
|
36
|
+
`byteContentsChange` with those byte offsets.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
byteContentsChange: QtCore.Signal[tuple[int, int, int, Point, Point, Point]] = (
|
|
40
|
+
QtCore.Signal(int, int, int, Point, Point, Point)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
44
|
+
super().__init__(*args, **kwargs)
|
|
45
|
+
self.setDocumentLayout(QtWidgets.QPlainTextDocumentLayout(self))
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def byte_to_char(byte_offset: int) -> int:
|
|
49
|
+
"""Convert a UTF-16LE byte offset from tree-sitter to a Qt position"""
|
|
50
|
+
return byte_offset // 2
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def char_to_byte(position: int) -> int:
|
|
54
|
+
"""Convert a Qt position to a UTF-16LE byte offset for tree-sitter"""
|
|
55
|
+
return position * 2
|
|
56
|
+
|
|
57
|
+
def row_col_to_char(self, row: int, col: int) -> int:
|
|
58
|
+
"""Get the Qt position of a block row and a column in UTF-16 code units"""
|
|
59
|
+
return self.findBlockByNumber(row).position() + col
|
|
60
|
+
|
|
61
|
+
def node_text(self, node: Node) -> str:
|
|
62
|
+
"""Get the current text of a tree-sitter node of this document"""
|
|
63
|
+
start, end = node.start_point, node.end_point
|
|
64
|
+
lines = []
|
|
65
|
+
block = self.findBlockByNumber(start.row)
|
|
66
|
+
for row in range(start.row, end.row + 1):
|
|
67
|
+
if not block.isValid():
|
|
68
|
+
break
|
|
69
|
+
data = block.text().encode(ENC)
|
|
70
|
+
first = start.column if row == start.row else 0
|
|
71
|
+
last = end.column if row == end.row else len(data)
|
|
72
|
+
lines.append(data[first:last].decode(ENC, errors="replace"))
|
|
73
|
+
block = block.next()
|
|
74
|
+
return "\n".join(lines)
|
|
75
|
+
|
|
76
|
+
def report_edit(self, position: int, removed: str, added: str) -> None:
|
|
77
|
+
"""Emit `byteContentsChange` for an edit at a Qt position
|
|
78
|
+
|
|
79
|
+
The signal carries the start, old end and new end as UTF-16LE byte
|
|
80
|
+
offsets, then the same three as Points with byte columns.
|
|
81
|
+
"""
|
|
82
|
+
start_block = self.findBlock(position)
|
|
83
|
+
start_line = start_block.blockNumber()
|
|
84
|
+
start_col = self.char_to_byte(position - start_block.position())
|
|
85
|
+
start_byte = self.char_to_byte(position)
|
|
86
|
+
|
|
87
|
+
def end_point(text: str) -> tuple[int, Point]:
|
|
88
|
+
"""Get the end byte and end point of text inserted at position"""
|
|
89
|
+
# QTextCursor.selectedText uses the paragraph separator for newlines
|
|
90
|
+
text = text.replace("
", "\n")
|
|
91
|
+
lines = text.count("\n")
|
|
92
|
+
if lines == 0:
|
|
93
|
+
col = start_col + len(text.encode(ENC))
|
|
94
|
+
else:
|
|
95
|
+
col = len(text[text.rfind("\n") + 1 :].encode(ENC))
|
|
96
|
+
return start_byte + len(text.encode(ENC)), Point(start_line + lines, col)
|
|
97
|
+
|
|
98
|
+
old_end_byte, old_end_point = end_point(removed)
|
|
99
|
+
new_end_byte, new_end_point = end_point(added)
|
|
100
|
+
self.byteContentsChange.emit(
|
|
101
|
+
start_byte,
|
|
102
|
+
old_end_byte,
|
|
103
|
+
new_end_byte,
|
|
104
|
+
Point(start_line, start_col),
|
|
105
|
+
old_end_point,
|
|
106
|
+
new_end_point,
|
|
107
|
+
)
|