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.
Files changed (49) hide show
  1. QCodeSitter/__init__.py +38 -0
  2. QCodeSitter/behaviors/__init__.py +115 -0
  3. QCodeSitter/behaviors/auto_bracket.py +141 -0
  4. QCodeSitter/behaviors/code_folding.py +530 -0
  5. QCodeSitter/behaviors/comment_toggle.py +71 -0
  6. QCodeSitter/behaviors/highlight_matching_brackets.py +181 -0
  7. QCodeSitter/behaviors/highlight_matching_selection.py +59 -0
  8. QCodeSitter/behaviors/line_numbers.py +49 -0
  9. QCodeSitter/behaviors/multi_cursor_paint.py +131 -0
  10. QCodeSitter/behaviors/overscroll.py +104 -0
  11. QCodeSitter/behaviors/smart_indent.py +191 -0
  12. QCodeSitter/behaviors/syntax_highlighting.py +181 -0
  13. QCodeSitter/behaviors/tab_completion/__init__.py +257 -0
  14. QCodeSitter/behaviors/tab_completion/popup.py +123 -0
  15. QCodeSitter/behaviors/tab_completion/providers.py +66 -0
  16. QCodeSitter/behaviors/tab_completion/types.py +55 -0
  17. QCodeSitter/code_editor.py +540 -0
  18. QCodeSitter/cursors/__init__.py +8 -0
  19. QCodeSitter/cursors/clipboard.py +51 -0
  20. QCodeSitter/cursors/iterator.py +141 -0
  21. QCodeSitter/cursors/line_edits.py +128 -0
  22. QCodeSitter/cursors/multi.py +240 -0
  23. QCodeSitter/cursors/state.py +55 -0
  24. QCodeSitter/cursors/tracked.py +97 -0
  25. QCodeSitter/defaults.py +93 -0
  26. QCodeSitter/editing_keys.py +150 -0
  27. QCodeSitter/editor_options.py +246 -0
  28. QCodeSitter/gui_colors.py +51 -0
  29. QCodeSitter/gutters.py +141 -0
  30. QCodeSitter/integrations/__init__.py +1 -0
  31. QCodeSitter/integrations/preditor_interface.py +390 -0
  32. QCodeSitter/languages/__init__.py +99 -0
  33. QCodeSitter/languages/base.py +60 -0
  34. QCodeSitter/languages/python.py +146 -0
  35. QCodeSitter/languages/python_highlights.py +444 -0
  36. QCodeSitter/py.typed +0 -0
  37. QCodeSitter/queries/__init__.py +73 -0
  38. QCodeSitter/queries/predicates.py +172 -0
  39. QCodeSitter/syntax_specs.py +65 -0
  40. QCodeSitter/themes.py +271 -0
  41. QCodeSitter/tracked_document.py +107 -0
  42. QCodeSitter/tree_manager.py +180 -0
  43. QCodeSitter/utils.py +84 -0
  44. qcodesitter-0.1.0.dist-info/METADATA +255 -0
  45. qcodesitter-0.1.0.dist-info/RECORD +49 -0
  46. qcodesitter-0.1.0.dist-info/WHEEL +5 -0
  47. qcodesitter-0.1.0.dist-info/entry_points.txt +2 -0
  48. qcodesitter-0.1.0.dist-info/licenses/LICENSE.txt +7 -0
  49. qcodesitter-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ from Qt import QtCore, QtGui, QtWidgets
