avrae-ls 0.4.0__py3-none-any.whl → 0.4.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.
avrae_ls/diagnostics.py CHANGED
@@ -1,13 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- import inspect
4
3
  import ast
4
+ import inspect
5
5
  import logging
6
- from typing import Dict, Iterable, List, Sequence, Set
6
+ from typing import Any, Dict, Iterable, List, Sequence, Set
7
7
 
8
8
  import draconic
9
9
  from lsprotocol import types
10
10
 
11
+ from .alias_preview import simulate_command
12
+ from .codes import MISSING_GVAR_CODE, UNDEFINED_NAME_CODE, UNSUPPORTED_IMPORT_CODE
11
13
  from .argument_parsing import apply_argument_parsing
12
14
  from .completions import _infer_type_map, _resolve_type_name, _type_meta
13
15
  from .config import DiagnosticSettings
@@ -41,6 +43,10 @@ class DiagnosticProvider:
41
43
  source = apply_argument_parsing(source)
42
44
  blocks = find_draconic_blocks(source)
43
45
  if not blocks:
46
+ plain = _plain_command_diagnostics(source)
47
+ if plain is not None:
48
+ diagnostics.extend(plain)
49
+ return diagnostics
44
50
  diagnostics.extend(await self._analyze_code(source, ctx_data, gvar_resolver))
45
51
  return diagnostics
46
52
 
@@ -150,6 +156,8 @@ class DiagnosticProvider:
150
156
  node,
151
157
  f"'{node.id}' may be undefined in this scope",
152
158
  severity_level,
159
+ code=UNDEFINED_NAME_CODE,
160
+ data={"name": node.id},
153
161
  )
154
162
  )
155
163
 
@@ -237,6 +245,8 @@ async def _check_gvars(
237
245
  arg_node,
238
246
  f"Unknown gvar '{gvar_id}'",
239
247
  settings.semantic_level,
248
+ code=MISSING_GVAR_CODE,
249
+ data={"gvar": gvar_id},
240
250
  )
241
251
  )
242
252
 
@@ -487,7 +497,14 @@ def _build_parent_map(root: ast.AST) -> Dict[ast.AST, ast.AST]:
487
497
  return parents
488
498
 
489
499
 
490
- def _make_diagnostic(node: ast.AST, message: str, level: str) -> types.Diagnostic:
500
+ def _make_diagnostic(
501
+ node: ast.AST,
502
+ message: str,
503
+ level: str,
504
+ *,
505
+ code: str | None = None,
506
+ data: Dict[str, Any] | None = None,
507
+ ) -> types.Diagnostic:
491
508
  severity = SEVERITY.get(level, types.DiagnosticSeverity.Warning)
492
509
  if hasattr(node, "lineno"):
493
510
  rng = _range_from_positions(
@@ -506,6 +523,8 @@ def _make_diagnostic(node: ast.AST, message: str, level: str) -> types.Diagnosti
506
523
  range=rng,
507
524
  severity=severity,
508
525
  source="avrae-ls",
526
+ code=code,
527
+ data=data,
509
528
  )
510
529
 
511
530
 
