avrae-ls 0.6.4__tar.gz → 0.7.1__tar.gz

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.
Files changed (40) hide show
  1. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/PKG-INFO +6 -1
  2. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/README.md +5 -0
  3. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/pyproject.toml +1 -1
  4. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/__main__.py +54 -5
  5. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/alias_preview.py +66 -6
  6. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/alias_tests.py +13 -1
  7. avrae_ls-0.7.1/src/avrae_ls/ast_utils.py +14 -0
  8. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/code_actions.py +6 -2
  9. avrae_ls-0.7.1/src/avrae_ls/completions.py +730 -0
  10. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/config.py +1 -0
  11. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/context.py +62 -32
  12. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/diagnostics.py +33 -60
  13. avrae_ls-0.7.1/src/avrae_ls/lsp_utils.py +41 -0
  14. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/parser.py +30 -3
  15. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/server.py +85 -47
  16. avrae_ls-0.7.1/src/avrae_ls/source_context.py +30 -0
  17. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/symbols.py +27 -60
  18. avrae_ls-0.7.1/src/avrae_ls/type_inference.py +470 -0
  19. avrae_ls-0.7.1/src/avrae_ls/type_system.py +729 -0
  20. avrae_ls-0.6.4/src/avrae_ls/completions.py +0 -1695
  21. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/.gitignore +0 -0
  22. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/LICENSE +0 -0
  23. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/__init__.py +0 -0
  24. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/api.py +0 -0
  25. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/argparser.py +0 -0
  26. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/argument_parsing.py +0 -0
  27. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/codes.py +0 -0
  28. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/cvars.py +0 -0
  29. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/dice.py +0 -0
  30. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/runtime.py +0 -0
  31. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/avrae_ls/signature_help.py +0 -0
  32. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/draconic/LICENSE +0 -0
  33. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/draconic/__init__.py +0 -0
  34. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/draconic/exceptions.py +0 -0
  35. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/draconic/helpers.py +0 -0
  36. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/draconic/interpreter.py +0 -0
  37. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/draconic/string.py +0 -0
  38. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/draconic/types.py +0 -0
  39. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/draconic/utils.py +0 -0
  40. {avrae_ls-0.6.4 → avrae_ls-0.7.1}/src/draconic/versions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avrae-ls
3
- Version: 0.6.4
3
+ Version: 0.7.1
4
4
  Summary: Language server for Avrae draconic aliases
5
5
  Author: 1drturtle
6
6
  License: MIT License
@@ -50,6 +50,7 @@ Language Server Protocol (LSP) implementation targeting Avrae-style draconic ali
50
50
 
51
51
  - Install from VSIX: download `avrae-ls-client.vsix` from the GitHub releases page, then in VS Code run “Extensions: Install from VSIX” and select the file.
52
52
  - Open your alias workspace; commands like `Avrae: Show Alias Preview` and `Avrae: Run Alias` will be available.
53
+ - Files ending with `.alias-module` are treated as full-file draconic modules under the `avrae-module` language id (no `<drac2>` tags; mock run/preview commands stay tied to `.alias` files).
53
54
 
54
55
  ## Developing locally
55
56
 
@@ -96,6 +97,10 @@ Language Server Protocol (LSP) implementation targeting Avrae-style draconic ali
96
97
  `name` is a label for reporting, `vars` are merged into cvars/uvars/svars/gvars, and `character` keys are deep-merged into the mock character.
97
98
  - Run them with `avrae-ls --run-tests [path]` (defaults to the current directory); non-zero exit codes indicate failures.
98
99
 
100
+ ## Config variable substitution
101
+
102
+ - `.avraels.json` values support environment variable substitution with `$NAME` or `${NAME}`. `workspaceRoot` and `workspaceFolder` are injected automatically. Missing variables are replaced with an empty string and logged as warnings.
103
+
99
104
  ## Runtime differences (mock vs. live Avrae)
