avrae-ls 0.2.1__py3-none-any.whl → 0.3.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/__main__.py +84 -0
- avrae_ls/api.py +113 -10
- avrae_ls/completions.py +298 -36
- avrae_ls/context.py +25 -7
- avrae_ls/diagnostics.py +32 -8
- avrae_ls/runtime.py +161 -36
- avrae_ls/server.py +1 -1
- avrae_ls/signature_help.py +73 -19
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/METADATA +4 -3
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/RECORD +14 -14
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/WHEEL +0 -0
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.0.dist-info}/top_level.txt +0 -0
avrae_ls/completions.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
import inspect
|
|
4
5
|
import re
|
|
6
|
+
import typing
|
|
5
7
|
from dataclasses import dataclass
|
|
8
|
+
from functools import lru_cache
|
|
6
9
|
from typing import Any, Dict, Iterable, List, Optional
|
|
7
10
|
|
|
8
11
|
from lsprotocol import types
|
|
@@ -75,8 +78,9 @@ TYPE_MAP: Dict[str, object] = {
|
|
|
75
78
|
|
|
76
79
|
|
|
77
80
|
IDENT_RE = re.compile(r"[A-Za-z_]\w*$")
|
|
78
|
-
ATTR_RE = re.compile(r"([A-Za-z_][\w
|
|
79
|
-
|
|
81
|
+
ATTR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.(?:([A-Za-z_]\w*)\s*)?$")
|
|
82
|
+
DICT_GET_RE = re.compile(r"^([A-Za-z_]\w*)\.get\(\s*(['\"])(.+?)\2")
|
|
83
|
+
ATTR_AT_CURSOR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.([A-Za-z_]\w*)")
|
|
80
84
|
|
|
81
85
|
|
|
82
86
|
@dataclass
|
|
@@ -87,6 +91,25 @@ class Suggestion:
|
|
|
87
91
|
documentation: str = ""
|
|
88
92
|
|
|
89
93
|
|
|
94
|
+
@dataclass
|
|
95
|
+
class AttrMeta:
|
|
96
|
+
doc: str = ""
|
|
97
|
+
type_name: str = ""
|
|
98
|
+
element_type: str = ""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class MethodMeta:
|
|
103
|
+
signature: str = ""
|
|
104
|
+
doc: str = ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class TypeMeta:
|
|
109
|
+
attrs: Dict[str, AttrMeta]
|
|
110
|
+
methods: Dict[str, MethodMeta]
|
|
111
|
+
|
|
112
|
+
|
|
90
113
|
def gather_suggestions(
|
|
91
114
|
ctx_data: ContextData,
|
|
92
115
|
resolver: GVarResolver,
|
|
@@ -130,15 +153,14 @@ def completion_items_for_position(
|
|
|
130
153
|
character: int,
|
|
131
154
|
suggestions: Iterable[Suggestion],
|
|
132
155
|
) -> List[types.CompletionItem]:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
receiver = attr_match.group(1)
|
|
137
|
-
attr_prefix = attr_match.group(2) or ""
|
|
156
|
+
attr_ctx = _attribute_receiver_and_prefix(code, line, character)
|
|
157
|
+
if attr_ctx:
|
|
158
|
+
receiver, attr_prefix = attr_ctx
|
|
138
159
|
sanitized = _sanitize_incomplete_line(code, line, character)
|
|
139
160
|
type_map = _infer_type_map(sanitized)
|
|
140
161
|
return _attribute_completions(receiver, attr_prefix, sanitized, type_map)
|
|
141
162
|
|
|
163
|
+
line_text = _line_text_to_cursor(code, line, character)
|
|
142
164
|
prefix = _current_prefix(line_text)
|
|
143
165
|
items: list[types.CompletionItem] = []
|
|
144
166
|
for sugg in suggestions:
|
|
@@ -158,10 +180,10 @@ def completion_items_for_position(
|
|
|
158
180
|
def _attribute_completions(receiver: str, prefix: str, code: str, type_map: Dict[str, str] | None = None) -> List[types.CompletionItem]:
|
|
159
181
|
items: list[types.CompletionItem] = []
|
|
160
182
|
type_key = _resolve_type_name(receiver, code, type_map)
|
|
161
|
-
|
|
183
|
+
meta = _type_meta(type_key)
|
|
162
184
|
detail = f"{type_key}()"
|
|
163
185
|
|
|
164
|
-
for name in attrs:
|
|
186
|
+
for name, attr_meta in meta.attrs.items():
|
|
165
187
|
if prefix and not name.startswith(prefix):
|
|
166
188
|
continue
|
|
167
189
|
items.append(
|
|
@@ -169,16 +191,19 @@ def _attribute_completions(receiver: str, prefix: str, code: str, type_map: Dict
|
|
|
169
191
|
label=name,
|
|
170
192
|
kind=types.CompletionItemKind.Field,
|
|
171
193
|
detail=detail,
|
|
194
|
+
documentation=attr_meta.doc or None,
|
|
172
195
|
)
|
|
173
196
|
)
|
|
174
|
-
for name in methods:
|
|
197
|
+
for name, method_meta in meta.methods.items():
|
|
175
198
|
if prefix and not name.startswith(prefix):
|
|
176
199
|
continue
|
|
200
|
+
method_detail = method_meta.signature or f"{name}()"
|
|
177
201
|
items.append(
|
|
178
202
|
types.CompletionItem(
|
|
179
203
|
label=name,
|
|
180
204
|
kind=types.CompletionItemKind.Method,
|
|
181
|
-
detail=
|
|
205
|
+
detail=method_detail,
|
|
206
|
+
documentation=method_meta.doc or None,
|
|
182
207
|
)
|
|
183
208
|
)
|
|
184
209
|
return items
|
|
@@ -195,29 +220,39 @@ def hover_for_position(
|
|
|
195
220
|
line_text = _line_text(code, line)
|
|
196
221
|
type_map = _infer_type_map(code)
|
|
197
222
|
bindings = _infer_constant_bindings(code, line, ctx_data)
|
|
198
|
-
|
|
199
|
-
if
|
|
223
|
+
attr_ctx = _attribute_receiver_and_prefix(code, line, character, capture_full_token=True)
|
|
224
|
+
if attr_ctx:
|
|
225
|
+
receiver, attr_prefix = attr_ctx
|
|
200
226
|
inferred = _resolve_type_name(receiver, code, type_map)
|
|
201
|
-
|
|
202
|
-
if
|
|
203
|
-
|
|
227
|
+
meta = _type_meta(inferred)
|
|
228
|
+
if attr_prefix in meta.attrs:
|
|
229
|
+
doc = meta.attrs[attr_prefix].doc
|
|
230
|
+
contents = f"```avrae\n{inferred}().{attr_prefix}\n```"
|
|
231
|
+
if doc:
|
|
232
|
+
contents += f"\n\n{doc}"
|
|
204
233
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
205
|
-
if
|
|
206
|
-
|
|
234
|
+
if attr_prefix in meta.methods:
|
|
235
|
+
method_meta = meta.methods[attr_prefix]
|
|
236
|
+
signature = method_meta.signature or f"{attr_prefix}()"
|
|
237
|
+
doc = method_meta.doc
|
|
238
|
+
contents = f"```avrae\n{signature}\n```"
|
|
239
|
+
if doc:
|
|
240
|
+
contents += f"\n\n{doc}"
|
|
207
241
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
208
242
|
|
|
209
243
|
word, _, _ = _word_at_position(line_text, character)
|
|
210
244
|
if not word:
|
|
211
245
|
return None
|
|
246
|
+
if word in bindings:
|
|
247
|
+
return _format_binding_hover(word, bindings[word], "local")
|
|
212
248
|
if word in type_map:
|
|
213
|
-
|
|
249
|
+
type_label = _display_type_label(type_map[word])
|
|
250
|
+
contents = f"`{word}` type: `{type_label}`"
|
|
214
251
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
215
252
|
if word in sigs:
|
|
216
253
|
sig = sigs[word]
|
|
217
254
|
contents = f"```avrae\n{sig.label}\n```\n\n{sig.doc}"
|
|
218
255
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
219
|
-
if word in bindings:
|
|
220
|
-
return _format_binding_hover(word, bindings[word], "local")
|
|
221
256
|
|
|
222
257
|
vars_map = ctx_data.vars.to_initial_names()
|
|
223
258
|
if word in vars_map:
|
|
@@ -256,6 +291,49 @@ def _line_text_to_cursor(code: str, line: int, character: int) -> str:
|
|
|
256
291
|
return lines[line][:character]
|
|
257
292
|
|
|
258
293
|
|
|
294
|
+
def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture_full_token: bool = False) -> Optional[tuple[str, str]]:
|
|
295
|
+
lines = code.splitlines()
|
|
296
|
+
if line >= len(lines):
|
|
297
|
+
return None
|
|
298
|
+
line_text = lines[line]
|
|
299
|
+
end = character
|
|
300
|
+
if capture_full_token:
|
|
301
|
+
while end < len(line_text) and (line_text[end].isalnum() or line_text[end] == "_"):
|
|
302
|
+
end += 1
|
|
303
|
+
line_text = line_text[: end]
|
|
304
|
+
dot = line_text.rfind(".")
|
|
305
|
+
if dot == -1:
|
|
306
|
+
return None
|
|
307
|
+
prefix = line_text[dot + 1 :].strip()
|
|
308
|
+
placeholder = "__COMPLETE__"
|
|
309
|
+
new_line = f"{line_text[:dot]}.{placeholder}"
|
|
310
|
+
mod_lines = list(lines)
|
|
311
|
+
mod_lines[line] = new_line
|
|
312
|
+
mod_code = "\n".join(mod_lines)
|
|
313
|
+
try:
|
|
314
|
+
tree = ast.parse(mod_code)
|
|
315
|
+
except SyntaxError:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
receiver_src: Optional[str] = None
|
|
319
|
+
|
|
320
|
+
class Finder(ast.NodeVisitor):
|
|
321
|
+
def visit_Attribute(self, node: ast.Attribute):
|
|
322
|
+
nonlocal receiver_src
|
|
323
|
+
if isinstance(node.attr, str) and node.attr == placeholder:
|
|
324
|
+
try:
|
|
325
|
+
receiver_src = ast.unparse(node.value)
|
|
326
|
+
except Exception:
|
|
327
|
+
receiver_src = None
|
|
328
|
+
self.generic_visit(node)
|
|
329
|
+
|
|
330
|
+
Finder().visit(tree)
|
|
331
|
+
if receiver_src is None:
|
|
332
|
+
return None
|
|
333
|
+
cleaned = re.sub(r"\[[^\]]*\]", "", receiver_src)
|
|
334
|
+
return cleaned, prefix
|
|
335
|
+
|
|
336
|
+
|
|
259
337
|
def _sanitize_incomplete_line(code: str, line: int, character: int) -> str:
|
|
260
338
|
lines = code.splitlines()
|
|
261
339
|
if 0 <= line < len(lines):
|
|
@@ -273,6 +351,12 @@ def _line_text(code: str, line: int) -> str:
|
|
|
273
351
|
return lines[line]
|
|
274
352
|
|
|
275
353
|
|
|
354
|
+
def _display_type_label(type_key: str) -> str:
|
|
355
|
+
if type_key in TYPE_MAP:
|
|
356
|
+
return TYPE_MAP[type_key].__name__
|
|
357
|
+
return type_key
|
|
358
|
+
|
|
359
|
+
|
|
276
360
|
def _infer_receiver_type(code: str, name: str) -> Optional[str]:
|
|
277
361
|
return _infer_type_map(code).get(name)
|
|
278
362
|
|
|
@@ -287,16 +371,20 @@ def _infer_type_map(code: str) -> Dict[str, str]:
|
|
|
287
371
|
class Visitor(ast.NodeVisitor):
|
|
288
372
|
def visit_Assign(self, node: ast.Assign):
|
|
289
373
|
val_type = self._value_type(node.value)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
374
|
+
for target in node.targets:
|
|
375
|
+
if not isinstance(target, ast.Name):
|
|
376
|
+
continue
|
|
377
|
+
if val_type:
|
|
378
|
+
type_map[target.id] = val_type
|
|
379
|
+
self._record_dict_key_types(target.id, node.value)
|
|
294
380
|
self.generic_visit(node)
|
|
295
381
|
|
|
296
382
|
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
297
383
|
val_type = self._value_type(node.value) if node.value else None
|
|
298
|
-
if
|
|
299
|
-
|
|
384
|
+
if isinstance(node.target, ast.Name):
|
|
385
|
+
if val_type:
|
|
386
|
+
type_map[node.target.id] = val_type
|
|
387
|
+
self._record_dict_key_types(node.target.id, node.value)
|
|
300
388
|
self.generic_visit(node)
|
|
301
389
|
|
|
302
390
|
def _value_type(self, value: ast.AST | None) -> Optional[str]:
|
|
@@ -322,13 +410,40 @@ def _infer_type_map(code: str) -> Dict[str, str]:
|
|
|
322
410
|
return None
|
|
323
411
|
return None
|
|
324
412
|
|
|
413
|
+
def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
|
|
414
|
+
if not isinstance(value, ast.Dict):
|
|
415
|
+
return
|
|
416
|
+
for key_node, val_node in zip(value.keys or [], value.values or []):
|
|
417
|
+
if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
|
|
418
|
+
val_type = self._value_type(val_node)
|
|
419
|
+
if val_type:
|
|
420
|
+
type_map[f"{var_name}.{key_node.value}"] = val_type
|
|
421
|
+
|
|
325
422
|
Visitor().visit(tree)
|
|
326
423
|
return type_map
|
|
327
424
|
|
|
328
425
|
|
|
329
426
|
def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None = None) -> str:
|
|
330
|
-
receiver = receiver.rstrip("()")
|
|
331
427
|
mapping = type_map or _infer_type_map(code)
|
|
428
|
+
get_match = DICT_GET_RE.match(receiver)
|
|
429
|
+
if get_match:
|
|
430
|
+
base, _, key = get_match.groups()
|
|
431
|
+
dict_key = f"{base}.{key}"
|
|
432
|
+
if dict_key in mapping:
|
|
433
|
+
return mapping[dict_key]
|
|
434
|
+
receiver = receiver.rstrip("()")
|
|
435
|
+
if "." in receiver:
|
|
436
|
+
base_expr, attr_name = receiver.rsplit(".", 1)
|
|
437
|
+
base_type = _resolve_type_name(base_expr, code, mapping)
|
|
438
|
+
if base_type:
|
|
439
|
+
meta = _type_meta(base_type)
|
|
440
|
+
attr_meta = meta.attrs.get(attr_name)
|
|
441
|
+
if attr_meta:
|
|
442
|
+
if attr_meta.element_type:
|
|
443
|
+
return attr_meta.element_type
|
|
444
|
+
if attr_meta.type_name:
|
|
445
|
+
return attr_meta.type_name
|
|
446
|
+
|
|
332
447
|
if receiver in mapping:
|
|
333
448
|
return mapping[receiver]
|
|
334
449
|
if receiver in TYPE_MAP:
|
|
@@ -339,13 +454,139 @@ def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None
|
|
|
339
454
|
return receiver
|
|
340
455
|
|
|
341
456
|
|
|
342
|
-
def _type_meta(type_name: str) ->
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
457
|
+
def _type_meta(type_name: str) -> TypeMeta:
|
|
458
|
+
return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}))
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@lru_cache()
|
|
462
|
+
def _type_meta_map() -> Dict[str, TypeMeta]:
|
|
463
|
+
meta: dict[str, TypeMeta] = {}
|
|
464
|
+
reverse_type_map: dict[type, str] = {}
|
|
465
|
+
for key, cls in TYPE_MAP.items():
|
|
466
|
+
reverse_type_map[cls] = key
|
|
467
|
+
|
|
468
|
+
def _iter_element_for_type_name(type_name: str) -> str:
|
|
469
|
+
cls = TYPE_MAP.get(type_name)
|
|
470
|
+
if not cls:
|
|
471
|
+
return ""
|
|
472
|
+
return _element_type_from_iterable(cls, reverse_type_map)
|
|
473
|
+
|
|
474
|
+
for type_name, cls in TYPE_MAP.items():
|
|
475
|
+
attrs: dict[str, AttrMeta] = {}
|
|
476
|
+
methods: dict[str, MethodMeta] = {}
|
|
477
|
+
|
|
478
|
+
for attr in getattr(cls, "ATTRS", []):
|
|
479
|
+
doc = ""
|
|
480
|
+
type_name_hint = ""
|
|
481
|
+
element_type_hint = ""
|
|
482
|
+
try:
|
|
483
|
+
attr_obj = getattr(cls, attr)
|
|
484
|
+
except Exception:
|
|
485
|
+
attr_obj = None
|
|
486
|
+
if isinstance(attr_obj, property) and attr_obj.fget:
|
|
487
|
+
doc = (attr_obj.fget.__doc__ or "").strip()
|
|
488
|
+
ann = _return_annotation(attr_obj.fget, cls)
|
|
489
|
+
type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
|
|
490
|
+
elif attr_obj is not None:
|
|
491
|
+
doc = (getattr(attr_obj, "__doc__", "") or "").strip()
|
|
492
|
+
if not type_name_hint and not element_type_hint:
|
|
493
|
+
ann = _class_annotation(cls, attr)
|
|
494
|
+
type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
|
|
495
|
+
if type_name_hint and not element_type_hint:
|
|
496
|
+
element_type_hint = _iter_element_for_type_name(type_name_hint)
|
|
497
|
+
attrs[attr] = AttrMeta(doc=doc, type_name=type_name_hint, element_type=element_type_hint)
|
|
498
|
+
|
|
499
|
+
for meth in getattr(cls, "METHODS", []):
|
|
500
|
+
doc = ""
|
|
501
|
+
sig_label = ""
|
|
502
|
+
try:
|
|
503
|
+
meth_obj = getattr(cls, meth)
|
|
504
|
+
except Exception:
|
|
505
|
+
meth_obj = None
|
|
506
|
+
if callable(meth_obj):
|
|
507
|
+
sig_label = _format_method_signature(meth, meth_obj)
|
|
508
|
+
doc = (meth_obj.__doc__ or "").strip()
|
|
509
|
+
methods[meth] = MethodMeta(signature=sig_label, doc=doc)
|
|
510
|
+
|
|
511
|
+
meta[type_name] = TypeMeta(attrs=attrs, methods=methods)
|
|
512
|
+
return meta
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _format_method_signature(name: str, obj: Any) -> str:
|
|
516
|
+
try:
|
|
517
|
+
sig = inspect.signature(obj)
|
|
518
|
+
except (TypeError, ValueError):
|
|
519
|
+
return f"{name}()"
|
|
520
|
+
params = list(sig.parameters.values())
|
|
521
|
+
if params and params[0].name in {"self", "cls"}:
|
|
522
|
+
params = params[1:]
|
|
523
|
+
sig = sig.replace(parameters=params)
|
|
524
|
+
return f"{name}{sig}"
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _return_annotation(func: Any, cls: type) -> Any:
|
|
528
|
+
try:
|
|
529
|
+
module = inspect.getmodule(func) or inspect.getmodule(cls)
|
|
530
|
+
globalns = module.__dict__ if module else None
|
|
531
|
+
hints = typing.get_type_hints(func, globalns=globalns, include_extras=False)
|
|
532
|
+
return hints.get("return")
|
|
533
|
+
except Exception:
|
|
534
|
+
return getattr(func, "__annotations__", {}).get("return")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _class_annotation(cls: type, attr: str) -> Any:
|
|
538
|
+
try:
|
|
539
|
+
module = inspect.getmodule(cls)
|
|
540
|
+
globalns = module.__dict__ if module else None
|
|
541
|
+
hints = typing.get_type_hints(cls, globalns=globalns, include_extras=False)
|
|
542
|
+
if attr in hints:
|
|
543
|
+
return hints[attr]
|
|
544
|
+
except Exception:
|
|
545
|
+
pass
|
|
546
|
+
return getattr(getattr(cls, "__annotations__", {}), "get", lambda _k: None)(attr)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _type_names_from_annotation(ann: Any, reverse_type_map: Dict[type, str]) -> tuple[str, str]:
|
|
550
|
+
if ann is None:
|
|
551
|
+
return "", ""
|
|
552
|
+
if isinstance(ann, str):
|
|
553
|
+
return "", ""
|
|
554
|
+
try:
|
|
555
|
+
origin = getattr(ann, "__origin__", None)
|
|
556
|
+
except Exception:
|
|
557
|
+
origin = None
|
|
558
|
+
args = getattr(ann, "__args__", ()) if origin else ()
|
|
559
|
+
|
|
560
|
+
if ann in reverse_type_map:
|
|
561
|
+
return reverse_type_map[ann], ""
|
|
562
|
+
|
|
563
|
+
# handle list/sequence typing to detect element type
|
|
564
|
+
iterable_origins = {list, List, Iterable, typing.Sequence, typing.Iterable}
|
|
565
|
+
try:
|
|
566
|
+
from collections.abc import Iterable as ABCIterable, Sequence as ABCSequence
|
|
567
|
+
iterable_origins.update({ABCIterable, ABCSequence})
|
|
568
|
+
except Exception:
|
|
569
|
+
pass
|
|
570
|
+
if origin in iterable_origins:
|
|
571
|
+
if args:
|
|
572
|
+
elem = args[0]
|
|
573
|
+
elem_name, _ = _type_names_from_annotation(elem, reverse_type_map)
|
|
574
|
+
return "", elem_name
|
|
575
|
+
return "", ""
|
|
576
|
+
|
|
577
|
+
if isinstance(ann, type) and ann in reverse_type_map:
|
|
578
|
+
return reverse_type_map[ann], ""
|
|
579
|
+
return "", ""
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _element_type_from_iterable(cls: type, reverse_type_map: Dict[type, str]) -> str:
|
|
583
|
+
try:
|
|
584
|
+
hints = typing.get_type_hints(cls.__iter__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
|
|
585
|
+
ret_ann = hints.get("return")
|
|
586
|
+
_, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
|
|
587
|
+
return elem
|
|
588
|
+
except Exception:
|
|
589
|
+
return ""
|
|
349
590
|
|
|
350
591
|
|
|
351
592
|
def _attribute_at_position(line_text: str, cursor: int) -> tuple[Optional[str], Optional[str]]:
|
|
@@ -550,12 +791,33 @@ def _is_safe_call(base: Any, method: str) -> bool:
|
|
|
550
791
|
|
|
551
792
|
|
|
552
793
|
def _format_binding_hover(name: str, value: Any, label: str) -> types.Hover:
|
|
553
|
-
type_name =
|
|
794
|
+
type_name = _describe_type(value)
|
|
554
795
|
preview = _preview_value(value)
|
|
555
796
|
contents = f"**{label}** `{name}`\n\nType: `{type_name}`\nValue: `{preview}`"
|
|
556
797
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
557
798
|
|
|
558
799
|
|
|
800
|
+
def _describe_type(value: Any) -> str:
|
|
801
|
+
# Provide light element-type hints for common iterables so hover shows list[Foo].
|
|
802
|
+
def _iterable_type(iterable: Iterable[Any], container: str) -> str:
|
|
803
|
+
try:
|
|
804
|
+
seen = {type(item).__name__ for item in iterable if item is not None}
|
|
805
|
+
except Exception:
|
|
806
|
+
return container
|
|
807
|
+
return f"{container}[{seen.pop()}]" if len(seen) == 1 else container
|
|
808
|
+
|
|
809
|
+
try:
|
|
810
|
+
if isinstance(value, list):
|
|
811
|
+
return _iterable_type(value, "list")
|
|
812
|
+
if isinstance(value, tuple):
|
|
813
|
+
return _iterable_type(value, "tuple")
|
|
814
|
+
if isinstance(value, set):
|
|
815
|
+
return _iterable_type(value, "set")
|
|
816
|
+
except Exception:
|
|
817
|
+
pass
|
|
818
|
+
return type(value).__name__
|
|
819
|
+
|
|
820
|
+
|
|
559
821
|
def _preview_value(value: Any) -> str:
|
|
560
822
|
text = repr(value)
|
|
561
823
|
if len(text) > 120:
|
avrae_ls/context.py
CHANGED
|
@@ -93,31 +93,49 @@ class GVarResolver:
|
|
|
93
93
|
async def ensure(self, key: str) -> bool:
|
|
94
94
|
key = str(key)
|
|
95
95
|
if key in self._cache:
|
|
96
|
+
log.debug("GVAR ensure cache hit for %s", key)
|
|
96
97
|
return True
|
|
97
98
|
if not self._config.enable_gvar_fetch:
|
|
99
|
+
log.warning("GVAR fetch disabled; skipping %s", key)
|
|
98
100
|
return False
|
|
99
101
|
if not self._config.service.token:
|
|
100
102
|
log.debug("GVAR fetch skipped for %s: no token configured", key)
|
|
101
103
|
return False
|
|
102
104
|
|
|
103
105
|
base_url = self._config.service.base_url.rstrip("/")
|
|
104
|
-
url = f"{base_url}/gvars/{key}"
|
|
105
|
-
|
|
106
|
+
url = f"{base_url}/customizations/gvars/{key}"
|
|
107
|
+
# Avrae service expects the JWT directly in Authorization (no Bearer prefix).
|
|
108
|
+
headers = {"Authorization": str(self._config.service.token)}
|
|
106
109
|
try:
|
|
110
|
+
log.debug("GVAR fetching %s from %s", key, url)
|
|
107
111
|
async with httpx.AsyncClient(timeout=5) as client:
|
|
108
112
|
resp = await client.get(url, headers=headers)
|
|
109
113
|
except Exception as exc:
|
|
110
|
-
log.
|
|
114
|
+
log.error("GVAR fetch failed for %s: %s", key, exc)
|
|
111
115
|
return False
|
|
112
116
|
|
|
113
117
|
if resp.status_code != 200:
|
|
114
|
-
log.
|
|
118
|
+
log.warning(
|
|
119
|
+
"GVAR fetch returned %s for %s (body: %s)",
|
|
120
|
+
resp.status_code,
|
|
121
|
+
key,
|
|
122
|
+
(resp.text or "").strip(),
|
|
123
|
+
)
|
|
115
124
|
return False
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
value: Any = None
|
|
127
|
+
try:
|
|
128
|
+
payload = resp.json()
|
|
129
|
+
except Exception:
|
|
130
|
+
payload = None
|
|
131
|
+
|
|
132
|
+
if isinstance(payload, dict) and "value" in payload:
|
|
133
|
+
value = payload["value"]
|
|
134
|
+
|
|
135
|
+
log.debug("GVAR fetch parsed value for %s (type=%s)", key, type(value).__name__)
|
|
136
|
+
|
|
119
137
|
if value is None:
|
|
120
|
-
log.
|
|
138
|
+
log.error("GVAR %s payload missing value", key)
|
|
121
139
|
return False
|
|
122
140
|
self._cache[key] = value
|
|
123
141
|
return True
|
avrae_ls/diagnostics.py
CHANGED
|
@@ -149,6 +149,13 @@ class DiagnosticProvider:
|
|
|
149
149
|
)
|
|
150
150
|
)
|
|
151
151
|
|
|
152
|
+
def visit_Call(self, node: ast.Call):
|
|
153
|
+
if isinstance(node.func, ast.Name) and node.func.id == "using":
|
|
154
|
+
for kw in node.keywords:
|
|
155
|
+
if kw.arg:
|
|
156
|
+
self.tracker.add(str(kw.arg))
|
|
157
|
+
self.generic_visit(node)
|
|
158
|
+
|
|
152
159
|
walker = Walker(known)
|
|
153
160
|
for stmt in body:
|
|
154
161
|
walker.visit(stmt)
|
|
@@ -200,24 +207,41 @@ async def _check_gvars(
|
|
|
200
207
|
settings: DiagnosticSettings,
|
|
201
208
|
) -> List[types.Diagnostic]:
|
|
202
209
|
diagnostics: list[types.Diagnostic] = []
|
|
210
|
+
seen: set[str] = set()
|
|
211
|
+
|
|
212
|
+
def _literal_value(node: ast.AST) -> str | None:
|
|
213
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
214
|
+
return node.value
|
|
215
|
+
if isinstance(node, ast.Str):
|
|
216
|
+
return node.s
|
|
217
|
+
return None
|
|
218
|
+
|
|
203
219
|
for node in _iter_calls(body):
|
|
204
|
-
if not isinstance(node.func, ast.Name)
|
|
220
|
+
if not isinstance(node.func, ast.Name):
|
|
205
221
|
continue
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
222
|
+
|
|
223
|
+
async def _validate_gvar(arg_node: ast.AST):
|
|
224
|
+
gvar_id = _literal_value(arg_node)
|
|
225
|
+
if gvar_id is None or gvar_id in seen:
|
|
226
|
+
return
|
|
227
|
+
seen.add(gvar_id)
|
|
211
228
|
found_local = resolver.get_local(gvar_id)
|
|
212
229
|
ensured = found_local is not None or await resolver.ensure(gvar_id)
|
|
213
230
|
if not ensured:
|
|
214
231
|
diagnostics.append(
|
|
215
232
|
_make_diagnostic(
|
|
216
|
-
|
|
233
|
+
arg_node,
|
|
217
234
|
f"Unknown gvar '{gvar_id}'",
|
|
218
235
|
settings.semantic_level,
|
|
219
236
|
)
|
|
220
237
|
)
|
|
238
|
+
|
|
239
|
+
if node.func.id == "get_gvar":
|
|
240
|
+
if node.args:
|
|
241
|
+
await _validate_gvar(node.args[0])
|
|
242
|
+
elif node.func.id == "using":
|
|
243
|
+
for kw in node.keywords:
|
|
244
|
+
await _validate_gvar(kw.value)
|
|
221
245
|
return diagnostics
|
|
222
246
|
|
|
223
247
|
|
|
@@ -342,7 +366,7 @@ def _build_builtin_signatures() -> dict[str, inspect.Signature]:
|
|
|
342
366
|
def get(name, default=None): ...
|
|
343
367
|
def using(**imports): ...
|
|
344
368
|
def signature(data=0): ...
|
|
345
|
-
def verify_signature(
|
|
369
|
+
def verify_signature(data): ...
|
|
346
370
|
def print_fn(*args, sep=" ", end="\n"): ...
|
|
347
371
|
|
|
348
372
|
helpers = {
|