@@ -666,10 +685,27 @@ def _check_imports(body: Sequence[ast.AST], severity_level: str) -> List[types.D
666
685
 
667
686
  class Visitor(ast.NodeVisitor):
668
687
  def visit_Import(self, node: ast.Import):
669
- diagnostics.append(_make_diagnostic(node, "Imports are not supported in draconic aliases", severity_level))
688
+ module = node.names[0].name if node.names else None
689
+ diagnostics.append(
690
+ _make_diagnostic(
691
+ node,
692
+ "Imports are not supported in draconic aliases",
693
+ severity_level,
694
+ code=UNSUPPORTED_IMPORT_CODE,
695
+ data={"module": module} if module else None,
696
+ )
697
+ )
670
698
 
671
699
  def visit_ImportFrom(self, node: ast.ImportFrom):
672
- diagnostics.append(_make_diagnostic(node, "Imports are not supported in draconic aliases", severity_level))
700
+ diagnostics.append(
701
+ _make_diagnostic(
702
+ node,
703
+ "Imports are not supported in draconic aliases",
704
+ severity_level,
705
+ code=UNSUPPORTED_IMPORT_CODE,
706
+ data={"module": node.module},
707
+ )
708
+ )
673
709
 
674
710
  visitor = Visitor()
675
711
  for stmt in body:
@@ -692,3 +728,24 @@ def _range_from_positions(
692
728
  character=max(((end_col_offset or col_offset or 1) - 1), 0),
693
729
  )
694
730
  return types.Range(start=start, end=end)
731
+
732
+
733
+ def _plain_command_diagnostics(source: str) -> list[types.Diagnostic] | None:
734
+ """Handle simple commands (embed/echo) without draconic blocks."""
735
+ simulated = simulate_command(source)
736
+ if not simulated.command_name:
737
+ return None
738
+ if simulated.command_name == "embed":
739
+ if simulated.validation_error:
740
+ return [
741
+ types.Diagnostic(
742
+ message=simulated.validation_error,
743
+ range=_range_from_positions(1, 1, 1, 1),
744
+ severity=SEVERITY["warning"],
745
+ source="avrae-ls",
746
+ )
747
+ ]
748
+ return []
749
+ if simulated.command_name == "echo":
750
+ return []
751
+ return None
avrae_ls/server.py CHANGED
@@ -18,6 +18,7 @@ from .alias_preview import render_alias_command, simulate_command
18
18
  from .parser import find_draconic_blocks
19
19
  from .signature_help import load_signatures, signature_help_for_code
20
20
  from .completions import gather_suggestions, completion_items_for_position, hover_for_position
21
+ from .code_actions import code_actions_for_document
21
22
  from .symbols import build_symbol_table, document_symbols, find_definition_range, find_references, range_for_word
22
23
  from .argument_parsing import apply_argument_parsing
23
24
 
@@ -260,6 +261,12 @@ def on_hover(server: AvraeLanguageServer, params: types.HoverParams):
260
261
  return None
261
262
 
262
263
 
264
+ @ls.feature(types.TEXT_DOCUMENT_CODE_ACTION)
265
+ def on_code_action(server: AvraeLanguageServer, params: types.CodeActionParams):
266
+ doc = server.workspace.get_text_document(params.text_document.uri)
267
+ return code_actions_for_document(doc.source, params, server.workspace_root)
268
+
269
+
263
270
  @ls.command(RUN_ALIAS_COMMAND)
264
271
  async def run_alias(server: AvraeLanguageServer, *args: Any):
265
272
  payload = args[0] if args else {}
@@ -289,18 +296,20 @@ async def run_alias(server: AvraeLanguageServer, *args: Any):
289
296
  server.state.context_builder.gvar_resolver,
290
297
  args=alias_args,
291
298
  )
292
- preview_output, command_name, validation_error = simulate_command(rendered.command)
299
+ preview = simulate_command(rendered.command)
293
300
 
294
301
  response: dict[str, Any] = {
295
302
  "stdout": rendered.stdout,
296
- "result": preview_output if preview_output is not None else rendered.last_value,
303
+ "result": preview.preview if preview.preview is not None else rendered.last_value,
297
304
  "command": rendered.command,
298
- "commandName": command_name,
305
+ "commandName": preview.command_name,
299
306
  }
300
307
  if rendered.error:
301
308
  response["error"] = _format_runtime_error(rendered.error)
302
- if validation_error:
303
- response["validationError"] = validation_error
309
+ if preview.validation_error:
310
+ response["validationError"] = preview.validation_error
311
+ if preview.embed:
312
+ response["embed"] = preview.embed.to_dict()
304
313
  if uri:
305
314
  extra = []
306
315
  if rendered.error:
