diff --git a/CHANGELOG.md b/CHANGELOG.md index 2760d1c..39d63d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ Versions correspond to the Git tags of this repository. ## Unreleased +### Added + +- Go to Definition from block template and address arguments in Tcl (`MOM_do_template`, `MOM_force`, `MOM_suppress`, `MOM_ask_address_value`, ...) to the `BLOCK_TEMPLATE`/`ADDRESS` declaration in the PSC `.def` files +- Hover over block templates shows the template body; hover over addresses shows format, leader, trailer, min/max, and modality +- Find References and Rename for block templates and addresses across Tcl and `.def` files, including addresses used inside block templates +- Go to Definition, hover, references, and rename also work inside `.def` files + ### Documentation - Document DEF block templates, addresses, `BLOCK_LIST`/`ADDR_LIST`, and the formatting changes in the README diff --git a/README.md b/README.md index ac69ed7..1db76f2 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,26 @@ Select the keyword from the completion list (or type it completely) to open the directly after the keyword, such as `ADDR_LISTSP`, narrows it. The keyword is replaced by the selected quoted name. +### Navigation between Tcl and DEF files + +Block template and address names are linked to their declarations in the PSC `.def` files: + +- **Go to Definition** (F12) on `MOM_do_template "steady_rest"` or `MOM_force Once X` jumps to + `BLOCK_TEMPLATE steady_rest` or `ADDRESS X`. Inside a `.def` file, an address used in a block + template (`X[$mom_pos(0)]`) jumps to its `ADDRESS` declaration. +- **Hover** shows the body of a block template, or the format (resolved to its `FORMAT` + definition), leader, trailer, min/max, and modality (`FORCE`) of an address. +- **Find All References** (Shift+F12) lists the declaration, all Tcl calls, and all block templates + that use an address. +- **Rename** (F2) changes the declaration and all usages in Tcl and `.def` files together. Only + names declared in a loaded `.def` file can be renamed. + +Recognized Tcl commands: `MOM_do_template`, `MOM_add_to_block_buffer`, `MOM_polar_motion`, +`MOM_force_block`, `MOM_ask_address_value`, `MOM_add_to_address_buffer`, `MOM_enable_address`, +`MOM_disable_address`, `MOM_force`, `MOM_suppress`, `MOM_incremental`, and +`MOM_ask_definition_element`/`MOM_has_definition_element` with `ADDRESS` or `BLOCK`. Names built +from variables (`MOM_do_template $name`) cannot be resolved statically. + ## NX Tcl Remote Debugger ### Add a VS Code attach configuration @@ -133,6 +153,7 @@ Simply open any supported file type and enjoy: - Context-aware completion with local symbols ranked before workspace and built-in symbols, plus semantic arguments, local paths, Tcl subcommands, and options such as `string compare -nocase` - Placeholder-based snippets for common Tcl control structures and procedures - Block template and address suggestions from PSC DEF files, including `BLOCK_LIST` and `ADDR_LIST` +- Go to Definition, hover, references, and rename for block templates and addresses across Tcl and DEF files - Remote NX Tcl debugging with breakpoints and full stepping ## Contributing diff --git a/client/src/common/defProviders.ts b/client/src/common/defProviders.ts new file mode 100644 index 0000000..33b869f --- /dev/null +++ b/client/src/common/defProviders.ts @@ -0,0 +1,81 @@ +import * as vscode from "vscode" +import { LanguageClient, Range, State } from "vscode-languageclient/node" + +// .def files are not synchronized with the language server, so each request +// carries the current text of the document. +const DEF_SELECTOR: vscode.DocumentSelector = { scheme: "file", language: "def" } + +function params(document: vscode.TextDocument, position: vscode.Position, extra: object = {}) { + return { + textDocument: { uri: document.uri.toString() }, + position: { line: position.line, character: position.character }, + text: document.getText(), + ...extra + } +} + +export function registerDefProviders(getClient: () => LanguageClient | undefined): vscode.Disposable[] { + const request = async ( + method: string, + document: vscode.TextDocument, + position: vscode.Position, + token: vscode.CancellationToken, + extra: object = {} + ): Promise<{ client: LanguageClient; result: T } | undefined> => { + const client = getClient() + if (!client || client.state !== State.Running) { + return undefined + } + const result = await client.sendRequest(method, params(document, position, extra), token) + return result ? { client, result } : undefined + } + + return [ + vscode.languages.registerDefinitionProvider(DEF_SELECTOR, { + async provideDefinition(document, position, token) { + const response = await request("nxPostSupport/def/definition", document, position, token) + return response && response.client.protocol2CodeConverter.asDefinitionResult(response.result, token) + } + }), + vscode.languages.registerHoverProvider(DEF_SELECTOR, { + async provideHover(document, position, token) { + const response = await request("nxPostSupport/def/hover", document, position, token) + return response && response.client.protocol2CodeConverter.asHover(response.result) + } + }), + vscode.languages.registerReferenceProvider(DEF_SELECTOR, { + async provideReferences(document, position, context, token) { + const response = await request("nxPostSupport/def/references", document, position, token, { + includeDeclaration: context.includeDeclaration + }) + return response && response.client.protocol2CodeConverter.asReferences(response.result, token) + } + }), + vscode.languages.registerRenameProvider(DEF_SELECTOR, { + async prepareRename(document, position, token) { + const response = await request<{ range: Range; placeholder: string }>( + "nxPostSupport/def/prepareRename", + document, + position, + token + ) + if (!response) { + throw new Error("Only declared block templates and addresses can be renamed.") + } + return { + range: response.client.protocol2CodeConverter.asRange(response.result.range), + placeholder: response.result.placeholder + } + }, + async provideRenameEdits(document, position, newName, token) { + const response = await request("nxPostSupport/def/rename", document, position, token, { + newName + }) + if (!response) { + throw new Error(`"${newName}" is not a valid block template or address name.`) + } + return response.client.protocol2CodeConverter.asWorkspaceEdit(response.result, token) + } + }) + ] +} diff --git a/client/src/extension.ts b/client/src/extension.ts index 6876c00..9454858 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -16,6 +16,7 @@ import { defDocumentSymbolProvider, definitionCdlEventHandler } from "./common/handlers" +import { registerDefProviders } from "./common/defProviders" import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging" import { checkVersion, @@ -234,6 +235,7 @@ export async function activate(context: vscode.ExtensionContext) { ) context.subscriptions.push(formatDefProvider) + context.subscriptions.push(...registerDefProviders(() => client)) const cdlSymbolProvider = vscode.languages.registerDocumentSymbolProvider( { scheme: "file", language: "cdl" }, diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 08c1cbc..778365d 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -52,6 +52,15 @@ from tools.completion_items import ( completion_context, ranked_completion_items, ) +from tools.def_navigation import ( + all_def_target_locations, + def_declarations, + def_definition_locations, + def_hover_markdown, + def_rename_edits, + def_symbol_at, +) +from tools.def_symbols import parse_def_document from tools.folding_ranges import build_folding_ranges from tools.index_cache import IndexCache from tools.inlay_hint import ( @@ -59,6 +68,7 @@ from tools.inlay_hint import ( build_builtin_inlay_signatures, ) from tools.navigation import ( + DEF_SYMBOL_KINDS, SymbolIdentity, call_hierarchy_identity, call_hierarchy_items, @@ -295,6 +305,7 @@ SYMBOL_LIST_KEYWORDS = { "BLOCK_LIST": (lambda: LSP_SERVER.block_template_items(), "block templates"), "ADDR_LIST": (lambda: LSP_SERVER.address_items(), "addresses"), } +DEF_NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") _WORD_BEFORE_CURSOR_RE = re.compile(r"(? lsp.Hover: else: return None + def_hover = _tcl_def_hover(document_uri, pos, token) + if def_hover is not None: + return def_hover + # 1) If token is a known MOM proc/variable, return built-in hover match = BUILTIN_HOVER_ITEMS.get(token) if match and match.get("kind") == "function": @@ -748,6 +763,22 @@ def hover(params: lsp.HoverParams) -> lsp.Hover: return None +def _def_target(identity: SymbolIdentity) -> tuple[str, str] | None: + return (identity.kind, identity.name) if identity.kind in DEF_SYMBOL_KINDS else None + + +def _tcl_def_hover(uri: str, position: lsp.Position, token: str) -> lsp.Hover | None: + documents = LSP_SERVER.def_documents_snapshot() + if not any(token in document.names(kind) for document in documents.values() for kind in DEF_SYMBOL_KINDS): + return None + context = _navigation_context(uri, position) + target = context and _def_target(context[3]) + markdown = target and def_hover_markdown(documents, target) + if not markdown: + return None + return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=markdown), range=context[2].range) + + @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DEFINITION) def goto_definition(params: lsp.DefinitionParams): """Resolve TclOO declarations, then indexed proc and variable definitions.""" @@ -770,6 +801,9 @@ def goto_definition(params: lsp.DefinitionParams): return None indexes, definitions, _, identity = context + target = _def_target(identity) + if target is not None: + return _sorted_locations(def_definition_locations(LSP_SERVER.def_documents_snapshot(), target)) or None locations = [lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in matching_occurrences(identity, indexes, definitions) if occurrence.is_definition] return _sorted_locations(locations) or None @@ -811,6 +845,11 @@ def references(params: lsp.ReferenceParams) -> list[lsp.Location]: return [] indexes, definitions, _, identity = context + target = _def_target(identity) + if target is not None: + return all_def_target_locations( + LSP_SERVER.def_documents_snapshot(), indexes, target, params.context.include_declaration + ) locations = [ lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in matching_occurrences(identity, indexes, definitions) @@ -858,7 +897,11 @@ def prepare_rename(params: lsp.PrepareRenameParams): return None indexes, definitions, occurrence, identity = context - if not _is_renamable(identity, indexes, definitions): + target = _def_target(identity) + if target is not None: + if not def_declarations(LSP_SERVER.def_documents_snapshot(), target): + return None + elif not _is_renamable(identity, indexes, definitions): return None return lsp.PrepareRenamePlaceholder(range=occurrence.range, placeholder=occurrence.placeholder) @@ -868,7 +911,7 @@ def prepare_rename(params: lsp.PrepareRenameParams): lsp.RenameOptions(prepare_provider=True), ) def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None: - if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", params.new_name): + if not DEF_NAME_RE.fullmatch(params.new_name): return None context = _navigation_context(params.text_document.uri, params.position) @@ -876,6 +919,9 @@ def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None: return None indexes, definitions, _, identity = context + target = _def_target(identity) + if target is not None: + return def_rename_edits(LSP_SERVER.def_documents_snapshot(), indexes, target, params.new_name) if not _is_renamable(identity, indexes, definitions): return None @@ -905,6 +951,83 @@ def rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None: return lsp.WorkspaceEdit(changes=changes) +# .def files are not synchronized with the server; the client sends their +# current text with each request. +DEF_REQUEST_DEFINITION = "nxPostSupport/def/definition" +DEF_REQUEST_HOVER = "nxPostSupport/def/hover" +DEF_REQUEST_REFERENCES = "nxPostSupport/def/references" +DEF_REQUEST_PREPARE_RENAME = "nxPostSupport/def/prepareRename" +DEF_REQUEST_RENAME = "nxPostSupport/def/rename" + + +def _def_request_context(params): + uri = params.textDocument.uri + position = lsp.Position(line=params.position.line, character=params.position.character) + path = uris.to_fs_path(uri) + documents = LSP_SERVER.def_documents_snapshot(path, params.text) + symbol = def_symbol_at(parse_def_document(params.text), position) + if symbol is None: + return None + target, range_, _ = symbol + current = next(key for key in documents if LSP_SERVER.paths_equal(key, path)) + return documents, {current: uri}, target, range_ + + +@LSP_SERVER.feature(DEF_REQUEST_DEFINITION) +def def_definition(params) -> list[lsp.Location] | None: + context = _def_request_context(params) + if context is None: + return None + documents, def_uris, target, _ = context + return _sorted_locations(def_definition_locations(documents, target, def_uris)) or None + + +@LSP_SERVER.feature(DEF_REQUEST_HOVER) +def def_hover(params) -> lsp.Hover | None: + context = _def_request_context(params) + if context is None: + return None + documents, _, target, range_ = context + markdown = def_hover_markdown(documents, target) + if markdown is None: + return None + return lsp.Hover(lsp.MarkupContent(kind=lsp.MarkupKind.Markdown, value=markdown), range=range_) + + +@LSP_SERVER.feature(DEF_REQUEST_REFERENCES) +def def_references(params) -> list[lsp.Location]: + context = _def_request_context(params) + if context is None: + return [] + documents, def_uris, target, _ = context + include_declaration = bool(getattr(params, "includeDeclaration", True)) + indexes, _ = LSP_SERVER.navigation_state() + return all_def_target_locations(documents, indexes, target, include_declaration, def_uris) + + +@LSP_SERVER.feature(DEF_REQUEST_PREPARE_RENAME) +def def_prepare_rename(params): + context = _def_request_context(params) + if context is None: + return None + documents, _, target, range_ = context + if not def_declarations(documents, target): + return None + return lsp.PrepareRenamePlaceholder(range=range_, placeholder=target[1]) + + +@LSP_SERVER.feature(DEF_REQUEST_RENAME) +def def_rename(params) -> lsp.WorkspaceEdit | None: + if not DEF_NAME_RE.fullmatch(params.newName): + return None + context = _def_request_context(params) + if context is None: + return None + documents, def_uris, target, _ = context + indexes, _ = LSP_SERVER.navigation_state() + return def_rename_edits(documents, indexes, target, params.newName, def_uris) + + @LSP_SERVER.feature(lsp.WORKSPACE_SYMBOL) def workspace_symbol(params: lsp.WorkspaceSymbolParams): return workspace_symbols(LSP_SERVER.navigation_snapshot(), params.query) diff --git a/server/src/lsp_tclserver.py b/server/src/lsp_tclserver.py index 81bd5a8..15489cc 100644 --- a/server/src/lsp_tclserver.py +++ b/server/src/lsp_tclserver.py @@ -18,7 +18,7 @@ from tools import checks, incremental_parse, parser from tools.completion_items import CompletionCollector from tools.tcloo_symbols import class_completion_items from tools.tcloo_completion import indexed_classes -from tools.def_symbols import DefSymbols, read_def_symbols +from tools.def_symbols import ADDRESS, BLOCK_TEMPLATE, DefDocument, parse_def_document, read_def_source from tools.file_sourcing import get_all_psc_files, psc_defined_event_files, psc_script_files from tools.formatter import NxFormatter as Formatter from tools.index_cache import FileStat, IndexCache, file_stat @@ -62,8 +62,8 @@ class TclLanguageServer(LanguageServer): self.psc_script_paths: list[str] = [] self._psc_files: dict[str, list[pathlib.Path]] = {} self._psc_lock = threading.RLock() - # .def path -> BLOCK_TEMPLATE/ADDRESS names, in PSC DefinedEvents order. - self.def_symbols: dict[str, DefSymbols] = {} + # .def path -> parsed declarations, in PSC DefinedEvents order. + self.def_documents: dict[str, DefDocument] = {} self.navigation_indexes: dict[str, FileSymbolIndex] = {} self.variable_indexes: dict[ str, @@ -257,7 +257,7 @@ class TclLanguageServer(LanguageServer): def refresh_def_symbols(self, roots, report=LOGGER.warning): """Read the block templates and addresses of all .def files listed as PSC DefinedEvents.""" - symbols: dict[str, DefSymbols] = {} + documents: dict[str, DefDocument] = {} for root in roots: for psc in get_all_psc_files(root): try: @@ -266,33 +266,40 @@ class TclLanguageServer(LanguageServer): report(f"Could not read PSC {psc}: {error}") continue for def_file in def_files: - if str(def_file) in symbols: + if str(def_file) in documents: continue try: - symbols[str(def_file)] = read_def_symbols(def_file) + documents[str(def_file)] = parse_def_document(read_def_source(def_file)) except OSError as error: report(f"Could not read DEF file {def_file}: {error}") with self._index_lock: - self.def_symbols = symbols + self.def_documents = documents - def _def_symbol_items(self, attribute: str, kind, description: str) -> list[lsp.CompletionItem]: + def def_documents_snapshot(self, current_path=None, current_source: str | None = None) -> dict[str, DefDocument]: + """Return the PSC .def documents; ``current_source`` replaces the file being edited.""" with self._index_lock: - symbols = dict(self.def_symbols) + documents = dict(self.def_documents) + if current_path is not None and current_source is not None: + key = next((path for path in documents if self.paths_equal(path, current_path)), os.fspath(current_path)) + documents[key] = parse_def_document(current_source) + return documents + + def _def_symbol_items(self, kind: str, item_kind, description: str) -> list[lsp.CompletionItem]: return [ lsp.CompletionItem( label=name, - kind=kind, + kind=item_kind, detail=f"{description} ({pathlib.Path(path).name})", ) - for path, def_symbols in symbols.items() - for name in getattr(def_symbols, attribute) + for path, document in self.def_documents_snapshot().items() + for name in document.names(kind) ] def block_template_items(self) -> list[lsp.CompletionItem]: - return self._def_symbol_items("block_templates", lsp.CompletionItemKind.Struct, "Block template") + return self._def_symbol_items(BLOCK_TEMPLATE, lsp.CompletionItemKind.Struct, "Block template") def address_items(self) -> list[lsp.CompletionItem]: - return self._def_symbol_items("addresses", lsp.CompletionItemKind.Field, "Address") + return self._def_symbol_items(ADDRESS, lsp.CompletionItemKind.Field, "Address") def refresh_psc_scripts(self, roots, report=LOGGER.warning): """Index PSC dependencies through the same pipeline as workspace procs.""" diff --git a/server/src/tools/def_symbols.py b/server/src/tools/def_symbols.py index 8e48b9c..9fbbea6 100644 --- a/server/src/tools/def_symbols.py +++ b/server/src/tools/def_symbols.py @@ -1,11 +1,53 @@ -"""Block templates and addresses declared in NX post definition (.def) files.""" +"""Block templates, addresses and formats declared in NX post definition (.def) files.""" import re -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path -_BLOCK_TEMPLATE_RE = re.compile(r"^\s*BLOCK_TEMPLATE\s+([^\s{]+)", re.MULTILINE) -_ADDRESS_RE = re.compile(r"^\s*ADDRESS\s+([^\s{]+)", re.MULTILINE) +BLOCK_TEMPLATE = "block_template" +ADDRESS = "address" +FORMAT = "format" + +_KINDS = {"BLOCK_TEMPLATE": BLOCK_TEMPLATE, "ADDRESS": ADDRESS, "FORMAT": FORMAT} +_HEADER_RE = re.compile(r"^\s*(BLOCK_TEMPLATE|ADDRESS|FORMAT)\s+([^\s{]+)") +# A block template element is an address followed by its expression: X[$mom_pos(0)]. +_ELEMENT_RE = re.compile(r"^\s*([A-Za-z_]\w*)\[") +_PROPERTY_RE = re.compile(r"^\s*([A-Za-z_]+)\s*(.*?)\s*$") + + +@dataclass(frozen=True) +class DefDeclaration: + kind: str + name: str + # 0-based line and UTF-16 columns of the name. + line: int + start: int + end: int + end_line: int + text: str + # ADDRESS: body properties such as ("LEADER", '"X"'); FORMAT: (("FORMAT", '"%d"'),). + properties: tuple[tuple[str, str], ...] = () + + +@dataclass(frozen=True) +class DefReference: + """An address used as element of a block template.""" + + kind: str + name: str + line: int + start: int + end: int + container: str + + +@dataclass(frozen=True) +class DefDocument: + declarations: tuple[DefDeclaration, ...] = () + references: tuple[DefReference, ...] = () + + def names(self, kind: str) -> tuple[str, ...]: + return tuple(dict.fromkeys(item.name for item in self.declarations if item.kind == kind)) @dataclass(frozen=True) @@ -14,23 +56,125 @@ class DefSymbols: addresses: tuple[str, ...] = () -def _names(pattern: re.Pattern, source: str) -> tuple[str, ...]: - return tuple(dict.fromkeys(pattern.findall(source))) +def _utf16_length(text: str) -> int: + return len(text.encode("utf-16-le")) // 2 + + +def _is_comment(line: str) -> bool: + return line.lstrip().startswith("#") + + +def _body(lines: list[str], header: int, rest: str) -> tuple[int, int, int] | None: + """Return (first body line, first body column, closing line) of a braced body. + + The body starts after the first "{" on the header line or on a following + line and ends at the next line starting with "}". + """ + if "{" in rest: + after = rest.split("{", 1)[1] + if "}" in after: + return None + start = header + 1 + else: + start = header + 1 + while start < len(lines) and (not lines[start].strip() or _is_comment(lines[start])): + start += 1 + if start >= len(lines) or not lines[start].lstrip().startswith("{"): + return None + start += 1 + end = start + while end < len(lines) and not lines[end].lstrip().startswith("}"): + end += 1 + return start, 0, min(end, len(lines) - 1) + + +def parse_def_document(source: str) -> DefDocument: + """Parse the block templates, addresses and formats of a .def source.""" + lines = source.splitlines() + declarations: list[DefDeclaration] = [] + references: list[DefReference] = [] + index = 0 + while index < len(lines): + line = lines[index] + match = None if _is_comment(line) else _HEADER_RE.match(line) + if match is None: + index += 1 + continue + + kind = _KINDS[match.group(1)] + name = match.group(2) + start = _utf16_length(line[: match.start(2)]) + end = start + _utf16_length(name) + rest = line[match.end(2) :] + + if kind == FORMAT: + declarations.append( + DefDeclaration(kind, name, index, start, end, index, line.strip(), (("FORMAT", rest.strip()),)) + ) + index += 1 + continue + + body = _body(lines, index, rest) + end_line = index if body is None else body[2] + properties: list[tuple[str, str]] = [] + if body is not None: + for number in range(body[0], body[2]): + body_line = lines[number] + if _is_comment(body_line) or not body_line.strip(): + continue + if kind == BLOCK_TEMPLATE: + element = _ELEMENT_RE.match(body_line) + if element is not None: + element_start = _utf16_length(body_line[: element.start(1)]) + references.append( + DefReference( + ADDRESS, + element.group(1), + number, + element_start, + element_start + _utf16_length(element.group(1)), + name, + ) + ) + else: + prop = _PROPERTY_RE.match(body_line) + if prop is not None: + properties.append((prop.group(1).upper(), prop.group(2))) + + declarations.append( + DefDeclaration( + kind, + name, + index, + start, + end, + end_line, + "\n".join(lines[index : end_line + 1]), + tuple(properties), + ) + ) + index = end_line + 1 + + return DefDocument(tuple(declarations), tuple(references)) def parse_def_symbols(source: str) -> DefSymbols: """Return the BLOCK_TEMPLATE and ADDRESS names of a .def source in declaration order.""" + document = parse_def_document(source) return DefSymbols( - block_templates=_names(_BLOCK_TEMPLATE_RE, source), - addresses=_names(_ADDRESS_RE, source), + block_templates=document.names(BLOCK_TEMPLATE), + addresses=document.names(ADDRESS), ) -def read_def_symbols(path: Path) -> DefSymbols: +def read_def_source(path: Path) -> str: data = path.read_bytes() try: - source = data.decode("utf-8-sig") + return data.decode("utf-8-sig") except UnicodeDecodeError: # Older Windows NX layers use the ANSI code page. - source = data.decode("cp1252") - return parse_def_symbols(source) + return data.decode("cp1252") + + +def read_def_symbols(path: Path) -> DefSymbols: + return parse_def_symbols(read_def_source(path)) diff --git a/server/src/tools/navigation.py b/server/src/tools/navigation.py index fb5fd52..09a4758 100644 --- a/server/src/tools/navigation.py +++ b/server/src/tools/navigation.py @@ -114,6 +114,49 @@ def _name_range(node: Node, raw_name: str, *, variable_sub: bool = False) -> lsp ) +DEF_BLOCK_TEMPLATE = "block_template" +DEF_ADDRESS = "address" +DEF_SYMBOL_KINDS = frozenset({DEF_BLOCK_TEMPLATE, DEF_ADDRESS}) + +# NX commands taking .def names: command -> (first argument index, last index or +# None for all following arguments, kind). +_DEF_ARGUMENTS: dict[str, tuple[tuple[int, int | None, str], ...]] = { + "MOM_do_template": ((0, 0, DEF_BLOCK_TEMPLATE),), + "MOM_add_to_block_buffer": ((0, 0, DEF_BLOCK_TEMPLATE),), + "MOM_polar_motion": ((0, 0, DEF_BLOCK_TEMPLATE),), + "MOM_force_block": ((1, None, DEF_BLOCK_TEMPLATE),), + "MOM_ask_address_value": ((0, 0, DEF_ADDRESS),), + "MOM_add_to_address_buffer": ((0, 0, DEF_ADDRESS),), + "MOM_enable_address": ((0, None, DEF_ADDRESS),), + "MOM_disable_address": ((0, None, DEF_ADDRESS),), + "MOM_force": ((1, None, DEF_ADDRESS),), + "MOM_suppress": ((1, None, DEF_ADDRESS),), + "MOM_incremental": ((1, None, DEF_ADDRESS),), +} +_DEFINITION_ELEMENT_KINDS = {"ADDRESS": DEF_ADDRESS, "BLOCK": DEF_BLOCK_TEMPLATE} + + +def def_argument_kinds(command: Command) -> list[tuple[Node, str]]: + """Return the arguments of ``command`` that name a .def block template or address.""" + routine = _static_contents(command.routine) + if routine in {"MOM_ask_definition_element", "MOM_has_definition_element"}: + kind = _DEFINITION_ELEMENT_KINDS.get((_static_contents(command.args[0]) or "").upper()) if command.args else None + return [(command.args[1], kind)] if kind and len(command.args) >= 2 else [] + result = [] + for first, last, kind in _DEF_ARGUMENTS.get(routine or "", ()): + for position, argument in enumerate(command.args): + if position >= first and (last is None or position <= last): + result.append((argument, kind)) + return result + + +def _def_name(node: Node) -> str | None: + name = _static_contents(node) + if not name or node.contents_pos is None or any(char.isspace() or char in "$[]{}\\\"" for char in name): + return None + return name + + def _node_range(node: Node) -> lsp.Range | None: if node.pos is None or node.end_pos is None: return None @@ -436,6 +479,23 @@ def build_file_symbol_index( if routine: add_proc(command.routine, routine, scope, is_definition=False) + for node, kind in def_argument_kinds(command): + name = _def_name(node) + if name is None: + continue + line, column = node.contents_pos + occurrences.append( + SymbolOccurrence( + identity=shared(SymbolIdentity(kind=kind, name=name)), + range=lsp.Range( + start=lsp.Position(line=line - 1, character=column - 1), + end=lsp.Position(line=line - 1, character=column - 1 + len(name)), + ), + placeholder=name, + symbol_kind=lsp.SymbolKind.Struct if kind == DEF_BLOCK_TEMPLATE else lsp.SymbolKind.Field, + ) + ) + declaration_nodes = _variable_declaration_nodes(command) declaration_ids = {id(node) for node in declaration_nodes} for node in declaration_nodes: