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