100
105
 
101
106
  - Mock execution never writes back to Avrae: cvar/uvar/gvar mutations only live for the current run and reset before the next.
@@ -10,6 +10,7 @@ Language Server Protocol (LSP) implementation targeting Avrae-style draconic ali
10
10
 
11
11
  - Install from VSIX: download `avrae-ls-client.vsix` from the GitHub releases page, then in VS Code run “Extensions: Install from VSIX” and select the file.
12
12
  - Open your alias workspace; commands like `Avrae: Show Alias Preview` and `Avrae: Run Alias` will be available.
13
+ - Files ending with `.alias-module` are treated as full-file draconic modules under the `avrae-module` language id (no `<drac2>` tags; mock run/preview commands stay tied to `.alias` files).
13
14
 
14
15
  ## Developing locally
15
16
 
@@ -56,6 +57,10 @@ Language Server Protocol (LSP) implementation targeting Avrae-style draconic ali
56
57
  `name` is a label for reporting, `vars` are merged into cvars/uvars/svars/gvars, and `character` keys are deep-merged into the mock character.
57
58
  - Run them with `avrae-ls --run-tests [path]` (defaults to the current directory); non-zero exit codes indicate failures.
58
59
 
60
+ ## Config variable substitution
61
+
62
+ - `.avraels.json` values support environment variable substitution with `$NAME` or `${NAME}`. `workspaceRoot` and `workspaceFolder` are injected automatically. Missing variables are replaced with an empty string and logged as warnings.
63
+
59
64
  ## Runtime differences (mock vs. live Avrae)
60
65
 
61
66
  - Mock execution never writes back to Avrae: cvar/uvar/gvar mutations only live for the current run and reset before the next.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "avrae-ls"
7
- version = "0.6.4"
7
+ version = "0.7.1"
8
8
  description = "Language server for Avrae draconic aliases"
9
9
  authors = [
10
10
  { name = "1drturtle" }
@@ -42,6 +42,11 @@ def main(argv: list[str] | None = None) -> None:
42
42
  const=".",
43
43
  help="Run alias tests in PATH (defaults to current directory)",
44
44
  )
45
+ parser.add_argument(
46
+ "--silent-gvar-fetch",
47
+ action="store_true",
48
+ help="Silently ignore gvar fetch failures and treat them as None",
49
+ )
45
50
  parser.add_argument("--token", help="Avrae API token (overrides config)")
46
51
  parser.add_argument("--base-url", help="Avrae API base URL (overrides config)")
47
52
  parser.add_argument("--version", action="store_true", help="Print version and exit")
@@ -58,12 +63,26 @@ def main(argv: list[str] | None = None) -> None:
58
63
  parser.error("--run-tests cannot be combined with --tcp")
59
64
  if args.analyze:
60
65
  parser.error("--run-tests cannot be combined with --analyze")
61
- sys.exit(_run_alias_tests(Path(args.run_tests), token_override=args.token, base_url_override=args.base_url))
66
+ sys.exit(
67
+ _run_alias_tests(
68
+ Path(args.run_tests),
69
+ token_override=args.token,
70
+ base_url_override=args.base_url,
71
+ silent_gvar_fetch=args.silent_gvar_fetch,
72
+ )
73
+ )
62
74
 
63
75
  if args.analyze:
64
76
  if args.tcp:
65
77
  parser.error("--analyze cannot be combined with --tcp")
66
- sys.exit(_run_analysis(Path(args.analyze), token_override=args.token, base_url_override=args.base_url))
78
+ sys.exit(
79
+ _run_analysis(
80
+ Path(args.analyze),
81
+ token_override=args.token,
82
+ base_url_override=args.base_url,
83
+ silent_gvar_fetch=args.silent_gvar_fetch,
84
+ )
85
+ )
67
86
 
68
87
  server = create_server()
69
88
  if args.tcp:
