avrae-ls 0.4.1__py3-none-any.whl → 0.5.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.
avrae_ls/code_actions.py DELETED
@@ -1,282 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import logging
5
- import re
6
- from dataclasses import dataclass
7
- from pathlib import Path
8
- from typing import Iterable, List, Sequence
9
-
10
- from lsprotocol import types
11
-
12
- from .codes import MISSING_GVAR_CODE, UNDEFINED_NAME_CODE, UNSUPPORTED_IMPORT_CODE
13
- from .parser import DraconicBlock, find_draconic_blocks
14
-
15
- log = logging.getLogger(__name__)
16
-
17
- # Workspace-relative file that can extend snippet-based code actions without touching code.
18
- SNIPPET_FILENAME = ".avraels.snippets.json"
19
-
20
-
21
- @dataclass
22
- class Snippet:
23
- key: str
24
- title: str
25
- body: str
26
- description: str | None = None
27
- kind: str = types.CodeActionKind.RefactorRewrite
28
-
29
-
30
- DEFAULT_SNIPPETS: tuple[Snippet, ...] = (
31
- Snippet(
32
- key="drac2Wrapper",
33
- title="Wrap in <drac2>…</drac2>",
34
- body="<drac2>\n{content}\n</drac2>\n",
35
- description="Wrap the current selection or file in a draconic block.",
36
- kind=types.CodeActionKind.RefactorRewrite,
37
- ),
38
- )
39
-
40
-
41
- def code_actions_for_document(
42
- source: str,
43
- params: types.CodeActionParams,
44
- workspace_root: Path,
45
- ) -> List[types.CodeAction]:
46
- """Collect code actions for a document without requiring a running server."""
47
- actions: list[types.CodeAction] = []
48
- blocks = find_draconic_blocks(source)
49
- snippets = _load_snippets(workspace_root)
50
- only_kinds = list(params.context.only or [])
51
-
52
- actions.extend(_snippet_actions(source, params, snippets, only_kinds, blocks))
53
- actions.extend(_diagnostic_actions(source, params, blocks, only_kinds))
54
- return actions
55
-
56
-
57
- def _diagnostic_actions(
58
- source: str,
59
- params: types.CodeActionParams,
60
- blocks: list[DraconicBlock],
61
- only_kinds: Sequence[str],
62
- ) -> Iterable[types.CodeAction]:
63
- for diag in params.context.diagnostics or []:
64
- if diag.code == UNDEFINED_NAME_CODE and _kind_allowed(types.CodeActionKind.QuickFix, only_kinds):
65
- name = (diag.data or {}).get("name") if isinstance(diag.data, dict) else None
66
- if name:
67
- yield _stub_variable_action(source, params.text_document.uri, blocks, diag, name)
68
- if diag.code == MISSING_GVAR_CODE and _kind_allowed(types.CodeActionKind.QuickFix, only_kinds):
69
- gvar_id = (diag.data or {}).get("gvar") if isinstance(diag.data, dict) else None
70
- if gvar_id:
71
- yield _using_stub_action(source, params.text_document.uri, blocks, diag, gvar_id)
72
- if diag.code == UNSUPPORTED_IMPORT_CODE and _kind_allowed(types.CodeActionKind.QuickFix, only_kinds):
73
- module = (diag.data or {}).get("module") if isinstance(diag.data, dict) else None
74
- yield _rewrite_import_action(source, params.text_document.uri, diag, module)
75
-
76
-
77
- def _snippet_actions(
78
- source: str,
79
- params: types.CodeActionParams,
80
- snippets: Sequence[Snippet],
81
- only_kinds: Sequence[str],
82
- blocks: list[DraconicBlock],
83
- ) -> Iterable[types.CodeAction]:
84
- # Skip wrapper suggestions when a draconic block already exists.
85
- has_draconic = bool(blocks)
86
- selection_text = _text_in_range(source, params.range)
87
- for snippet in snippets:
88
- if not _kind_allowed(snippet.kind, only_kinds):
89
- continue
90
- if snippet.key == "drac2Wrapper" and has_draconic:
91
- continue
92
- rendered = _render_snippet(snippet, selection_text or source)
93
- edit_range = params.range
94
- if not selection_text:
95
- edit_range = _full_range(source)
96
- edit = types.TextEdit(range=edit_range, new_text=rendered)
97
- action = types.CodeAction(
98
- title=snippet.title,
99
- kind=snippet.kind,
100
- edit=types.WorkspaceEdit(changes={params.text_document.uri: [edit]}),
101
- )
102
- if snippet.description:
103
- action.diagnostics = None
104
- action.command = None
105
- yield action
106
-
107
-
108
- def _stub_variable_action(
109
- source: str,
110
- uri: str,
111
- blocks: list[DraconicBlock],
112
- diag: types.Diagnostic,
113
- name: str,
114
- ) -> types.CodeAction:
115
- insertion_line, indent = _block_insertion(blocks, diag.range.start.line, source)
116
- edit = types.TextEdit(
117
- range=types.Range(
118
- start=types.Position(line=insertion_line, character=indent),
119
- end=types.Position(line=insertion_line, character=indent),
120
- ),
121
- new_text=f"{' ' * indent}{name} = None\n",
122
- )
123
- return types.CodeAction(
124
- title=f"Create stub variable '{name}'",
125
- kind=types.CodeActionKind.QuickFix,
126
- diagnostics=[diag],
127
- edit=types.WorkspaceEdit(changes={uri: [edit]}),
128
- )
129
-
130
-
131
- def _using_stub_action(
132
- source: str,
133
- uri: str,
134
- blocks: list[DraconicBlock],
135
- diag: types.Diagnostic,
136
- gvar_id: str,
137
- ) -> types.CodeAction:
138
- alias = _sanitize_symbol(gvar_id)
139
- insertion_line, indent = _block_insertion(blocks, diag.range.start.line, source)
140
- text = f"{' ' * indent}using({alias}=\"{gvar_id}\")\n"
141
- edit = types.TextEdit(
142
- range=types.Range(
143
- start=types.Position(line=insertion_line, character=indent),
144
- end=types.Position(line=insertion_line, character=indent),
145
- ),
146
- new_text=text,
147
- )
148
- return types.CodeAction(
149
- title=f"Add using() stub for gvar '{gvar_id}'",
150
- kind=types.CodeActionKind.QuickFix,
151
- diagnostics=[diag],
152
- edit=types.WorkspaceEdit(changes={uri: [edit]}),
153
- )
154
-
155
-
156
- def _rewrite_import_action(
157
- source: str,
158
- uri: str,
159
- diag: types.Diagnostic,
160
- module: str | None,
161
- ) -> types.CodeAction:
162
- target = module or "module"
163
- replacement = f"using({target}=\"<gvar-id>\")"
164
- edit = types.TextEdit(range=diag.range, new_text=replacement)
165
- return types.CodeAction(
166
- title="Replace import with using()",
167
- kind=types.CodeActionKind.QuickFix,
168
- diagnostics=[diag],
169
- edit=types.WorkspaceEdit(changes={uri: [edit]}),
170
- )
171
-
172
-
173
- def _block_insertion(blocks: list[DraconicBlock], line: int, source: str) -> tuple[int, int]:
174
- for block in blocks:
175
- start = block.line_offset
176
- end = block.line_offset + block.line_count
177
- if start <= line <= end:
178
- indent = _line_indent(source, start, default=block.char_offset)
179
- return start, indent
180
- return 0, _line_indent(source, 0, default=0)
181
-
182
-
183
- def _line_indent(source: str, line: int, default: int = 0) -> int:
184
- lines = source.splitlines()
185
- if 0 <= line < len(lines):
186
- match = re.match(r"(\s*)", lines[line])
187
- if match:
188
- return len(match.group(1))
189
- return default
190
-
191
-
192
- def _sanitize_symbol(label: str) -> str:
193
- cleaned = re.sub(r"\W+", "_", str(label))
194
- if cleaned and cleaned[0].isdigit():
195
- cleaned = f"gvar_{cleaned}"
196
- return cleaned or "gvar_import"
197
-
198
-
199
- def _kind_allowed(kind: str, only: Sequence[str]) -> bool:
200
- if not only:
201
- return True
202
- for requested in only:
203
- if kind.startswith(requested) or requested.startswith(kind):
204
- return True
205
- return False
206
-
207
-
208
- def _text_in_range(source: str, rng: types.Range) -> str:
209
- if rng.start == rng.end:
210
- return ""
211
- start = _offset_at_position(source, rng.start)
212
- end = _offset_at_position(source, rng.end)
213
- return source[start:end]
214
-
215
-
216
- def _offset_at_position(source: str, pos: types.Position) -> int:
217
- if pos.line < 0:
218
- return 0
219
- lines = source.splitlines(keepends=True)
220
- if not lines:
221
- return 0
222
- if pos.line >= len(lines):
223
- return len(source)
224
- offset = sum(len(line) for line in lines[: pos.line])
225
- return min(offset + pos.character, offset + len(lines[pos.line]))
226
-
227
-
228
- def _render_snippet(snippet: Snippet, selection: str) -> str:
229
- if "{content}" in snippet.body:
230
- return snippet.body.replace("{content}", selection)
231
- return snippet.body
232
-
233
-
234
- def _full_range(source: str) -> types.Range:
235
- lines = source.splitlines()
236
- if not lines:
237
- return types.Range(start=types.Position(line=0, character=0), end=types.Position(line=0, character=0))
238
- end_line = len(lines) - 1
239
- end_char = len(lines[-1])
240
- return types.Range(
241
- start=types.Position(line=0, character=0),
242
- end=types.Position(line=end_line, character=end_char),
243
- )
244
-
245
-
246
- def _load_snippets(root: Path) -> List[Snippet]:
247
- snippets: list[Snippet] = list(DEFAULT_SNIPPETS)
248
- user_file = root / SNIPPET_FILENAME
249
- if not user_file.exists():
250
- return snippets
251
- try:
252
- raw = json.loads(user_file.read_text())
253
- except Exception as exc: # pragma: no cover - best-effort load
254
- log.warning("Failed to read snippet file %s: %s", user_file, exc)
255
- return snippets
256
-
257
- def _coerce(entry) -> Snippet | None:
258
- if not isinstance(entry, dict):
259
- return None
260
- key = str(entry.get("key") or entry.get("title") or "")
261
- body = entry.get("body")
262
- title = entry.get("title") or key
263
- if not key or not isinstance(body, str):
264
- return None
265
- return Snippet(
266
- key=key,
267
- title=str(title),
268
- body=body,
269
- description=str(entry.get("description") or ""),
270
- kind=str(entry.get("kind") or types.CodeActionKind.RefactorRewrite),
271
- )
272
-
273
- entries: Iterable[dict] = []
274
- if isinstance(raw, dict):
275
- entries = raw.values()
276
- elif isinstance(raw, list):
277
- entries = raw
278
- for entry in entries:
279
- coerced = _coerce(entry)
280
- if coerced:
281
- snippets.append(coerced)
282
- return snippets
avrae_ls/codes.py DELETED
@@ -1,3 +0,0 @@
1
- UNDEFINED_NAME_CODE = "avrae.undefinedName"
2
- MISSING_GVAR_CODE = "avrae.missingGvar"
3
- UNSUPPORTED_IMPORT_CODE = "avrae.unsupportedImport"