4
+
5
+ from ..cursors.line_edits import replace_lines_at_cursors
6
+ from ..languages import IndentRules, language_spec
7
+ from ..tracked_document import utf16_to_index
8
+ from ..utils import KEY, MM, MO, add_menu_action, dedent_string, leading_whitespace
9
+ from . import Behavior
10
+
11
+ CLOSING_BRACKETS = ("]", ")", "}")
12
+
13
+
14
+ class SmartIndent(Behavior):
15
+ """Syntax-aware indentation for Enter, Tab, Shift+Tab, Backspace and closing brackets
16
+
17
+ The indent settings come from the options, and the indent rules come from
18
+ the editor language's LanguageSpec
19
+ """
20
+
21
+ @property
22
+ def _use_tabs(self) -> bool:
23
+ return self.options["indent_using_tabs"]
24
+
25
+ @property
26
+ def _width(self) -> int:
27
+ return self.options["space_indent_width"]
28
+
29
+ def add_menu_actions(
30
+ self, menu: QtWidgets.QMenu, pos: QtCore.QPoint | None
31
+ ) -> None:
32
+ add_menu_action(menu, "Indent", self.insert_indent, "Tab")
33
+ add_menu_action(menu, "Unindent", self.unindent, "Shift+Tab")
34
+
35
+ def indent_rules(self) -> IndentRules:
36
+ spec = language_spec(self.editor.tree_manager.language)
37
+ return spec.indent_rules(self.editor.tree_manager)
38
+
39
+ def handle_key_press(self, event: QtGui.QKeyEvent) -> bool:
40
+ key = event.key()
41
+ if key == KEY.Key_Tab:
42
+ self.insert_indent()
43
+ return True
44
+ if key == KEY.Key_Backtab:
45
+ self.unindent()
46
+ return True
47
+ # These only handle some cursors, and leave the rest to the editor
48
+ if key in (KEY.Key_Return, KEY.Key_Enter):
49
+ self.smart_newline()
50
+ elif key == KEY.Key_Backspace:
51
+ self._smart_backspace()
52
+ elif event.text() in CLOSING_BRACKETS:
53
+ self._smart_closing_bracket(event.text())
54
+ else:
55
+ return False
56
+ return self.editor.citer.all_completed()
57
+
58
+ def _one_indent(self) -> str:
59
+ return "\t" if self._use_tabs else " " * self._width
60
+
61
+ def _dedent(self, indent: str) -> str:
62
+ return dedent_string(indent, self._use_tabs, self._width)
63
+
64
+ def _newline_indent(
65
+ self, cursor: QtGui.QTextCursor, rules: IndentRules
66
+ ) -> tuple[str, int]:
67
+ """Work out the indentation to insert after a newline at the cursor
68
+
69
+ Returns:
70
+ The text to insert after the newline
71
+ How far to move the cursor back from the end of that text
72
+ """
73
+ block = cursor.block()
74
+ line_text = block.text()
75
+ indent = leading_whitespace(line_text)
76
+ col = cursor.positionInBlock()
77
+
78
+ # At the start of a line, move the line down unchanged
79
+ if col == 0:
80
+ return "", 0
81
+
82
+ # A blank line keeps its indentation
83
+ if not line_text.strip():
84
+ return indent, 0
85
+
86
+ # Between a pair of brackets, put the closing bracket on its own line
87
+ # below an indented empty line
88
+ index = utf16_to_index(line_text, col)
89
+ if line_text[index - 1 : index + 1] in self.options["indent_bracket_pairs"]:
90
+ return indent + self._one_indent() + "\n" + indent, len(indent) + 1
91
+
92
+ # Look at the character just before the cursor to find the statement we just finished
93
+ row = block.blockNumber()
94
+ if rules.should_indent_after(row, col - 1):
95
+ return indent + self._one_indent(), 0
96
+ if rules.should_dedent_after(row, col - 1):
97
+ return self._dedent(indent), 0
98
+ return indent, 0
99
+
100
+ def smart_newline(self) -> None:
101
+ """Insert a newline indented to suit the code around each cursor"""
102
+ # Work out every indent before editing, because the edits change the tree
103
+ citer = self.editor.citer
104
+ rules = self.indent_rules()
105
+ indents = []
106
+ with self.editor.edit_session():
107
+ for cursor in citer.read_cursors():
108
+ if cursor.hasSelection():
109
+ cursor.setPosition(cursor.selectionStart())
110
+ indents.append(self._newline_indent(cursor, rules))
111
+
112
+ for i, cursor in enumerate(citer.iterate_cursors(complete_all=True)):
113
+ indent_str, move_back = indents[i]
114
+ cursor.insertText("\n" + indent_str)
115
+ if move_back:
116
+ cursor.movePosition(MO.Left, MM.MoveAnchor, move_back)
117
+
118
+ def insert_indent(self) -> None:
119
+ """Indent to the next tab stop, or indent every cursor's lines if any selects
120
+
121
+ A selection anywhere indents whole lines, so a cursor on a line that
122
+ another cursor's selection indents doesn't indent it twice
123
+ """
124
+ citer = self.editor.citer
125
+ width = self._width
126
+ indent = self._one_indent()
127
+
128
+ def indent_lines(lines: list[str]) -> list[str]:
129
+ return [indent + ln if ln.strip() else ln for ln in lines]
130
+
131
+ with self.editor.edit_session():
132
+ if any(cursor.hasSelection() for cursor in citer.read_cursors()):
133
+ replace_lines_at_cursors(citer, indent_lines)
134
+ return
135
+ for cursor in citer.iterate_cursors(complete_all=True):
136
+ if self._use_tabs:
137
+ cursor.insertText("\t")
138
+ else:
139
+ cursor.insertText(" " * (width - cursor.positionInBlock() % width))
140
+
141
+ def unindent(self) -> None:
142
+ """Remove one indent from every line of each cursor's selection or line"""
143
+ citer = self.editor.citer
144
+ width = self._width
145
+ use_tabs = self._use_tabs
146
+
147
+ def unindent_line(line: str) -> str:
148
+ if use_tabs:
149
+ return line.removeprefix("\t")
150
+ return line[:width].lstrip(" ") + line[width:]
151
+
152
+ with self.editor.edit_session():
153
+ replace_lines_at_cursors(
154
+ citer, lambda lines: [unindent_line(ln) for ln in lines]
155
+ )
156
+
157
+ def _smart_backspace(self) -> None:
158
+ """Backspace over a whole indent when the cursor is in the indentation"""
159
+ if self._use_tabs:
160
+ return
161
+ citer = self.editor.citer
162
+ width = self._width
163
+ for cursor in citer.iterate_cursors():
164
+ col = cursor.positionInBlock()
165
+ if cursor.hasSelection() or col == 0:
166
+ continue
167
+ # Spaces are one code unit each, so col is also the str index
168
+ if set(cursor.block().text()[:col]) != {" "}:
169
+ continue
170
+
171
+ # Delete back to the previous tab stop
172
+ cursor.movePosition(MO.Left, MM.KeepAnchor, col % width or width)
173
+ cursor.removeSelectedText()
174
+ citer.mark_cursor_completed()
175
+
176
+ def _smart_closing_bracket(self, bracket: str) -> None:
177
+ """Dedent a blank line when a closing bracket is typed on it"""
178
+ citer = self.editor.citer
179
+ for cursor in citer.iterate_cursors():
180
+ if cursor.hasSelection():
181
+ cursor.removeSelectedText()
182
+
183
+ line_text = cursor.block().text()
184
+ indent = leading_whitespace(line_text)
185
+ if not indent or line_text.strip():
186
+ continue
187
+
188
+ cursor.movePosition(MO.StartOfLine)
189
+ cursor.movePosition(MO.EndOfLine, MM.KeepAnchor)
190
+ cursor.insertText(self._dedent(indent) + bracket)
191
+ citer.mark_cursor_completed()
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from Qt import QtGui
6
+
7
+ from ..languages import highlight_query
8
+ from ..queries import query_byte_range
9
+ from ..syntax_specs import SyntaxSpecs, resolve_capture
10
+ from ..tracked_document import len16
11
+ from . import Behavior
12
+
13
+ if TYPE_CHECKING:
14
+ from tree_sitter import Query
15
+
16
+ from ..code_editor import CodeEditor
17
+ from ..tree_manager import TreeManager
18
+
19
+
20
+ class TreeSitterHighlighter(QtGui.QSyntaxHighlighter):
21
+ """Highlights each block with a tree-sitter query over the block's bytes
22
+
23
+ The SyntaxHighlighting behavior only rehighlights the rows that changed
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ tree_manager: TreeManager,
29
+ query: Query,
30
+ format_specs: SyntaxSpecs,
31
+ ) -> None:
32
+ super().__init__(tree_manager.document)
33
+ self.tree_manager = tree_manager
34
+ self._doc = tree_manager.document
35
+ self.query = query
36
+ self.formats = self._compile_formats(format_specs)
37
+
38
+ def _compile_formats(
39
+ self, format_specs: SyntaxSpecs
40
+ ) -> dict[str, QtGui.QTextCharFormat]:
41
+ """Convert the syntax specs into text formats, keyed by capture name"""
42
+ out: dict[str, QtGui.QTextCharFormat] = {}
43
+
44
+ for name, spec in format_specs.items():
45
+ fmt = QtGui.QTextCharFormat()
46
+ if "color" in spec:
47
+ fmt.setForeground(QtGui.QColor(spec["color"]))
48
+ if spec.get("bold"):
49
+ fmt.setFontWeight(QtGui.QFont.Weight.Bold)
50
+ if spec.get("italic"):
51
+ fmt.setFontItalic(True)
52
+ out[name] = fmt
53
+
54
+ # Captures in the query without an explicit format fall back to their
55
+ # parent group. ie: "keyword.return" -> "keyword"
56
+ for name in self.query_capture_names():
57
+ if name not in out:
58
+ fallback = resolve_capture(name, out)
59
+ if fallback is not None:
60
+ out[name] = fallback
61
+
62
+ return out
63
+
64
+ def query_capture_names(self) -> list[str]:
65
+ # capture_count is a property, but tree-sitter's stubs call it a method
66
+ count: int = self.query.capture_count # type: ignore[assignment]
67
+ return [self.query.capture_name(i) for i in range(count)]
68
+
69
+ def _block_byte_range(
70
+ self, block: QtGui.QTextBlock, text_len: int
71
+ ) -> tuple[int, int]:
72
+ """Get the UTF-16LE byte range of a block, including its newline"""
73
+ start = self._doc.char_to_byte(block.position())
74
+ next_block = block.next()
75
+ if next_block.isValid():
76
+ return start, self._doc.char_to_byte(next_block.position())
77
+ return start, start + self._doc.char_to_byte(text_len)
78
+
79
+ def highlightBlock(self, text: str) -> None:
80
+ tree = self.tree_manager.tree
81
+ block = self.currentBlock()
82
+ if tree is None or not block.isValid():
83
+ return
84
+
85
+ # Qt positions are UTF-16 code units, but python's len counts code points
86
+ # so they differ when the text contains characters outside the BMP
87
+ text_len = len16(text)
88
+ start_byte, end_byte = self._block_byte_range(block, text_len)
89
+ # Skip empty blocks, and blocks past the end of a tree that is out of date
90
+ root = tree.root_node
91
+ if start_byte >= end_byte or end_byte > root.end_byte:
92
+ return
93
+
94
+ captures = query_byte_range(
95
+ self.query, root, start_byte, end_byte, self._doc.node_text
96
+ )
97
+ block_start = block.position()
98
+ for capture_name, nodes in captures.items():
99
+ fmt = self.formats.get(capture_name)
100
+ if fmt is None:
101
+ continue
102
+ for node in nodes:
103
+ # Block-local UTF-16 positions, clamped to the block
104
+ local_start = self._doc.byte_to_char(node.start_byte) - block_start
105
+ local_end = self._doc.byte_to_char(node.end_byte) - block_start
106
+ local_start, local_end = max(0, local_start), min(text_len, local_end)
107
+ if local_end > local_start:
108
+ self.setFormat(local_start, local_end - local_start, fmt)
109
+
110
+
111
+ class SyntaxHighlighting(Behavior):
112
+ """Tree-sitter syntax highlighting
113
+
114
+ The query comes from the editor language's LanguageSpec, and the formats
115
+ of its captures from the "syntax" option
116
+ """
117
+
118
+ def __init__(self, editor: CodeEditor) -> None:
119
+ super().__init__(editor)
120
+ self._syntax: SyntaxSpecs = {}
121
+ self.highlighter: TreeSitterHighlighter | None = None
122
+ self.connect(
123
+ self.editor.tree_manager.languageChanged, self._rebuild_highlighter
124
+ )
125
+ self.connect(self.editor.tree_manager.reparsed, self._on_reparsed)
126
+ self.watch_options({"syntax": self._set_syntax})
127
+
128
+ def _set_syntax(self, syntax: SyntaxSpecs) -> None:
129
+ self._syntax = syntax
130
+ self._rebuild_highlighter()
131
+
132
+ def _rebuild_highlighter(self) -> None:
133
+ """Highlight with the current language's query and the syntax formats"""
134
+ self._detach_highlighter()
135
+ language = self.editor.tree_manager.language
136
+ query = highlight_query(language) if language is not None else None
137
+ if query is None or not self._syntax:
138
+ return
139
+ self.highlighter = TreeSitterHighlighter(
140
+ self.editor.tree_manager, query, self._syntax
141
+ )
142
+
143
+ def _detach_highlighter(self) -> None:
144
+ """Stop the current highlighter from formatting the document
145
+
146
+ The QSyntaxHighlighter is parented to the document, so dropping our
147
+ reference isn't enough to stop it from running.
148
+ """
149
+ if self.highlighter is None:
150
+ return
151
+ self.highlighter.setDocument(None)
152
+ self.highlighter.deleteLater()
153
+ self.highlighter = None
154
+
155
+ def _on_reparsed(self) -> None:
156
+ if self.highlighter is None:
157
+ return
158
+ changed_rows = self.editor.tree_manager.changed_rows
159
+ if changed_rows is None:
160
+ self.highlighter.rehighlight()
161
+ return
162
+
163
+ # Only rehighlight the rows that changed instead of the whole document
164
+ # Qt already highlighted the edited blocks with the old tree when the text
165
+ # changed. That goes one block past the edit when text is removed, so
166
+ # include that block too
167
+ doc = self.editor.document()
168
+ done: set[int] = set()
169
+ for first, last in changed_rows:
170
+ block = doc.findBlockByNumber(first)
171
+ for row in range(first, last + 2):
172
+ if not block.isValid():
173
+ break
174
+ if row not in done:
175
+ done.add(row)
176
+ self.highlighter.rehighlightBlock(block)
177
+ block = block.next()
178
+
179
+ def on_remove(self) -> None:
180
+ # Detaching the highlighter from the document also clears its formats
181
+ self._detach_highlighter()
@@ -0,0 +1,257 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterable
4
+ from typing import TYPE_CHECKING, TypeVar
5
+
6
+ from Qt import QtCore, QtGui
7
+
8
+ from ...languages import language_spec
9
+ from ...tracked_document import len16, utf16_to_index
10
+ from ...utils import KEY, MM, MOD
11
+ from .. import Behavior
12
+ from .popup import CompletionPopup
13
+ from .types import Completion, CompletionContext, Provider
14
+
15
+ if TYPE_CHECKING:
16
+ from ...code_editor import CodeEditor
17
+
18
+ T_Provider = TypeVar("T_Provider", bound=Provider)
19
+
20
+ MIN_PREFIX_LENGTH = 2
21
+
22
+
23
+ def is_identifier_char(char: str) -> bool:
24
+ """Whether a character can be part of an identifier, like a letter or digit"""
25
+ return char.isalnum() or char == "_"
26
+
27
+
28
+ def identifier_before(line: str, index: int) -> tuple[int, str]:
29
+ """Get the identifier that ends at a str index in the line
30
+
31
+ Returns:
32
+ The str index where the identifier starts
33
+ The identifier, like "jo" in "os.path.jo|"
34
+ """
35
+ start = index
36
+ while start > 0 and is_identifier_char(line[start - 1]):
37
+ start -= 1
38
+ return start, line[start:index]
39
+
40
+
41
+ def context_at_cursor(editor: CodeEditor) -> CompletionContext:
42
+ """Get the completion context at the editor's primary cursor"""
43
+ cursor = editor.textCursor()
44
+ block = cursor.block()
45
+ line = block.text()
46
+ index = utf16_to_index(line, cursor.positionInBlock())
47
+ start, prefix = identifier_before(line, index)
48
+ return CompletionContext(
49
+ line_num=block.blockNumber(),
50
+ index=index,
51
+ prefix=prefix,
52
+ start=start,
53
+ line=line,
54
+ node=editor.tree_manager.node_at_char(cursor.position()),
55
+ )
56
+
57
+
58
+ def merge_completions(completions: Iterable[Completion]) -> set[Completion]:
59
+ """Merge the completions of the same name and kind, keeping the highest priority"""
60
+ best: dict[Completion, Completion] = {}
61
+ for comp in completions:
62
+ current = best.get(comp)
63
+ if current is None or comp.priority > current.priority:
64
+ best[comp] = comp
65
+ return set(best.values())
66
+
67
+
68
+ class TabCompletion(Behavior):
69
+ """Shows a completion popup while typing an identifier
70
+
71
+ Completions come from the providers added with `add_provider`. Tab or
72
+ Enter accepts the selected completion, and Up/Down change it.
73
+ """
74
+
75
+ # Tab and Enter accept a completion before SmartIndent can indent with them
76
+ KEY_PRIORITY = 10
77
+
78
+ def __init__(self, editor: CodeEditor) -> None:
79
+ super().__init__(editor)
80
+ self._providers: list[Provider] = []
81
+ # Where the shown completions were asked for, or None before the first time
82
+ self._last_context: CompletionContext | None = None
83
+ # Whether the text changed since the cursor last moved, so moving
84
+ # the cursor around without typing doesn't open the popup
85
+ self._typing_mode = False
86
+
87
+ self.connect(self.editor.cursorPositionChanged, self.on_cursor_changed)
88
+ self.connect(self.editor.textChanged, self.on_text_changed)
89
+
90
+ # Wait for a pause in typing before asking the providers
91
+ self.debounce_timer = QtCore.QTimer(self.editor)
92
+ self.debounce_timer.setSingleShot(True)
93
+ self.debounce_timer.timeout.connect(self.refresh_completions)
94
+
95
+ self.completion_popup = CompletionPopup(self.editor)
96
+ self.watch_options({"colors": self.completion_popup.set_colors})
97
+
98
+ def add_provider(self, provider_cls: type[T_Provider]) -> T_Provider:
99
+ provider = provider_cls(self.editor)
100
+ self._providers.append(provider)
101
+ return provider
102
+
103
+ def on_remove(self) -> None:
104
+ self.debounce_timer.stop()
105
+ self.debounce_timer.deleteLater()
106
+ self.completion_popup.hide()
107
+ self.completion_popup.deleteLater()
108
+
109
+ def on_text_changed(self) -> None:
110
+ self._typing_mode = True
111
+
112
+ def _showing_completions_for(self, context: CompletionContext) -> bool:
113
+ """Whether the popup is showing the completions for the identifier at context
114
+
115
+ Once the popup hides, the next identifier asks the providers again
116
+ """
117
+ return (
118
+ self.completion_popup.isVisible()
119
+ and self._last_context is not None
120
+ and context.line_num == self._last_context.line_num
121
+ and context.start == self._last_context.start
122
+ )
123
+
124
+ def _hide_or_filter(self) -> CompletionContext | None:
125
+ """Hide the popup, or filter what it shows, for the identifier at the cursor
126
+
127
+ Returns:
128
+ The cursor's context when the providers need asking for new
129
+ completions, or None when the popup was hidden or filtered
130
+ """
131
+ context = context_at_cursor(self.editor)
132
+ if not self._should_trigger(context):
133
+ self.completion_popup.hide()
134
+ return None
135
+ if self._showing_completions_for(context):
136
+ # Still completing the same word, so just filter what's showing
137
+ self.completion_popup.update_filter(context.prefix)
138
+ self._last_context = context
139
+ return None
140
+ return context
141
+
142
+ def on_cursor_changed(self) -> None:
143
+ if self._hide_or_filter() is not None and self._typing_mode:
144
+ self.debounce_timer.start(self.options["debounce_delay"])
145
+ self._typing_mode = False
146
+
147
+ def _should_trigger(self, context: CompletionContext) -> bool:
148
+ """Whether the cursor is at the end of a long enough identifier, in code"""
149
+ spec = language_spec(self.editor.tree_manager.language)
150
+ if context.node is not None and context.node.type in spec.no_completion_types:
151
+ return False
152
+ if is_identifier_char(context.line[context.index : context.index + 1]):
153
+ # In the middle of an identifier
154
+ return False
155
+ prefix = context.prefix
156
+ return len(prefix) >= MIN_PREFIX_LENGTH and (
157
+ prefix[0].isalpha() or prefix[0] == "_"
158
+ )
159
+
160
+ def refresh_completions(self) -> None:
161
+ """Show the completions for the identifier at the cursor
162
+
163
+ This asks the providers for the completions of a new identifier, and
164
+ only filters the shown ones while still typing the same identifier.
165
+ It runs once typing pauses for the "debounce_delay" option
166
+ """
167
+ # The context might have changed while waiting
168
+ context = self._hide_or_filter()
169
+ if context is None:
170
+ return
171
+ completions = merge_completions(
172
+ comp for provider in self._providers for comp in provider.provide(context)
173
+ )
174
+ self.completion_popup.show_completions(completions, context.prefix)
175
+ self._last_context = context
176
+
177
+ # ------------------------------------------------------------------
178
+ # Keys. Each returns whether to consume the key
179
+ # ------------------------------------------------------------------
180
+
181
+ def accept_item(self) -> bool:
182
+ """Replace the prefix before every cursor with the selected completion
183
+
184
+ A cursor with other text before it only gets the rest of the completion.
185
+ Call it while the popup shows
186
+ """
187
+ comp = self.completion_popup.selected_completion()
188
+ if comp is not None:
189
+ prefix = self.completion_popup.current_prefix
190
+ citer = self.editor.citer
191
+ with self.editor.edit_session():
192
+ for cursor in citer.iterate_cursors(complete_all=True):
193
+ pos = cursor.position()
194
+ cursor.setPosition(max(0, pos - len16(prefix)), MM.KeepAnchor)
195
+ # Replace the prefix in one edit, so the tree only sees one change
196
+ if cursor.selectedText() == prefix:
197
+ cursor.insertText(comp.text)
198
+ else:
199
+ cursor.setPosition(pos)
200
+ cursor.insertText(comp.text[len(prefix) :])
201
+ self.completion_popup.hide()
202
+ return True
203
+
204
+ def select_previous(self) -> bool:
205
+ self.completion_popup.offset_current(-1)
206
+ return True
207
+
208
+ def select_next(self) -> bool:
209
+ self.completion_popup.offset_current(1)
210
+ return True
211
+
212
+ def dismiss(self) -> bool:
213
+ """Hide the popup, and consume the key"""
214
+ self.completion_popup.hide()
215
+ return True
216
+
217
+ def dismiss_and_pass_key(self) -> bool:
218
+ """Hide the popup, and let the key through"""
219
+ self.completion_popup.hide()
220
+ return False
221
+
222
+ def handle_key_press(self, event: QtGui.QKeyEvent) -> bool:
223
+ # The keys only act on a showing popup, and otherwise go to the next handler
224
+ if not self.completion_popup.isVisible():
225
+ return False
226
+ # Keyed by int, since event.key() is an int and not every key is in the enum
227
+ actions: dict[int, Callable[[], bool]] = {
228
+ KEY.Key_Return: self.accept_item,
229
+ KEY.Key_Enter: self.accept_item,
230
+ KEY.Key_Tab: self.accept_item,
231
+ KEY.Key_Up: self.select_previous,
232
+ KEY.Key_Down: self.select_next,
233
+ KEY.Key_Escape: self.dismiss,
234
+ KEY.Key_Backspace: self.dismiss_and_pass_key,
235
+ }
236
+ vim_keys = self.options["vim_completion_keys"]
237
+ if vim_keys and event.modifiers() & MOD.ControlModifier:
238
+ actions.update(
239
+ {
240
+ KEY.Key_Y: self.accept_item,
241
+ KEY.Key_P: self.select_previous,
242
+ KEY.Key_N: self.select_next,
243
+ }
244
+ )
245
+ action = actions.get(event.key())
246
+ return action() if action is not None else False
247
+
248
+
249
+ __all__ = [
250
+ "Completion",
251
+ "CompletionContext",
252
+ "CompletionPopup",
253
+ "Provider",
254
+ "TabCompletion",
255
+ "context_at_cursor",
256
+ "identifier_before",
257
+ ]