@@ -82,7 +101,13 @@ def _configure_logging(level: str) -> None:
82
101
  )
83
102
 
84
103
 
85
- def _run_analysis(path: Path, *, token_override: str | None = None, base_url_override: str | None = None) -> int:
104
+ def _run_analysis(
105
+ path: Path,
106
+ *,
107
+ token_override: str | None = None,
108
+ base_url_override: str | None = None,
109
+ silent_gvar_fetch: bool = False,
110
+ ) -> int:
86
111
  if not path.exists():
87
112
  print(f"File not found: {path}", file=sys.stderr)
88
113
  return 2
@@ -96,6 +121,8 @@ def _run_analysis(path: Path, *, token_override: str | None = None, base_url_ove
96
121
  config.service.token = token_override
97
122
  if base_url_override:
98
123
  config.service.base_url = base_url_override
124
+ if silent_gvar_fetch:
125
+ config.silent_gvar_fetch = True
99
126
  for warning in warnings:
100
127
  log.warning(warning)
101
128
 
@@ -111,7 +138,11 @@ def _run_analysis(path: Path, *, token_override: str | None = None, base_url_ove
111
138
 
112
139
 
113
140
  def _run_alias_tests(
114
- target: Path, *, token_override: str | None = None, base_url_override: str | None = None
141
+ target: Path,
142
+ *,
143
+ token_override: str | None = None,
144
+ base_url_override: str | None = None,
145
+ silent_gvar_fetch: bool = False,
115
146
  ) -> int:
116
147
  if not target.exists():
117
148
  print(f"Test path not found: {target}", file=sys.stderr)
@@ -126,6 +157,8 @@ def _run_alias_tests(
126
157
  config.service.token = token_override
127
158
  if base_url_override:
128
159
  config.service.base_url = base_url_override
160
+ if silent_gvar_fetch:
161
+ config.silent_gvar_fetch = True
129
162
  for warning in warnings:
130
163
  log.warning(warning)
131
164
 
@@ -169,7 +202,17 @@ def _print_test_results(results: Iterable[AliasTestResult], workspace_root: Path
169
202
  passed += 1
170
203
  continue
171
204
  if res.error:
172
- print(f" Error: {res.error}")
205
+ if res.error_line is not None and res.error_col is not None:
206
+ label = f" Execution Error (line {res.error_line} col {res.error_col})"
207
+ elif res.error_line is not None:
208
+ label = f" Execution Error (line {res.error_line})"
209
+ else:
210
+ label = " Execution Error"
211
+ print(_colorize_error_line(label))
212
+ print(f" {res.error}")
213
+ if res.stdout:
214
+ print(f" Stdout: {res.stdout.strip()}")
215
+ continue
173
216
  if res.details:
174
217
  print(f" {res.details}")
175
218
  expected_val, actual_val = _summarize_mismatch(res.case.expected, res.actual)
@@ -225,6 +268,12 @@ def _colorize_diff_line(line: str) -> str:
225
268
  return line
226
269
 
227
270
 
271
+ def _colorize_error_line(line: str) -> str:
272
+ if not sys.stdout.isatty():
273
+ return line
274
+ return f"\x1b[31m{line}\x1b[0m"
275
+
276
+
228
277
  def _print_labeled_value(label: str, value: str) -> None:
229
278
  lines = value.splitlines() or [""]
230
279
  if len(lines) == 1:
@@ -17,6 +17,8 @@ class RenderedAlias:
17
17
  stdout: str
18
18
  error: Optional[BaseException]
19
19
  last_value: Any | None = None
20
+ error_line: int | None = None
21
+ error_col: int | None = None
20
22
 
21
23
 
22
24
  @dataclass
@@ -58,7 +60,7 @@ class SimulatedCommand:
58
60
  embed: EmbedPreview | None = None
59
61
 
60
62
 
61
- def _strip_alias_header(text: str) -> str:
63
+ def _strip_alias_header_with_offset(text: str) -> tuple[str, int]:
62
64
  lines = text.splitlines()
63
65
  if lines and lines[0].lstrip().startswith("!alias"):
64
66
  first = lines[0].lstrip()
@@ -66,9 +68,57 @@ def _strip_alias_header(text: str) -> str:
66
68
  remainder = parts[2] if len(parts) > 2 else ""
67
69
  body = "\n".join(lines[1:])
68
70
  if remainder:
69
- return remainder + ("\n" + body if body else "")
70
- return body
71
- return text
71
+ return remainder + ("\n" + body if body else ""), 0
72
+ return body, 1
73
+ return text, 0
74
+
75
+
76
+ def _strip_alias_header(text: str) -> str:
77
+ body, _ = _strip_alias_header_with_offset(text)
78
+ return body
79
+
80
+
81
+ def _line_index_for_offset(text: str, offset: int) -> int:
82
+ return text.count("\n", 0, offset)
83
+
84
+
85
+ def _error_position_for_match(
86
+ body: str, match: re.Match[str], error: BaseException, line_offset: int
87
+ ) -> tuple[int | None, int | None]:
88
+ base_line = _line_index_for_offset(body, match.start(1))
89
+ base_line_start = body.rfind("\n", 0, match.start(1))
90
+ base_col = match.start(1) - (base_line_start + 1 if base_line_start != -1 else 0)
91
+ line_in_code: int | None = None
92
+ col_in_code: int | None = None
93
+ node = getattr(error, "node", None)
94
+ if node is not None:
95
+ node_line = getattr(node, "lineno", None)
96
+ if isinstance(node_line, int) and node_line > 0:
97
+ line_in_code = node_line
98
+ node_col = getattr(node, "col_offset", None)
99
+ if isinstance(node_col, int) and node_col >= 0:
100
+ col_in_code = node_col
101
+ if line_in_code is None:
102
+ lineno = getattr(error, "lineno", None)
103
+ if isinstance(lineno, int) and lineno > 0:
104
+ line_in_code = lineno
105
+ offset = getattr(error, "offset", None)
106
+ if isinstance(offset, int) and offset > 0:
107
+ col_in_code = offset - 1
108
+ if line_in_code is not None:
109
+ line_index = base_line + (line_in_code - 1)
110
+ else:
111
+ code = match.group(1)
112
+ leading_newlines = len(code) - len(code.lstrip("\n"))
113
+ line_index = base_line + leading_newlines
114
+ col_in_code = 0 if col_in_code is None else col_in_code
115
+ if col_in_code is None:
116
+ col_in_code = 0
117
+ if line_in_code is None or line_in_code == 1:
118
+ col_index = base_col + col_in_code
119
+ else:
120
+ col_index = col_in_code
121
+ return line_index + line_offset + 1, col_index + 1
72
122
 
73
123
 
74
124
  async def render_alias_command(
@@ -79,12 +129,14 @@ async def render_alias_command(
79
129
  args: list[str] | None = None,
80
130
  ) -> RenderedAlias:
81
131
  """Replace <drac2> blocks with their evaluated values and return final command."""
82
- body = _strip_alias_header(text)
132
+ body, line_offset = _strip_alias_header_with_offset(text)
83
133
  body = apply_argument_parsing(body, args)
84
134
  stdout_parts: list[str] = []
85
135
  parts: list[str] = []
86
136
  last_value = None
87
137
  error: BaseException | None = None
138
+ error_line: int | None = None
139
+ error_col: int | None = None
88
140
 
89
141
  pos = 0
90
142
  matches: list[tuple[str, re.Match[str]]] = []
@@ -109,6 +161,7 @@ async def render_alias_command(
109
161
  stdout_parts.append(result.stdout)
110
162
  if result.error:
111
163
  error = result.error
164
+ error_line, error_col = _error_position_for_match(body, match, result.error, line_offset)
112
165
  break
113
166
  last_value = result.value
114
167
  if result.value is not None:
@@ -124,7 +177,14 @@ async def render_alias_command(
124
177
  parts.append(body[pos:])
125
178
 
126
179
  final_command = "".join(parts)
127
- return RenderedAlias(command=final_command, stdout="".join(stdout_parts), error=error, last_value=last_value)
180
+ return RenderedAlias(
181
+ command=final_command,
182
+ stdout="".join(stdout_parts),
183
+ error=error,
184
+ last_value=last_value,
185
+ error_line=error_line,
186
+ error_col=error_col,
187
+ )
128
188
 
129
189
 
130
190
  def validate_embed_payload(payload: str) -> Tuple[bool, str | None]:
@@ -42,6 +42,8 @@ class AliasTestResult:
42
42
  embed: dict[str, Any] | None = None
43
43
  error: str | None = None
44
44
  details: str | None = None
45
+ error_line: int | None = None
46
+ error_col: int | None = None
45
47
 
46
48
 
47
49
  def discover_test_files(
@@ -159,6 +161,8 @@ async def run_alias_test(case: AliasTestCase, builder: ContextBuilder, executor:
159
161
  actual=None,
160
162
  stdout=rendered.stdout,
161
163
  error=str(rendered.error),
164
+ error_line=rendered.error_line,
165
+ error_col=rendered.error_col,
162
166
  )
163
167
 
164
168
  preview = simulate_command(rendered.command)
@@ -171,7 +175,15 @@ async def run_alias_test(case: AliasTestCase, builder: ContextBuilder, executor:
171
175
  error=preview.validation_error,
172
176
  )
173
177
 
174
- actual = preview.preview if preview.preview is not None else rendered.last_value
178
+ if preview.preview is not None:
179
+ actual = preview.preview
180
+ else:
181
+ if rendered.command.strip() == "" and rendered.last_value is None:
182
+ actual = None
183
+ elif rendered.last_value is not None and rendered.command.strip() == str(rendered.last_value):
184
+ actual = rendered.last_value
185
+ else:
186
+ actual = rendered.command
175
187
  embed_dict = preview.embed.to_dict() if preview.embed else None
176
188
 
177
189
  if embed_dict is not None and isinstance(case.expected, dict):
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import Iterable, List
5
+
6
+
7
+ def collect_target_names(targets: Iterable[ast.AST]) -> List[str]:
8
+ names: list[str] = []
9
+ for target in targets:
10
+ if isinstance(target, ast.Name):
11
+ names.append(target.id)
12
+ elif isinstance(target, (ast.Tuple, ast.List)):
13
+ names.extend(collect_target_names(target.elts))
14
+ return names
@@ -10,7 +10,8 @@ from typing import Iterable, List, Sequence
10
10
  from lsprotocol import types
11
11
 
12
12
  from .codes import MISSING_GVAR_CODE, UNDEFINED_NAME_CODE, UNSUPPORTED_IMPORT_CODE
13
- from .parser import DraconicBlock, find_draconic_blocks
13
+ from .parser import DraconicBlock
14
+ from .source_context import build_source_context
14
15
 
15
16
  log = logging.getLogger(__name__)
16
17
 
@@ -42,10 +43,13 @@ def code_actions_for_document(
42
43
  source: str,
43
44
  params: types.CodeActionParams,
44
45
  workspace_root: Path,
46
+ *,
47
+ treat_as_module: bool = False,
45
48
  ) -> List[types.CodeAction]:
46
49
  """Collect code actions for a document without requiring a running server."""
47
50
  actions: list[types.CodeAction] = []
48
- blocks = find_draconic_blocks(source)
51
+ source_ctx = build_source_context(source, treat_as_module, apply_args=False)
52
+ blocks = source_ctx.blocks
49
53
  snippets = _load_snippets(workspace_root)
50
54
  only_kinds = list(params.context.only or [])
51
55