@@ -157,15 +157,29 @@ def signature_help_for_code(code: str, line: int, character: int, sigs: Dict[str
157
157
  return None
158
158
 
159
159
  target_call: ast.Call | None = None
160
- for node in ast.walk(tree):
161
- if isinstance(node, ast.Call):
160
+ target_depth = -1
161
+
162
+ class Finder(ast.NodeVisitor):
163
+ def __init__(self):
164
+ self.stack: list[ast.AST] = []
165
+
166
+ def visit_Call(self, node: ast.Call):
167
+ nonlocal target_call, target_depth
162
168
  if hasattr(node, "lineno") and hasattr(node, "col_offset"):
163
169
  start = (node.lineno - 1, node.col_offset)
164
170
  end_line = getattr(node, "end_lineno", node.lineno) - 1
165
171
  end_col = getattr(node, "end_col_offset", node.col_offset)
166
172
  if _pos_within((line, character), start, (end_line, end_col)):
167
- target_call = node
168
- break
173
+ depth = len(self.stack)
174
+ # Prefer the most nested call covering the cursor
175
+ if depth >= target_depth:
176
+ target_call = node
177
+ target_depth = depth
178
+ self.stack.append(node)
179
+ self.generic_visit(node)
180
+ self.stack.pop()
181
+
182
+ Finder().visit(tree)
169
183
 
170
184
  if not target_call:
171
185
  return None
@@ -184,7 +198,7 @@ def signature_help_for_code(code: str, line: int, character: int, sigs: Dict[str
184
198
  documentation=fsig.doc,
185
199
  parameters=[types.ParameterInformation(label=p) for p in fsig.params],
186
200
  )
187
- active_param = min(len(target_call.args), max(len(fsig.params) - 1, 0))
201
+ active_param = _active_param_index(target_call, (line, character), fsig.params)
188
202
  return types.SignatureHelp(signatures=[sig_info], active_signature=0, active_parameter=active_param)
189
203
 
190
204
 
@@ -199,3 +213,40 @@ def _pos_within(pos: Tuple[int, int], start: Tuple[int, int], end: Tuple[int, in
199
213
  if line == el and col > ec:
200
214
  return False
201
215
  return True
216
+
217
+
218
+ def _active_param_index(call: ast.Call, cursor: Tuple[int, int], params: List[str]) -> int:
219
+ if not params:
220
+ return 0
221
+
222
+ # Build spans for positional args and keywords in source order.
223
+ spans: list[tuple[Tuple[int, int], Tuple[int, int], ast.AST]] = []
224
+ for arg in call.args:
225
+ spans.append((_node_start(arg), _node_end(arg), arg))
226
+ for kw in call.keywords:
227
+ spans.append((_node_start(kw), _node_end(kw), kw))
228
+ spans.sort(key=lambda s: (s[0][0], s[0][1]))
229
+
230
+ def _clamp(idx: int) -> int:
231
+ return max(0, min(idx, max(len(params) - 1, 0)))
232
+
233
+ for idx, (start, end, node) in enumerate(spans):
234
+ if _pos_within(cursor, start, end):
235
+ if isinstance(node, ast.keyword) and node.arg and node.arg in params:
236
+ return _clamp(params.index(node.arg))
237
+ return _clamp(idx)
238
+
239
+ # If cursor is after some args but not inside one, infer next argument slot.
240
+ before_count = sum(1 for start, _, _ in spans if start <= cursor)
241
+ return _clamp(before_count)
242
+
243
+
244
+ def _node_start(node: ast.AST) -> Tuple[int, int]:
245
+ return (getattr(node, "lineno", 1) - 1, getattr(node, "col_offset", 0))
246
+
247
+
248
+ def _node_end(node: ast.AST) -> Tuple[int, int]:
249
+ return (
250
+ getattr(node, "end_lineno", getattr(node, "lineno", 1)) - 1,
251
+ getattr(node, "end_col_offset", getattr(node, "col_offset", 0)),
252
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avrae-ls
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Language server for Avrae draconic aliases
5
5
  Author: 1drturtle
6
6
  Requires-Python: >=3.11
@@ -1,21 +1,23 @@
1
1
  avrae_ls/__init__.py,sha256=BmjrnksGkbG7TPqwbyQvgYj9uei8pFSFpfkRpaGVdJU,63
2
2
  avrae_ls/__main__.py,sha256=ch287lWe11go5xHAE9OkVppixt0vRF401E4zTs2tqQ0,3557
3
- avrae_ls/alias_preview.py,sha256=Wy_ZNRq73ojPiLFf6P6fJ7rG7DeFFsFLkLlhFrAjkKI,6117
3
+ avrae_ls/alias_preview.py,sha256=YFUiY36runNe6R0k-9YrbGff4l3evgEsoWbVc8WT5nU,11462
4
4
  avrae_ls/api.py,sha256=_AHvIEIlz34YeWdZDpXd1EwgdQUGk8-nqNIEN4qdNR8,65093
5
5
  avrae_ls/argparser.py,sha256=DRptXGyK4f0r7NsuV1Sg4apG-qihIClOX408jgk4HH4,13762
6
6
  avrae_ls/argument_parsing.py,sha256=ezKl65VwuNEDxt6KlYwVQcpy1110UDvf4BqZqgZTcqk,2122
7
- avrae_ls/completions.py,sha256=ht0J7JQX5QS8cz85itfozLBP9GPr1BjmNyttFEmJN-0,34514
7
+ avrae_ls/code_actions.py,sha256=CYl3nMzCaE9k35p5fWAmzsTOVfcv2RrkA3SbtyCdcsI,9423
8
+ avrae_ls/codes.py,sha256=iPRPQ6i9DZheae4_ra1y29vCw3Y4SEu6Udf5WiZj_RY,136
9
+ avrae_ls/completions.py,sha256=CvwKp3f20RsPFPnjUv03xCbuaGKIyIF7VgVhYNbig4U,51180
8
10
  avrae_ls/config.py,sha256=ULEfWbzIO05cllHBLUeGY_CHLpX6tHyCJYIEQgKSoXc,17383
9
11
  avrae_ls/context.py,sha256=KcCKqzqJCG2xlEtou5HWz1OWISApGl8IfwOvVnNi9nc,8464
10
12
  avrae_ls/cvars.py,sha256=0tcVbUHx_CKJ6aou3kEsKX37LRWAjkUWlqqIuSRFlXk,3197
11
- avrae_ls/diagnostics.py,sha256=2wn9Gu5vDGWt32zap9rhduv4q4B9c_X3baI2_yDHGYg,23074
13
+ avrae_ls/diagnostics.py,sha256=wq5sBjI11NyTNtnjoZTGm1vdcvOvW7OoPHbCDAbF308,24914
12
14
  avrae_ls/dice.py,sha256=DY7V7L-EwAXaCgddgVe9xU1s9lVtiw5Zc2reipNgdTk,874
13
15
  avrae_ls/parser.py,sha256=UQDwjupuJVU6KvoF4D8r3azPT7ksP_nTZsm5FUhGnWE,1395
14
16
  avrae_ls/runtime.py,sha256=PDnjZXcU5DqM9jKSc4S0jHObBE7SWKte17PWWXGQ34M,23356
15
- avrae_ls/server.py,sha256=jKypzveP2iRZjEdK0xJxfcn6QfbjrDe03XnXNhsga9Q,14709
16
- avrae_ls/signature_help.py,sha256=JheaEzINV4FO72t5U0AJfL2ZX15y3-gcA6xk3M1jHcY,6980
17
+ avrae_ls/server.py,sha256=s3Cl6Ta0954FqBUh9pM326TwdUBLb7ZuyzR8vjQqs5c,15101
18
+ avrae_ls/signature_help.py,sha256=sVQncCeLNCD3UBVPZu9ZFw0b-8wq64n3qFPLtWSy_Cs,8827
17
19
  avrae_ls/symbols.py,sha256=Lm5LH-F60JYNuAtSjRhHeyHEWAWxpnNb0w6KR0u1Zac,8422
18
- avrae_ls-0.4.0.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
20
+ avrae_ls-0.4.1.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
19
21
  draconic/LICENSE,sha256=Fzvu32_DafLKKn2mzxhEdlmrKZzAsigDZ87O7uoVqZI,1067
20
22
  draconic/__init__.py,sha256=YPH420Pcn_nTkfB62hJy_YqC5kpJdzSa78jP8n4z_xY,109
21
23
  draconic/exceptions.py,sha256=siahnHIsumbaUhKBDSrw_DmLZ-0oZks8L5oytPH8hD4,3753
@@ -25,8 +27,8 @@ draconic/string.py,sha256=kGrRc6wNHRq1y5xw8Os-fBhfINDtIY2nBWQWkyLSfQI,2858
25
27
  draconic/types.py,sha256=1Lsr6z8bW5agglGI4hLt_nPtYuZOIf_ueSpPDB4WDrs,13686
26
28
  draconic/utils.py,sha256=D4vJ-txqS2-rlqsEpXAC46_j1sZX4UjY-9zIgElo96k,3122
27
29
  draconic/versions.py,sha256=CUEsgUWjAmjez0432WwiBwZlIzWPIObwZUf8Yld18EE,84
28
- avrae_ls-0.4.0.dist-info/METADATA,sha256=z-bp7Y2zrilkbNmLujQYnHOW6rRlAyPMdQpJBDUB4cU,4713
29
- avrae_ls-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- avrae_ls-0.4.0.dist-info/entry_points.txt,sha256=OtYXipMQzqmxpMoApgo0MeJYFmMbkbFN51Ibhpb8hF4,52
31
- avrae_ls-0.4.0.dist-info/top_level.txt,sha256=TL68uzGHmB2R2ID32_s2zocmcNnpMJVQ6_4NBvyo8a4,18
32
- avrae_ls-0.4.0.dist-info/RECORD,,
30
+ avrae_ls-0.4.1.dist-info/METADATA,sha256=4kw6QJtVMqp_8pHBPra7sJXjchuCQdSOYiwj_dt7j1k,4713
31
+ avrae_ls-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ avrae_ls-0.4.1.dist-info/entry_points.txt,sha256=OtYXipMQzqmxpMoApgo0MeJYFmMbkbFN51Ibhpb8hF4,52
33
+ avrae_ls-0.4.1.dist-info/top_level.txt,sha256=TL68uzGHmB2R2ID32_s2zocmcNnpMJVQ6_4NBvyo8a4,18
34
+ avrae_ls-0.4.1.dist-info/RECORD,,