avrae-ls 0.4.0__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/server.py DELETED
@@ -1,390 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from dataclasses import dataclass, field
5
- from pathlib import Path
6
- from typing import Any, Dict
7
-
8
- import draconic
9
- from lsprotocol import types
10
- from pygls import uris
11
- from pygls.lsp.server import LanguageServer
12
-
13
- from .config import AvraeLSConfig, load_config
14
- from .context import ContextBuilder
15
- from .diagnostics import DiagnosticProvider
16
- from .runtime import MockExecutor
17
- from .alias_preview import render_alias_command, simulate_command
18
- from .parser import find_draconic_blocks
19
- from .signature_help import load_signatures, signature_help_for_code
20
- from .completions import gather_suggestions, completion_items_for_position, hover_for_position
21
- from .symbols import build_symbol_table, document_symbols, find_definition_range, find_references, range_for_word
22
- from .argument_parsing import apply_argument_parsing
23
-
24
- __version__ = "0.1.0"
25
- log = logging.getLogger(__name__)
26
-
27
- RUN_ALIAS_COMMAND = "avrae.runAlias"
28
- REFRESH_CONFIG_COMMAND = "avrae.reloadConfig"
29
- REFRESH_GVARS_COMMAND = "avrae.refreshGvars"
30
- LEVEL_TO_SEVERITY = {
31
- "error": types.DiagnosticSeverity.Error,
32
- "warning": types.DiagnosticSeverity.Warning,
33
- "info": types.DiagnosticSeverity.Information,
34
- }
35
-
36
-
37
- @dataclass
38
- class ServerState:
39
- config: AvraeLSConfig
40
- context_builder: ContextBuilder
41
- diagnostics: DiagnosticProvider
42
- executor: MockExecutor
43
- warnings: list[str] = field(default_factory=list)
44
-
45
-
46
- class AvraeLanguageServer(LanguageServer):
47
- def __init__(self):
48
- super().__init__(
49
- name="avrae-ls",
50
- version=__version__,
51
- text_document_sync_kind=types.TextDocumentSyncKind.Incremental,
52
- )
53
- self._state: ServerState | None = None
54
- self._workspace_root: Path | None = None
55
- self._signatures: Dict[str, Any] = load_signatures()
56
-
57
- @property
58
- def state(self) -> ServerState:
59
- if self._state is None:
60
- raise RuntimeError("Server has not been initialized")
61
- return self._state
62
-
63
- @property
64
- def workspace_root(self) -> Path:
65
- if self._workspace_root is None:
66
- return Path.cwd()
67
- return self._workspace_root
68
-
69
- def load_workspace(self, root: Path) -> None:
70
- config, warnings = load_config(root)
71
- executor = MockExecutor(config.service)
72
- context_builder = ContextBuilder(config)
73
- diagnostics = DiagnosticProvider(executor, config.diagnostics)
74
- self._state = ServerState(
75
- config=config,
76
- context_builder=context_builder,
77
- diagnostics=diagnostics,
78
- executor=executor,
79
- warnings=list(warnings),
80
- )
81
- self._workspace_root = root
82
- log.info("Loaded workspace at %s", root)
83
-
84
-
85
- ls = AvraeLanguageServer()
86
-
87
-
88
- @ls.feature(types.INITIALIZE)
89
- def on_initialize(server: AvraeLanguageServer, params: types.InitializeParams):
90
- root_uri = params.root_uri or (params.workspace_folders[0].uri if params.workspace_folders else None)
91
- root_path = Path(uris.to_fs_path(root_uri)) if root_uri else Path.cwd()
92
- server.load_workspace(root_path)
93
-
94
-
95
- @ls.feature(types.INITIALIZED)
96
- async def on_initialized(server: AvraeLanguageServer, params: types.InitializedParams):
97
- for warning in server.state.warnings:
98
- server.window_log_message(
99
- types.LogMessageParams(type=types.MessageType.Warning, message=warning)
100
- )
101
-
102
-
103
- @ls.feature(types.TEXT_DOCUMENT_DID_OPEN)
104
- async def did_open(server: AvraeLanguageServer, params: types.DidOpenTextDocumentParams):
105
- await _publish_diagnostics(server, params.text_document.uri)
106
-
107
-
108
- @ls.feature(types.TEXT_DOCUMENT_DID_CHANGE)
109
- async def did_change(server: AvraeLanguageServer, params: types.DidChangeTextDocumentParams):
110
- await _publish_diagnostics(server, params.text_document.uri)
111
-
112
-
113
- @ls.feature(types.TEXT_DOCUMENT_DID_SAVE)
114
- async def did_save(server: AvraeLanguageServer, params: types.DidSaveTextDocumentParams):
115
- await _publish_diagnostics(server, params.text_document.uri)
116
-
117
-
118
- @ls.feature(types.WORKSPACE_DID_CHANGE_CONFIGURATION)
119
- async def did_change_config(server: AvraeLanguageServer, params: types.DidChangeConfigurationParams):
120
- server.load_workspace(server.workspace_root)
121
- for warning in server.state.warnings:
122
- server.window_log_message(
123
- types.LogMessageParams(type=types.MessageType.Warning, message=warning)
124
- )
125
-
126
-
127
- @ls.feature(types.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
128
- def on_document_symbol(server: AvraeLanguageServer, params: types.DocumentSymbolParams):
129
- doc = server.workspace.get_text_document(params.text_document.uri)
130
- symbols = document_symbols(doc.source)
131
- return symbols
132
-
133
-
134
- @ls.feature(types.TEXT_DOCUMENT_DEFINITION)
135
- def on_definition(server: AvraeLanguageServer, params: types.DefinitionParams):
136
- doc = server.workspace.get_text_document(params.text_document.uri)
137
- table = build_symbol_table(doc.source)
138
- word = doc.word_at_position(params.position)
139
- rng = find_definition_range(table, word)
140
- if rng is None:
141
- return None
142
- return types.Location(uri=params.text_document.uri, range=rng)
143
-
144
-
145
- @ls.feature(types.TEXT_DOCUMENT_REFERENCES)
146
- def on_references(server: AvraeLanguageServer, params: types.ReferenceParams):
147
- doc = server.workspace.get_text_document(params.text_document.uri)
148
- table = build_symbol_table(doc.source)
149
- word = doc.word_at_position(params.position)
150
- if not word or not table.lookup(word):
151
- return []
152
-
153
- ranges = find_references(table, doc.source, word, include_declaration=params.context.include_declaration)
154
- return [types.Location(uri=params.text_document.uri, range=rng) for rng in ranges]
155
-
156
-
157
- @ls.feature(types.TEXT_DOCUMENT_PREPARE_RENAME)
158
- def on_prepare_rename(server: AvraeLanguageServer, params: types.PrepareRenameParams):
159
- doc = server.workspace.get_text_document(params.text_document.uri)
160
- table = build_symbol_table(doc.source)
161
- word = doc.word_at_position(params.position)
162
- if not word or not table.lookup(word):
163
- return None
164
- return range_for_word(doc.source, params.position)
165
-
166
-
167
- @ls.feature(types.TEXT_DOCUMENT_RENAME)
168
- def on_rename(server: AvraeLanguageServer, params: types.RenameParams):
169
- doc = server.workspace.get_text_document(params.text_document.uri)
170
- table = build_symbol_table(doc.source)
171
- word = doc.word_at_position(params.position)
172
- if not word or not table.lookup(word) or not params.new_name:
173
- return None
174
-
175
- ranges = find_references(table, doc.source, word, include_declaration=True)
176
- if not ranges:
177
- return None
178
- edits = [types.TextEdit(range=rng, new_text=params.new_name) for rng in ranges]
179
- return types.WorkspaceEdit(changes={params.text_document.uri: edits})
180
-
181
-
182
- @ls.feature(types.WORKSPACE_SYMBOL)
183
- def on_workspace_symbol(server: AvraeLanguageServer, params: types.WorkspaceSymbolParams):
184
- symbols: list[types.SymbolInformation] = []
185
- query = (params.query or "").lower()
186
- for uri, doc in server.workspace.text_documents.items():
187
- table = build_symbol_table(doc.source)
188
- for entry in table.entries:
189
- if query and query not in entry.name.lower():
190
- continue
191
- symbols.append(
192
- types.SymbolInformation(
193
- name=entry.name,
194
- kind=entry.kind,
195
- location=types.Location(uri=uri, range=entry.range),
196
- )
197
- )
198
- return symbols
199
-
200
-
201
- @ls.feature(types.TEXT_DOCUMENT_SIGNATURE_HELP)
202
- def on_signature_help(server: AvraeLanguageServer, params: types.SignatureHelpParams):
203
- doc = server.workspace.get_text_document(params.text_document.uri)
204
- source = apply_argument_parsing(doc.source)
205
- blocks = find_draconic_blocks(source)
206
- pos = params.position
207
- if not blocks:
208
- return signature_help_for_code(source, pos.line, pos.character, server._signatures)
209
-
210
- for block in blocks:
211
- start = block.line_offset
212
- end = block.line_offset + block.line_count
213
- if start <= pos.line <= end:
214
- rel_line = pos.line - start
215
- help_ = signature_help_for_code(block.code, rel_line, pos.character, server._signatures)
216
- if help_:
217
- return help_
218
- return None
219
-
220
-
221
- @ls.feature(
222
- types.TEXT_DOCUMENT_COMPLETION,
223
- types.CompletionOptions(trigger_characters=["."]),
224
- )
225
- def on_completion(server: AvraeLanguageServer, params: types.CompletionParams):
226
- doc = server.workspace.get_text_document(params.text_document.uri)
227
- ctx_data = server.state.context_builder.build()
228
- suggestions = gather_suggestions(ctx_data, server.state.context_builder.gvar_resolver, server._signatures)
229
- source = apply_argument_parsing(doc.source)
230
- blocks = find_draconic_blocks(source)
231
- pos = params.position
232
- if not blocks:
233
- return completion_items_for_position(source, pos.line, pos.character, suggestions)
234
-
235
- for block in blocks:
236
- start = block.line_offset
237
- end = block.line_offset + block.line_count
238
- if start <= pos.line <= end:
239
- rel_line = pos.line - start
240
- return completion_items_for_position(block.code, rel_line, pos.character, suggestions)
241
- return []
242
-
243
-
244
- @ls.feature(types.TEXT_DOCUMENT_HOVER)
245
- def on_hover(server: AvraeLanguageServer, params: types.HoverParams):
246
- doc = server.workspace.get_text_document(params.text_document.uri)
247
- ctx_data = server.state.context_builder.build()
248
- pos = params.position
249
- source = apply_argument_parsing(doc.source)
250
- blocks = find_draconic_blocks(source)
251
- if not blocks:
252
- return hover_for_position(source, pos.line, pos.character, server._signatures, ctx_data, server.state.context_builder.gvar_resolver)
253
-
254
- for block in blocks:
255
- start = block.line_offset
256
- end = block.line_offset + block.line_count
257
- if start <= pos.line <= end:
258
- rel_line = pos.line - start
259
- return hover_for_position(block.code, rel_line, pos.character, server._signatures, ctx_data, server.state.context_builder.gvar_resolver)
260
- return None
261
-
262
-
263
- @ls.command(RUN_ALIAS_COMMAND)
264
- async def run_alias(server: AvraeLanguageServer, *args: Any):
265
- payload = args[0] if args else {}
266
- uri = None
267
- text = None
268
- profile = None
269
- alias_args: list[str] | None = None
270
- if isinstance(payload, dict):
271
- uri = payload.get("uri")
272
- text = payload.get("text")
273
- profile = payload.get("profile")
274
- if isinstance(payload.get("args"), list):
275
- alias_args = [str(a) for a in payload["args"]]
276
-
277
- if text is None and uri:
278
- doc = server.workspace.get_text_document(uri)
279
- text = doc.source
280
-
281
- if text is None:
282
- return {"error": "No alias content supplied"}
283
-
284
- ctx_data = server.state.context_builder.build(profile)
285
- rendered = await render_alias_command(
286
- text,
287
- server.state.executor,
288
- ctx_data,
289
- server.state.context_builder.gvar_resolver,
290
- args=alias_args,
291
- )
292
- preview_output, command_name, validation_error = simulate_command(rendered.command)
293
-
294
- response: dict[str, Any] = {
295
- "stdout": rendered.stdout,
296
- "result": preview_output if preview_output is not None else rendered.last_value,
297
- "command": rendered.command,
298
- "commandName": command_name,
299
- }
300
- if rendered.error:
301
- response["error"] = _format_runtime_error(rendered.error)
302
- if validation_error:
303
- response["validationError"] = validation_error
304
- if uri:
305
- extra = []
306
- if rendered.error:
307
- extra.append(_runtime_diagnostic(rendered.error, server.state.config.diagnostics.runtime_level))
308
- await _publish_diagnostics(server, uri, profile=profile, extra=extra)
309
- return response
310
-
311
-
312
- @ls.command(REFRESH_GVARS_COMMAND)
313
- async def refresh_gvars(server: AvraeLanguageServer, *args: Any):
314
- payload = args[0] if args else {}
315
- profile = None
316
- keys: list[str] | None = None
317
- if isinstance(payload, dict):
318
- profile = payload.get("profile")
319
- raw_keys = payload.get("keys")
320
- if isinstance(raw_keys, list):
321
- keys = [str(k) for k in raw_keys]
322
-
323
- ctx_data = server.state.context_builder.build(profile)
324
- resolver = server.state.context_builder.gvar_resolver
325
- snapshot = await resolver.refresh(ctx_data.vars.gvars, keys)
326
- return {"count": len(snapshot), "gvars": snapshot}
327
-
328
-
329
- @ls.command(REFRESH_CONFIG_COMMAND)
330
- def reload_config(server: AvraeLanguageServer, *args: Any):
331
- server.load_workspace(server.workspace_root)
332
- return {"status": "ok"}
333
-
334
-
335
- async def _publish_diagnostics(
336
- server: AvraeLanguageServer,
337
- uri: str,
338
- profile: str | None = None,
339
- extra: list[types.Diagnostic] | None = None,
340
- ) -> None:
341
- doc = server.workspace.get_text_document(uri)
342
- ctx_data = server.state.context_builder.build(profile)
343
- diags = await server.state.diagnostics.analyze(
344
- doc.source, ctx_data, server.state.context_builder.gvar_resolver
345
- )
346
- if extra:
347
- diags.extend(extra)
348
- server.text_document_publish_diagnostics(
349
- types.PublishDiagnosticsParams(uri=uri, diagnostics=diags, version=doc.version)
350
- )
351
-
352
-
353
- def _format_runtime_error(error: BaseException) -> str:
354
- if isinstance(error, draconic.DraconicException):
355
- return error.msg
356
- return str(error)
357
-
358
-
359
- def _runtime_diagnostic(error: BaseException, level: str) -> types.Diagnostic:
360
- severity = LEVEL_TO_SEVERITY.get(level.lower(), types.DiagnosticSeverity.Error)
361
- if isinstance(error, draconic.DraconicSyntaxError):
362
- rng = types.Range(
363
- start=types.Position(line=max((error.lineno or 1) - 1, 0), character=max((error.offset or 1) - 1, 0)),
364
- end=types.Position(
365
- line=max(((error.end_lineno or error.lineno or 1) - 1), 0),
366
- character=max(((error.end_offset or error.offset or 1) - 1), 0),
367
- ),
368
- )
369
- message = error.msg
370
- elif hasattr(error, "node"):
371
- node = getattr(error, "node")
372
- rng = types.Range(
373
- start=types.Position(line=max(getattr(node, "lineno", 1) - 1, 0), character=max(getattr(node, "col_offset", 0), 0)),
374
- end=types.Position(
375
- line=max(getattr(node, "end_lineno", getattr(node, "lineno", 1)) - 1, 0),
376
- character=max(getattr(node, "end_col_offset", getattr(node, "col_offset", 0) + 1), 0),
377
- ),
378
- )
379
- message = getattr(error, "msg", str(error))
380
- else:
381
- rng = types.Range(
382
- start=types.Position(line=0, character=0),
383
- end=types.Position(line=0, character=1),
384
- )
385
- message = str(error)
386
- return types.Diagnostic(message=message, range=rng, severity=severity, source="avrae-ls-runtime")
387
-
388
-
389
- def create_server() -> AvraeLanguageServer:
390
- return ls
@@ -1,201 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import ast
4
- import inspect
5
- from dataclasses import dataclass
6
- from pathlib import Path
7
- from typing import Dict, List, Optional, Tuple
8
-
9
- from lsprotocol import types
10
-
11
- from .runtime import _default_builtins
12
-
13
-
14
- @dataclass
15
- class FunctionSig:
16
- name: str
17
- params: List[str]
18
- doc: str = ""
19
-
20
- @property
21
- def label(self) -> str:
22
- params = ", ".join(self.params)
23
- return f"{self.name}({params})"
24
-
25
-
26
- def load_signatures() -> Dict[str, FunctionSig]:
27
- sigs: dict[str, FunctionSig] = {}
28
- sigs.update(_builtin_sigs())
29
- sigs.update(_runtime_helper_sigs())
30
- sigs.update(_avrae_function_sigs())
31
- return sigs
32
-
33
-
34
- def _builtin_sigs() -> Dict[str, FunctionSig]:
35
- sigs: dict[str, FunctionSig] = {}
36
- for name, fn in _default_builtins().items():
37
- try:
38
- sig = inspect.signature(fn)
39
- except (TypeError, ValueError):
40
- continue
41
- params = [p.name for p in sig.parameters.values()]
42
- sigs[name] = FunctionSig(name=name, params=params, doc=fn.__doc__ or "")
43
- return sigs
44
-
45
-
46
- def _runtime_helper_sigs() -> Dict[str, FunctionSig]:
47
- helpers = {
48
- "get_gvar": (
49
- ["address"],
50
- "Retrieves and returns the value of a global variable (gvar) by address.",
51
- ),
52
- "get_svar": (
53
- ["name", "default=None"],
54
- "Gets a server variable by name, returning default if it is not present.",
55
- ),
56
- "get_cvar": (
57
- ["name", "default=None"],
58
- "Gets a character variable by name as a string, returning default if it is not present.",
59
- ),
60
- "get_uvar": (
61
- ["name", "default=None"],
62
- "Gets a user variable by name as a string, returning default if it is not present.",
63
- ),
64
- "get_uvars": (
65
- [],
66
- "Returns the mapping of user variables available to the caller (values are strings).",
67
- ),
68
- "set_uvar": (
69
- ["name", "value"],
70
- "Sets a user variable (stored as a string) and returns the stored value.",
71
- ),
72
- "set_uvar_nx": (
73
- ["name", "value"],
74
- "Sets a user variable only if it does not already exist; returns the stored string value.",
75
- ),
76
- "delete_uvar": (
77
- ["name"],
78
- "Deletes a user variable and returns its previous value or None if missing.",
79
- ),
80
- "uvar_exists": (
81
- ["name"],
82
- "Returns whether a user variable is set.",
83
- ),
84
- "exists": (
85
- ["name"],
86
- "Returns whether a name is set in the current evaluation context.",
87
- ),
88
- "get": (
89
- ["name", "default=None"],
90
- "Gets the value of a name using local > cvar > uvar resolution order; returns default if not set.",
91
- ),
92
- "using": (
93
- ["**imports"],
94
- "Imports one or more gvars as modules into the current namespace with the provided aliases.",
95
- ),
96
- "signature": (
97
- ["data=0"],
98
- "Generates a signed invocation signature encoding invocation context and optional 5-bit user data.",
99
- ),
100
- "verify_signature": (
101
- ["data"],
102
- "Verifies a signature generated by signature(); returns context data or raises ValueError when invalid.",
103
- ),
104
- "print": (
105
- ["*values"],
106
- "Writes values to alias output using the configured separator/end (mirrors Python print).",
107
- ),
108
- "character": (
109
- [],
110
- "Returns the active character object for this alias, or raises if none is available.",
111
- ),
112
- "combat": (
113
- [],
114
- "Returns the current combat context if one exists, otherwise None.",
115
- ),
116
- "argparse": (
117
- ["args", "character=None", "splitter=argsplit", "parse_ephem=True"],
118
- "Parses alias arguments using Avrae's argparse helper.",
119
- ),
120
- }
121
- return {name: FunctionSig(name=name, params=params, doc=doc) for name, (params, doc) in helpers.items()}
122
-
123
-
124
- def _avrae_function_sigs() -> Dict[str, FunctionSig]:
125
- sigs: dict[str, FunctionSig] = {}
126
- module_path = Path(__file__).resolve().parent.parent / "avrae" / "aliasing" / "api" / "functions.py"
127
- if not module_path.exists():
128
- return sigs
129
- try:
130
- tree = ast.parse(module_path.read_text())
131
- except Exception:
132
- return sigs
133
-
134
- for node in tree.body:
135
- if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"):
136
- params: list[str] = []
137
- defaults = list(node.args.defaults)
138
- default_offset = len(node.args.args) - len(defaults)
139
- for idx, arg in enumerate(node.args.args):
140
- default_val = None
141
- if idx >= default_offset:
142
- default_node = defaults[idx - default_offset]
143
- try:
144
- default_val = ast.literal_eval(default_node)
145
- except Exception:
146
- default_val = None
147
- params.append(f"{arg.arg}={default_val}" if default_val is not None else arg.arg)
148
- doc = ast.get_docstring(node) or ""
149
- sigs[node.name] = FunctionSig(name=node.name, params=params, doc=doc)
150
- return sigs
151
-
152
-
153
- def signature_help_for_code(code: str, line: int, character: int, sigs: Dict[str, FunctionSig]) -> Optional[types.SignatureHelp]:
154
- try:
155
- tree = ast.parse(code)
156
- except SyntaxError:
157
- return None
158
-
159
- target_call: ast.Call | None = None
160
- for node in ast.walk(tree):
161
- if isinstance(node, ast.Call):
162
- if hasattr(node, "lineno") and hasattr(node, "col_offset"):
163
- start = (node.lineno - 1, node.col_offset)
164
- end_line = getattr(node, "end_lineno", node.lineno) - 1
165
- end_col = getattr(node, "end_col_offset", node.col_offset)
166
- if _pos_within((line, character), start, (end_line, end_col)):
167
- target_call = node
168
- break
169
-
170
- if not target_call:
171
- return None
172
-
173
- if isinstance(target_call.func, ast.Name):
174
- name = target_call.func.id
175
- else:
176
- return None
177
-
178
- if name not in sigs:
179
- return None
180
-
181
- fsig = sigs[name]
182
- sig_info = types.SignatureInformation(
183
- label=fsig.label,
184
- documentation=fsig.doc,
185
- parameters=[types.ParameterInformation(label=p) for p in fsig.params],
186
- )
187
- active_param = min(len(target_call.args), max(len(fsig.params) - 1, 0))
188
- return types.SignatureHelp(signatures=[sig_info], active_signature=0, active_parameter=active_param)
189
-
190
-
191
- def _pos_within(pos: Tuple[int, int], start: Tuple[int, int], end: Tuple[int, int]) -> bool:
192
- (line, col) = pos
193
- (sl, sc) = start
194
- (el, ec) = end
195
- if line < sl or line > el:
196
- return False
197
- if line == sl and col < sc:
198
- return False
199
- if line == el and col > ec:
200
- return False
201
- return True