rpr-cli 0.1.1__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.
- rpr/__init__.py +1 -0
- rpr/agent/__init__.py +29 -0
- rpr/agent/approval.py +19 -0
- rpr/agent/bootstrap.py +60 -0
- rpr/agent/client.py +115 -0
- rpr/agent/control.py +16 -0
- rpr/agent/mock.py +158 -0
- rpr/agent/runtime.py +291 -0
- rpr/agent/session.py +91 -0
- rpr/agent/tools/__init__.py +10 -0
- rpr/agent/tools/base.py +62 -0
- rpr/agent/tools/mutating.py +47 -0
- rpr/agent/tools/readonly.py +147 -0
- rpr/agent/tools/registry.py +30 -0
- rpr/application/__init__.py +1 -0
- rpr/application/catalog.py +427 -0
- rpr/application/chat_service.py +413 -0
- rpr/application/checks.py +49 -0
- rpr/application/cli_adapter.py +36 -0
- rpr/application/completer.py +163 -0
- rpr/application/conversation_service.py +92 -0
- rpr/application/prompt_service.py +85 -0
- rpr/application/selector.py +240 -0
- rpr/application/shell.py +543 -0
- rpr/checks/__init__.py +0 -0
- rpr/checks/base.py +13 -0
- rpr/checks/instructions.py +89 -0
- rpr/checks/packages.py +619 -0
- rpr/checks/workspace.py +170 -0
- rpr/cli.py +78 -0
- rpr/commands/__init__.py +0 -0
- rpr/commands/add.py +166 -0
- rpr/commands/chat.py +48 -0
- rpr/commands/check.py +53 -0
- rpr/commands/generate/__init__.py +0 -0
- rpr/commands/generate/api.py +228 -0
- rpr/commands/generate/domain.py +383 -0
- rpr/commands/generate/engine.py +148 -0
- rpr/commands/generate/storybook.py +442 -0
- rpr/commands/generate/ui.py +414 -0
- rpr/commands/init.py +822 -0
- rpr/commands/map.py +113 -0
- rpr/commands/settings.py +102 -0
- rpr/commands/sync.py +97 -0
- rpr/context.py +203 -0
- rpr/generators/__init__.py +0 -0
- rpr/generators/base.py +110 -0
- rpr/generators/claude.py +33 -0
- rpr/generators/copilot.py +36 -0
- rpr/generators/cursor.py +38 -0
- rpr/generators/gemini.py +33 -0
- rpr/map/__init__.py +0 -0
- rpr/map/architecture.py +495 -0
- rpr/map/chains.py +317 -0
- rpr/map/classifier.py +170 -0
- rpr/map/coverage.py +200 -0
- rpr/map/dependencies.py +243 -0
- rpr/map/extractor.py +223 -0
- rpr/map/graph.py +318 -0
- rpr/map/output.py +1030 -0
- rpr/map/responsibility.py +345 -0
- rpr/map/topology.py +327 -0
- rpr/map/walker.py +151 -0
- rpr/scaffolds/domain/base_entity.md +30 -0
- rpr/scaffolds/domain/base_repo.md +48 -0
- rpr/scaffolds/domain/container.md +76 -0
- rpr/scaffolds/domain/settings.md +57 -0
- rpr/scaffolds/instructions/all.instructions.md +50 -0
- rpr/scaffolds/instructions/api.instructions.md +42 -0
- rpr/scaffolds/instructions/domain.instructions.md +93 -0
- rpr/scaffolds/instructions/frontend.instructions.md +97 -0
- rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
- rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
- rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
- rpr/scaffolds/instructions/tooling.instructions.md +42 -0
- rpr/scaffolds/js_special_files/fetch.service.md +222 -0
- rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
- rpr/scaffolds/special_files/domain_container.md +76 -0
- rpr/scaffolds/special_files/domain_settings.md +57 -0
- rpr/scaffolds/special_files/dto_util.md +62 -0
- rpr/scaffolds/special_files/encrypted_column.md +98 -0
- rpr/scaffolds/special_files/mapper_util.md +159 -0
- rpr/scaffolds/special_files/partial_update.md +61 -0
- rpr/templates/__init__.py +0 -0
- rpr/templates/registry.py +81 -0
- rpr/ui/__init__.py +0 -0
- rpr/ui/console.py +32 -0
- rpr/ui/markdown.py +59 -0
- rpr/ui/prompt_session.py +430 -0
- rpr/ui/renderers.py +167 -0
- rpr/ui/theme.py +286 -0
- rpr/workspace.py +131 -0
- rpr_cli-0.1.1.dist-info/METADATA +201 -0
- rpr_cli-0.1.1.dist-info/RECORD +97 -0
- rpr_cli-0.1.1.dist-info/WHEEL +4 -0
- rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
- rpr_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from rpr.agent.approval import ApprovalMode
|
|
9
|
+
from rpr.agent.session import ChatSession
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True, frozen=True)
|
|
13
|
+
class ConversationSummary:
|
|
14
|
+
session_id: str
|
|
15
|
+
title: str
|
|
16
|
+
project_dir: Path
|
|
17
|
+
created_at: datetime
|
|
18
|
+
updated_at: datetime
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConversationRepository(Protocol):
|
|
22
|
+
def save(self, session: ChatSession) -> None: ...
|
|
23
|
+
def find(self, session_id: str) -> ChatSession | None: ...
|
|
24
|
+
def all(self) -> list[ChatSession]: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InMemoryConversationRepository:
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._store: dict[str, ChatSession] = {}
|
|
30
|
+
|
|
31
|
+
def save(self, session: ChatSession) -> None:
|
|
32
|
+
self._store[session.session_id] = session
|
|
33
|
+
|
|
34
|
+
def find(self, session_id: str) -> ChatSession | None:
|
|
35
|
+
return self._store.get(session_id)
|
|
36
|
+
|
|
37
|
+
def all(self) -> list[ChatSession]:
|
|
38
|
+
return list(self._store.values())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConversationService:
|
|
42
|
+
def __init__(self, repository: ConversationRepository | None = None) -> None:
|
|
43
|
+
self._repository = repository or InMemoryConversationRepository()
|
|
44
|
+
self._current_session_id: str | None = None
|
|
45
|
+
|
|
46
|
+
def list_conversations(self) -> list[ConversationSummary]:
|
|
47
|
+
sessions = sorted(
|
|
48
|
+
self._repository.all(),
|
|
49
|
+
key=lambda session: session.updated_at,
|
|
50
|
+
reverse=True,
|
|
51
|
+
)
|
|
52
|
+
return [
|
|
53
|
+
ConversationSummary(
|
|
54
|
+
session_id=session.session_id,
|
|
55
|
+
title=session.title,
|
|
56
|
+
project_dir=session.project_dir,
|
|
57
|
+
created_at=session.created_at,
|
|
58
|
+
updated_at=session.updated_at,
|
|
59
|
+
)
|
|
60
|
+
for session in sessions
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
def create_conversation(
|
|
64
|
+
self, *, project_dir: Path, approval_mode: ApprovalMode
|
|
65
|
+
) -> ChatSession:
|
|
66
|
+
conversation = ChatSession(project_dir=project_dir, approval_mode=approval_mode)
|
|
67
|
+
self._repository.save(conversation)
|
|
68
|
+
self._current_session_id = conversation.session_id
|
|
69
|
+
return conversation
|
|
70
|
+
|
|
71
|
+
def load_conversation(self, session_id: str) -> ChatSession:
|
|
72
|
+
conversation = self._repository.find(session_id)
|
|
73
|
+
if conversation is None:
|
|
74
|
+
raise ValueError(f"Unknown conversation: {session_id}")
|
|
75
|
+
conversation.touch()
|
|
76
|
+
self._current_session_id = session_id
|
|
77
|
+
return conversation
|
|
78
|
+
|
|
79
|
+
def save_conversation(self, session: ChatSession | None = None) -> None:
|
|
80
|
+
if session is None:
|
|
81
|
+
if self._current_session_id is None:
|
|
82
|
+
return
|
|
83
|
+
session = self._repository.find(self._current_session_id)
|
|
84
|
+
if session is None:
|
|
85
|
+
return
|
|
86
|
+
session.touch()
|
|
87
|
+
self._repository.save(session)
|
|
88
|
+
self._current_session_id = session.session_id
|
|
89
|
+
|
|
90
|
+
def reset(self, repository: ConversationRepository | None = None) -> None:
|
|
91
|
+
self._repository = repository or InMemoryConversationRepository()
|
|
92
|
+
self._current_session_id = None
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit.completion import Completer
|
|
8
|
+
|
|
9
|
+
from rpr.agent.approval import ApprovalMode
|
|
10
|
+
from rpr.agent.control import TurnControl
|
|
11
|
+
from rpr.agent.session import ChatSession
|
|
12
|
+
from rpr.application.conversation_service import (
|
|
13
|
+
ConversationService,
|
|
14
|
+
ConversationSummary,
|
|
15
|
+
InMemoryConversationRepository,
|
|
16
|
+
)
|
|
17
|
+
from rpr.ui.prompt_session import PromptSessionAdapter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PromptService:
|
|
21
|
+
_instance: ClassVar[PromptService | None] = None
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._conversations = ConversationService()
|
|
25
|
+
self._session = PromptSessionAdapter()
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def instance(cls) -> PromptService:
|
|
29
|
+
if cls._instance is None:
|
|
30
|
+
cls._instance = cls()
|
|
31
|
+
return cls._instance
|
|
32
|
+
|
|
33
|
+
def set_active_task(self, task: asyncio.Task | None) -> None:
|
|
34
|
+
self._session.set_active_task(task)
|
|
35
|
+
|
|
36
|
+
def set_active_control(self, control: TurnControl | None) -> None:
|
|
37
|
+
self._session.set_active_control(control)
|
|
38
|
+
|
|
39
|
+
def set_queue_info(self, count: int, preview: str | None) -> None:
|
|
40
|
+
self._session.set_queue_info(count, preview)
|
|
41
|
+
|
|
42
|
+
def set_completer(self, completer: Completer) -> None:
|
|
43
|
+
self._session.set_completer(completer)
|
|
44
|
+
|
|
45
|
+
def has_modal_input(self) -> bool:
|
|
46
|
+
return self._session.has_modal_input()
|
|
47
|
+
|
|
48
|
+
def get_prompt_session(self):
|
|
49
|
+
return self._session.get_prompt_session()
|
|
50
|
+
|
|
51
|
+
def list_conversations(self) -> list[ConversationSummary]:
|
|
52
|
+
return self._conversations.list_conversations()
|
|
53
|
+
|
|
54
|
+
def create_conversation(
|
|
55
|
+
self, *, project_dir: Path, approval_mode: ApprovalMode
|
|
56
|
+
) -> ChatSession:
|
|
57
|
+
return self._conversations.create_conversation(
|
|
58
|
+
project_dir=project_dir,
|
|
59
|
+
approval_mode=approval_mode,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def load_conversation(self, session_id: str) -> ChatSession:
|
|
63
|
+
return self._conversations.load_conversation(session_id)
|
|
64
|
+
|
|
65
|
+
def save_conversation(self, session: ChatSession | None = None) -> None:
|
|
66
|
+
self._conversations.save_conversation(session)
|
|
67
|
+
|
|
68
|
+
async def read_input(
|
|
69
|
+
self,
|
|
70
|
+
message: str = "> ",
|
|
71
|
+
*,
|
|
72
|
+
multiline: bool = False,
|
|
73
|
+
modal: bool = False,
|
|
74
|
+
) -> str:
|
|
75
|
+
return await self._session.read_input(message, multiline=multiline, modal=modal)
|
|
76
|
+
|
|
77
|
+
async def prompt(self, label: str, *, default: str | None = None) -> str:
|
|
78
|
+
return await self._session.prompt(label, default=default)
|
|
79
|
+
|
|
80
|
+
async def confirm(self, label: str, *, default: bool = False) -> bool:
|
|
81
|
+
return await self._session.confirm(label, default=default)
|
|
82
|
+
|
|
83
|
+
def reset(self) -> None:
|
|
84
|
+
self._session.reset()
|
|
85
|
+
self._conversations.reset()
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(slots=True, frozen=True)
|
|
8
|
+
class TriggerSelectorCandidate:
|
|
9
|
+
value: str
|
|
10
|
+
label: str
|
|
11
|
+
description: str
|
|
12
|
+
insert_text: str
|
|
13
|
+
detail: str | None = None
|
|
14
|
+
keep_open_on_exact_match: bool = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True, frozen=True)
|
|
18
|
+
class TriggerSelectorSource:
|
|
19
|
+
trigger: str
|
|
20
|
+
candidates: tuple[TriggerSelectorCandidate, ...]
|
|
21
|
+
anchored_to_start: bool = True
|
|
22
|
+
hide_on_exact_match: bool = True
|
|
23
|
+
case_sensitive: bool = True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True, frozen=True)
|
|
27
|
+
class TriggerSelectorApplication:
|
|
28
|
+
text: str
|
|
29
|
+
cursor_position: int
|
|
30
|
+
candidate: TriggerSelectorCandidate
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True, frozen=True)
|
|
34
|
+
class TriggerSelectorSnapshot:
|
|
35
|
+
trigger: str | None
|
|
36
|
+
query: str
|
|
37
|
+
replacement_start: int
|
|
38
|
+
replacement_end: int
|
|
39
|
+
matches: tuple[TriggerSelectorCandidate, ...]
|
|
40
|
+
visible_matches: tuple[TriggerSelectorCandidate, ...]
|
|
41
|
+
selected_index: int
|
|
42
|
+
visible_start: int
|
|
43
|
+
dismissed: bool = False
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def selected_candidate(self) -> TriggerSelectorCandidate | None:
|
|
47
|
+
if not self.matches:
|
|
48
|
+
return None
|
|
49
|
+
return self.matches[self.selected_index]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def visible(self) -> bool:
|
|
53
|
+
return self.trigger is not None and bool(self.matches) and not self.dismissed
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TriggerSelector:
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
sources: Sequence[TriggerSelectorSource] = (),
|
|
60
|
+
*,
|
|
61
|
+
max_visible_matches: int = 6,
|
|
62
|
+
) -> None:
|
|
63
|
+
self._sources: tuple[TriggerSelectorSource, ...] = tuple(sources)
|
|
64
|
+
self._max_visible_matches = max_visible_matches
|
|
65
|
+
self._selected_index = 0
|
|
66
|
+
self._active_key: tuple[str, int, int, str] | None = None
|
|
67
|
+
self._dismissed_key: tuple[str, int, int, str] | None = None
|
|
68
|
+
|
|
69
|
+
def set_sources(self, sources: Sequence[TriggerSelectorSource]) -> None:
|
|
70
|
+
self._sources = tuple(sources)
|
|
71
|
+
self.reset()
|
|
72
|
+
|
|
73
|
+
def reset(self) -> None:
|
|
74
|
+
self._selected_index = 0
|
|
75
|
+
self._active_key = None
|
|
76
|
+
self._dismissed_key = None
|
|
77
|
+
|
|
78
|
+
def snapshot(
|
|
79
|
+
self, text: str, cursor_position: int | None = None
|
|
80
|
+
) -> TriggerSelectorSnapshot:
|
|
81
|
+
resolved_cursor_position = (
|
|
82
|
+
len(text) if cursor_position is None else cursor_position
|
|
83
|
+
)
|
|
84
|
+
source, query, replacement_start, replacement_end = self._find_active_source(
|
|
85
|
+
text=text,
|
|
86
|
+
cursor_position=resolved_cursor_position,
|
|
87
|
+
)
|
|
88
|
+
if source is None:
|
|
89
|
+
self._active_key = None
|
|
90
|
+
return TriggerSelectorSnapshot(
|
|
91
|
+
trigger=None,
|
|
92
|
+
query="",
|
|
93
|
+
replacement_start=resolved_cursor_position,
|
|
94
|
+
replacement_end=resolved_cursor_position,
|
|
95
|
+
matches=(),
|
|
96
|
+
visible_matches=(),
|
|
97
|
+
selected_index=0,
|
|
98
|
+
visible_start=0,
|
|
99
|
+
dismissed=False,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
normalized_query = query if source.case_sensitive else query.casefold()
|
|
103
|
+
matches = tuple(
|
|
104
|
+
candidate
|
|
105
|
+
for candidate in source.candidates
|
|
106
|
+
if self._normalize_value(
|
|
107
|
+
candidate.value, case_sensitive=source.case_sensitive
|
|
108
|
+
).startswith(normalized_query)
|
|
109
|
+
)
|
|
110
|
+
snapshot_key = (source.trigger, replacement_start, replacement_end, query)
|
|
111
|
+
if snapshot_key != self._active_key:
|
|
112
|
+
self._selected_index = 0
|
|
113
|
+
self._dismissed_key = None
|
|
114
|
+
self._active_key = snapshot_key
|
|
115
|
+
exact_matches = tuple(
|
|
116
|
+
candidate
|
|
117
|
+
for candidate in matches
|
|
118
|
+
if self._normalize_value(
|
|
119
|
+
candidate.value, case_sensitive=source.case_sensitive
|
|
120
|
+
)
|
|
121
|
+
== normalized_query
|
|
122
|
+
)
|
|
123
|
+
has_exact_match = (
|
|
124
|
+
source.hide_on_exact_match
|
|
125
|
+
and bool(exact_matches)
|
|
126
|
+
and all(
|
|
127
|
+
not candidate.keep_open_on_exact_match for candidate in exact_matches
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if matches:
|
|
132
|
+
self._selected_index = min(self._selected_index, len(matches) - 1)
|
|
133
|
+
else:
|
|
134
|
+
self._selected_index = 0
|
|
135
|
+
|
|
136
|
+
visible_start = self._visible_start(self._selected_index, len(matches))
|
|
137
|
+
visible_end = min(len(matches), visible_start + self._max_visible_matches)
|
|
138
|
+
return TriggerSelectorSnapshot(
|
|
139
|
+
trigger=source.trigger,
|
|
140
|
+
query=query,
|
|
141
|
+
replacement_start=replacement_start,
|
|
142
|
+
replacement_end=replacement_end,
|
|
143
|
+
matches=matches,
|
|
144
|
+
visible_matches=matches[visible_start:visible_end],
|
|
145
|
+
selected_index=self._selected_index,
|
|
146
|
+
visible_start=visible_start,
|
|
147
|
+
dismissed=snapshot_key == self._dismissed_key or has_exact_match,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def is_visible(self, text: str, cursor_position: int | None = None) -> bool:
|
|
151
|
+
return self.snapshot(text, cursor_position).visible
|
|
152
|
+
|
|
153
|
+
def move_selection(
|
|
154
|
+
self,
|
|
155
|
+
text: str,
|
|
156
|
+
cursor_position: int | None = None,
|
|
157
|
+
*,
|
|
158
|
+
step: int,
|
|
159
|
+
) -> TriggerSelectorSnapshot:
|
|
160
|
+
snapshot = self.snapshot(text, cursor_position)
|
|
161
|
+
if not snapshot.visible or not snapshot.matches:
|
|
162
|
+
return snapshot
|
|
163
|
+
self._selected_index = (snapshot.selected_index + step) % len(snapshot.matches)
|
|
164
|
+
return self.snapshot(text, cursor_position)
|
|
165
|
+
|
|
166
|
+
def dismiss(self, text: str, cursor_position: int | None = None) -> None:
|
|
167
|
+
snapshot = self.snapshot(text, cursor_position)
|
|
168
|
+
if snapshot.trigger is None:
|
|
169
|
+
return
|
|
170
|
+
self._dismissed_key = (
|
|
171
|
+
snapshot.trigger,
|
|
172
|
+
snapshot.replacement_start,
|
|
173
|
+
snapshot.replacement_end,
|
|
174
|
+
snapshot.query,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def apply_selection(
|
|
178
|
+
self,
|
|
179
|
+
text: str,
|
|
180
|
+
cursor_position: int | None = None,
|
|
181
|
+
) -> TriggerSelectorApplication | None:
|
|
182
|
+
snapshot = self.snapshot(text, cursor_position)
|
|
183
|
+
candidate = snapshot.selected_candidate
|
|
184
|
+
if not snapshot.visible or candidate is None:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
new_text = (
|
|
188
|
+
text[: snapshot.replacement_start]
|
|
189
|
+
+ candidate.insert_text
|
|
190
|
+
+ text[snapshot.replacement_end :]
|
|
191
|
+
)
|
|
192
|
+
new_cursor_position = snapshot.replacement_start + len(candidate.insert_text)
|
|
193
|
+
self.reset()
|
|
194
|
+
return TriggerSelectorApplication(
|
|
195
|
+
text=new_text,
|
|
196
|
+
cursor_position=new_cursor_position,
|
|
197
|
+
candidate=candidate,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _find_active_source(
|
|
201
|
+
self,
|
|
202
|
+
*,
|
|
203
|
+
text: str,
|
|
204
|
+
cursor_position: int,
|
|
205
|
+
) -> tuple[TriggerSelectorSource | None, str, int, int]:
|
|
206
|
+
text_before_cursor = text[:cursor_position]
|
|
207
|
+
for source in self._sources:
|
|
208
|
+
if source.anchored_to_start:
|
|
209
|
+
if not text_before_cursor.startswith(source.trigger):
|
|
210
|
+
continue
|
|
211
|
+
query = text_before_cursor[len(source.trigger) :]
|
|
212
|
+
if any(character.isspace() for character in query):
|
|
213
|
+
continue
|
|
214
|
+
return source, query, 0, cursor_position
|
|
215
|
+
|
|
216
|
+
trigger_index = text_before_cursor.rfind(source.trigger)
|
|
217
|
+
if trigger_index < 0:
|
|
218
|
+
continue
|
|
219
|
+
if (
|
|
220
|
+
trigger_index > 0
|
|
221
|
+
and not text_before_cursor[trigger_index - 1].isspace()
|
|
222
|
+
):
|
|
223
|
+
continue
|
|
224
|
+
query = text_before_cursor[trigger_index + len(source.trigger) :]
|
|
225
|
+
if any(character.isspace() for character in query):
|
|
226
|
+
continue
|
|
227
|
+
return source, query, trigger_index, cursor_position
|
|
228
|
+
|
|
229
|
+
return None, "", cursor_position, cursor_position
|
|
230
|
+
|
|
231
|
+
def _visible_start(self, selected_index: int, match_count: int) -> int:
|
|
232
|
+
if match_count <= self._max_visible_matches:
|
|
233
|
+
return 0
|
|
234
|
+
max_start = match_count - self._max_visible_matches
|
|
235
|
+
centered_start = selected_index - (self._max_visible_matches // 2)
|
|
236
|
+
return max(0, min(centered_start, max_start))
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _normalize_value(value: str, *, case_sensitive: bool) -> str:
|
|
240
|
+
return value if case_sensitive else value.casefold()
|