avrae-ls 0.4.1__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,399 +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 .code_actions import code_actions_for_document
22
- from .symbols import build_symbol_table, document_symbols, find_definition_range, find_references, range_for_word
23
- from .argument_parsing import apply_argument_parsing
24
-
25
- __version__ = "0.1.0"
26
- log = logging.getLogger(__name__)
27
-
28
- RUN_ALIAS_COMMAND = "avrae.runAlias"
29
- REFRESH_CONFIG_COMMAND = "avrae.reloadConfig"
30
- REFRESH_GVARS_COMMAND = "avrae.refreshGvars"
31
- LEVEL_TO_SEVERITY = {
32
- "error": types.DiagnosticSeverity.Error,
33
- "warning": types.DiagnosticSeverity.Warning,
34
- "info": types.DiagnosticSeverity.Information,
35
- }
36
-
37
-
38
- @dataclass
39
- class ServerState:
40
- config: AvraeLSConfig
41
- context_builder: ContextBuilder
42
- diagnostics: DiagnosticProvider
43
- executor: MockExecutor
44
- warnings: list[str] = field(default_factory=list)
45
-
46
-
47
- class AvraeLanguageServer(LanguageServer):
48
- def __init__(self):
49
- super().__init__(
50
- name="avrae-ls",
51
- version=__version__,
52
- text_document_sync_kind=types.TextDocumentSyncKind.Incremental,
53
- )
54
- self._state: ServerState | None = None
55
- self._workspace_root: Path | None = None
56
- self._signatures: Dict[str, Any] = load_signatures()
57
-
58
- @property
59
- def state(self) -> ServerState:
60
- if self._state is None:
61
- raise RuntimeError("Server has not been initialized")
62
- return self._state
63
-
64
- @property
65
- def workspace_root(self) -> Path:
66
- if self._workspace_root is None:
67
- return Path.cwd()
68
- return self._workspace_root
69
-
70
- def load_workspace(self, root: Path) -> None:
71
- config, warnings = load_config(root)
72
- executor = MockExecutor(config.service)
73
- context_builder = ContextBuilder(config)
74
- diagnostics = DiagnosticProvider(executor, config.diagnostics)
75
- self._state = ServerState(
76
- config=config,
77
- context_builder=context_builder,
78
- diagnostics=diagnostics,
79
- executor=executor,
80
- warnings=list(warnings),
81
- )
82
- self._workspace_root = root
83
- log.info("Loaded workspace at %s", root)
84
-
85
-
86
- ls = AvraeLanguageServer()
87
-
88
-
89
- @ls.feature(types.INITIALIZE)
90
- def on_initialize(server: AvraeLanguageServer, params: types.InitializeParams):
91
- root_uri = params.root_uri or (params.workspace_folders[0].uri if params.workspace_folders else None)
92
- root_path = Path(uris.to_fs_path(root_uri)) if root_uri else Path.cwd()
93
- server.load_workspace(root_path)
94
-
95
-
96
- @ls.feature(types.INITIALIZED)
97
- async def on_initialized(server: AvraeLanguageServer, params: types.InitializedParams):
98
- for warning in server.state.warnings:
99
- server.window_log_message(
100
- types.LogMessageParams(type=types.MessageType.Warning, message=warning)
101
- )
102
-
103
-
104
- @ls.feature(types.TEXT_DOCUMENT_DID_OPEN)
105
- async def did_open(server: AvraeLanguageServer, params: types.DidOpenTextDocumentParams):
106
- await _publish_diagnostics(server, params.text_document.uri)
107
-
108
-
109
- @ls.feature(types.TEXT_DOCUMENT_DID_CHANGE)
110
- async def did_change(server: AvraeLanguageServer, params: types.DidChangeTextDocumentParams):
111
- await _publish_diagnostics(server, params.text_document.uri)
112
-
113
-
114
- @ls.feature(types.TEXT_DOCUMENT_DID_SAVE)
115
- async def did_save(server: AvraeLanguageServer, params: types.DidSaveTextDocumentParams):
116
- await _publish_diagnostics(server, params.text_document.uri)
117
-
118
-
119
- @ls.feature(types.WORKSPACE_DID_CHANGE_CONFIGURATION)
120
- async def did_change_config(server: AvraeLanguageServer, params: types.DidChangeConfigurationParams):
121
- server.load_workspace(server.workspace_root)
122
- for warning in server.state.warnings:
123
- server.window_log_message(
124
- types.LogMessageParams(type=types.MessageType.Warning, message=warning)
125
- )
126
-
127
-
128
- @ls.feature(types.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
129
- def on_document_symbol(server: AvraeLanguageServer, params: types.DocumentSymbolParams):
130
- doc = server.workspace.get_text_document(params.text_document.uri)
131
- symbols = document_symbols(doc.source)
132
- return symbols
133
-
134
-
135
- @ls.feature(types.TEXT_DOCUMENT_DEFINITION)
136
- def on_definition(server: AvraeLanguageServer, params: types.DefinitionParams):
137
- doc = server.workspace.get_text_document(params.text_document.uri)
138
- table = build_symbol_table(doc.source)
139
- word = doc.word_at_position(params.position)
140
- rng = find_definition_range(table, word)
141
- if rng is None:
142
- return None
143
- return types.Location(uri=params.text_document.uri, range=rng)
144
-
145
-
146
- @ls.feature(types.TEXT_DOCUMENT_REFERENCES)
147
- def on_references(server: AvraeLanguageServer, params: types.ReferenceParams):
148
- doc = server.workspace.get_text_document(params.text_document.uri)
149
- table = build_symbol_table(doc.source)
150
- word = doc.word_at_position(params.position)
151
- if not word or not table.lookup(word):
152
- return []
153
-
154
- ranges = find_references(table, doc.source, word, include_declaration=params.context.include_declaration)
155
- return [types.Location(uri=params.text_document.uri, range=rng) for rng in ranges]
156
-
157
-
158
- @ls.feature(types.TEXT_DOCUMENT_PREPARE_RENAME)
159
- def on_prepare_rename(server: AvraeLanguageServer, params: types.PrepareRenameParams):
160
- doc = server.workspace.get_text_document(params.text_document.uri)
161
- table = build_symbol_table(doc.source)
162
- word = doc.word_at_position(params.position)
163
- if not word or not table.lookup(word):
164
- return None
165
- return range_for_word(doc.source, params.position)
166
-
167
-
168
- @ls.feature(types.TEXT_DOCUMENT_RENAME)
169
- def on_rename(server: AvraeLanguageServer, params: types.RenameParams):
170
- doc = server.workspace.get_text_document(params.text_document.uri)
171
- table = build_symbol_table(doc.source)
172
- word = doc.word_at_position(params.position)
173
- if not word or not table.lookup(word) or not params.new_name:
174
- return None
175
-
176
- ranges = find_references(table, doc.source, word, include_declaration=True)
177
- if not ranges:
178
- return None
179
- edits = [types.TextEdit(range=rng, new_text=params.new_name) for rng in ranges]
180
- return types.WorkspaceEdit(changes={params.text_document.uri: edits})
181
-
182
-
183
- @ls.feature(types.WORKSPACE_SYMBOL)
184
- def on_workspace_symbol(server: AvraeLanguageServer, params: types.WorkspaceSymbolParams):
185
- symbols: list[types.SymbolInformation] = []
186
- query = (params.query or "").lower()
187
- for uri, doc in server.workspace.text_documents.items():
188
- table = build_symbol_table(doc.source)
189
- for entry in table.entries:
190
- if query and query not in entry.name.lower():
191
- continue
192
- symbols.append(
193
- types.SymbolInformation(
194
- name=entry.name,
195
- kind=entry.kind,
196
- location=types.Location(uri=uri, range=entry.range),
197
- )
198
- )
199
- return symbols
200
-
201
-
202
- @ls.feature(types.TEXT_DOCUMENT_SIGNATURE_HELP)
203
- def on_signature_help(server: AvraeLanguageServer, params: types.SignatureHelpParams):
204
- doc = server.workspace.get_text_document(params.text_document.uri)
205
- source = apply_argument_parsing(doc.source)
206
- blocks = find_draconic_blocks(source)
207
- pos = params.position
208
- if not blocks:
209
- return signature_help_for_code(source, pos.line, pos.character, server._signatures)
210
-
211
- for block in blocks:
212
- start = block.line_offset
213
- end = block.line_offset + block.line_count
214
- if start <= pos.line <= end:
215
- rel_line = pos.line - start
216
- help_ = signature_help_for_code(block.code, rel_line, pos.character, server._signatures)
217
- if help_:
218
- return help_
219
- return None
220
-
221
-
222
- @ls.feature(
223
- types.TEXT_DOCUMENT_COMPLETION,
224
- types.CompletionOptions(trigger_characters=["."]),
225
- )
226
- def on_completion(server: AvraeLanguageServer, params: types.CompletionParams):
227
- doc = server.workspace.get_text_document(params.text_document.uri)
228
- ctx_data = server.state.context_builder.build()
229
- suggestions = gather_suggestions(ctx_data, server.state.context_builder.gvar_resolver, server._signatures)
230
- source = apply_argument_parsing(doc.source)
231
- blocks = find_draconic_blocks(source)
232
- pos = params.position
233
- if not blocks:
234
- return completion_items_for_position(source, pos.line, pos.character, suggestions)
235
-
236
- for block in blocks:
237
- start = block.line_offset
238
- end = block.line_offset + block.line_count
239
- if start <= pos.line <= end:
240
- rel_line = pos.line - start
241
- return completion_items_for_position(block.code, rel_line, pos.character, suggestions)
242
- return []
243
-
244
-
245
- @ls.feature(types.TEXT_DOCUMENT_HOVER)
246
- def on_hover(server: AvraeLanguageServer, params: types.HoverParams):
247
- doc = server.workspace.get_text_document(params.text_document.uri)
248
- ctx_data = server.state.context_builder.build()
249
- pos = params.position
250
- source = apply_argument_parsing(doc.source)
251
- blocks = find_draconic_blocks(source)
252
- if not blocks:
253
- return hover_for_position(source, pos.line, pos.character, server._signatures, ctx_data, server.state.context_builder.gvar_resolver)
254
-
255
- for block in blocks:
256
- start = block.line_offset
257
- end = block.line_offset + block.line_count
258
- if start <= pos.line <= end:
259
- rel_line = pos.line - start
260
- return hover_for_position(block.code, rel_line, pos.character, server._signatures, ctx_data, server.state.context_builder.gvar_resolver)
261
- return None
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
-
270
- @ls.command(RUN_ALIAS_COMMAND)
271
- async def run_alias(server: AvraeLanguageServer, *args: Any):
272
- payload = args[0] if args else {}
273
- uri = None
274
- text = None
275
- profile = None
276
- alias_args: list[str] | None = None
277
- if isinstance(payload, dict):
278
- uri = payload.get("uri")
279
- text = payload.get("text")
280
- profile = payload.get("profile")
281
- if isinstance(payload.get("args"), list):
282
- alias_args = [str(a) for a in payload["args"]]
283
-
284
- if text is None and uri:
285
- doc = server.workspace.get_text_document(uri)
286
- text = doc.source
287
-
288
- if text is None:
289
- return {"error": "No alias content supplied"}
290
-
291
- ctx_data = server.state.context_builder.build(profile)
292
- rendered = await render_alias_command(
293
- text,
294
- server.state.executor,
295
- ctx_data,
296
- server.state.context_builder.gvar_resolver,
297
- args=alias_args,
298
- )
299
- preview = simulate_command(rendered.command)
300
-
301
- response: dict[str, Any] = {
302
- "stdout": rendered.stdout,
303
- "result": preview.preview if preview.preview is not None else rendered.last_value,
304
- "command": rendered.command,
305
- "commandName": preview.command_name,
306
- }
307
- if rendered.error:
308
- response["error"] = _format_runtime_error(rendered.error)
309
- if preview.validation_error:
310
- response["validationError"] = preview.validation_error
311
- if preview.embed:
312
- response["embed"] = preview.embed.to_dict()
313
- if uri:
314
- extra = []
315
- if rendered.error:
316
- extra.append(_runtime_diagnostic(rendered.error, server.state.config.diagnostics.runtime_level))
317
- await _publish_diagnostics(server, uri, profile=profile, extra=extra)
318
- return response
319
-
320
-
321
- @ls.command(REFRESH_GVARS_COMMAND)
322
- async def refresh_gvars(server: AvraeLanguageServer, *args: Any):
323
- payload = args[0] if args else {}
324
- profile = None
325
- keys: list[str] | None = None
326
- if isinstance(payload, dict):
327
- profile = payload.get("profile")
328
- raw_keys = payload.get("keys")
329
- if isinstance(raw_keys, list):
330
- keys = [str(k) for k in raw_keys]
331
-
332
- ctx_data = server.state.context_builder.build(profile)
333
- resolver = server.state.context_builder.gvar_resolver
334
- snapshot = await resolver.refresh(ctx_data.vars.gvars, keys)
335
- return {"count": len(snapshot), "gvars": snapshot}
336
-
337
-
338
- @ls.command(REFRESH_CONFIG_COMMAND)
339
- def reload_config(server: AvraeLanguageServer, *args: Any):
340
- server.load_workspace(server.workspace_root)
341
- return {"status": "ok"}
342
-
343
-
344
- async def _publish_diagnostics(
345
- server: AvraeLanguageServer,
346
- uri: str,
347
- profile: str | None = None,
348
- extra: list[types.Diagnostic] | None = None,
349
- ) -> None:
350
- doc = server.workspace.get_text_document(uri)
351
- ctx_data = server.state.context_builder.build(profile)
352
- diags = await server.state.diagnostics.analyze(
353
- doc.source, ctx_data, server.state.context_builder.gvar_resolver
354
- )
355
- if extra:
356
- diags.extend(extra)
357
- server.text_document_publish_diagnostics(
358
- types.PublishDiagnosticsParams(uri=uri, diagnostics=diags, version=doc.version)
359
- )
360
-
361
-
362
- def _format_runtime_error(error: BaseException) -> str:
363
- if isinstance(error, draconic.DraconicException):
364
- return error.msg
365
- return str(error)
366
-
367
-
368
- def _runtime_diagnostic(error: BaseException, level: str) -> types.Diagnostic:
369
- severity = LEVEL_TO_SEVERITY.get(level.lower(), types.DiagnosticSeverity.Error)
370
- if isinstance(error, draconic.DraconicSyntaxError):
371
- rng = types.Range(
372
- start=types.Position(line=max((error.lineno or 1) - 1, 0), character=max((error.offset or 1) - 1, 0)),
373
- end=types.Position(
374
- line=max(((error.end_lineno or error.lineno or 1) - 1), 0),
375
- character=max(((error.end_offset or error.offset or 1) - 1), 0),
376
- ),
377
- )
378
- message = error.msg
379
- elif hasattr(error, "node"):
380
- node = getattr(error, "node")
381
- rng = types.Range(
382
- start=types.Position(line=max(getattr(node, "lineno", 1) - 1, 0), character=max(getattr(node, "col_offset", 0), 0)),
383
- end=types.Position(
384
- line=max(getattr(node, "end_lineno", getattr(node, "lineno", 1)) - 1, 0),
385
- character=max(getattr(node, "end_col_offset", getattr(node, "col_offset", 0) + 1), 0),
386
- ),
387
- )
388
- message = getattr(error, "msg", str(error))
389
- else:
390
- rng = types.Range(
391
- start=types.Position(line=0, character=0),
392
- end=types.Position(line=0, character=1),
393
- )
394
- message = str(error)
395
- return types.Diagnostic(message=message, range=rng, severity=severity, source="avrae-ls-runtime")
396
-
397
-
398
- def create_server() -> AvraeLanguageServer:
399
- return ls
@@ -1,252 +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
- 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
168
- if hasattr(node, "lineno") and hasattr(node, "col_offset"):
169
- start = (node.lineno - 1, node.col_offset)
170
- end_line = getattr(node, "end_lineno", node.lineno) - 1
171
- end_col = getattr(node, "end_col_offset", node.col_offset)
172
- if _pos_within((line, character), start, (end_line, end_col)):
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)
183
-
184
- if not target_call:
185
- return None
186
-
187
- if isinstance(target_call.func, ast.Name):
188
- name = target_call.func.id
189
- else:
190
- return None
191
-
192
- if name not in sigs:
193
- return None
194
-
195
- fsig = sigs[name]
196
- sig_info = types.SignatureInformation(
197
- label=fsig.label,
198
- documentation=fsig.doc,
199
- parameters=[types.ParameterInformation(label=p) for p in fsig.params],
200
- )
201
- active_param = _active_param_index(target_call, (line, character), fsig.params)
202
- return types.SignatureHelp(signatures=[sig_info], active_signature=0, active_parameter=active_param)
203
-
204
-
205
- def _pos_within(pos: Tuple[int, int], start: Tuple[int, int], end: Tuple[int, int]) -> bool:
206
- (line, col) = pos
207
- (sl, sc) = start
208
- (el, ec) = end
209
- if line < sl or line > el:
210
- return False
211
- if line == sl and col < sc:
212
- return False
213
- if line == el and col > ec:
214
- return False
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
- )