Merge pull request 'Cross-file navigation and rename for PSC .def templates and addresses' (#46) from DEF_file into main
build_and_puplish.yml / build_and_publish (release) Successful in 34s

This commit was merged in pull request #46.
This commit is contained in:
2026-09-24 20:56:27 +00:00
9 changed files with 477 additions and 28 deletions
+7
View File
@@ -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
+21
View File
@@ -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
+81
View File
@@ -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 <T>(
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<T>(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<any>("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<any>("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<any>("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<any>("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)
}
})
]
}
+2
View File
@@ -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" },
+125 -2
View File
@@ -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"(?<![$\w])[A-Za-z_]+$")
@@ -709,6 +720,10 @@ def hover(params: lsp.HoverParams) -> 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)
+21 -14
View File
@@ -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."""
+156 -12
View File
@@ -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))
+60
View File
@@ -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:
@@ -430,6 +430,7 @@ VALUES_BY_POSITION: dict[tuple[tuple[str, ...], int], tuple[str, ...]] = {
(("array", "names"), 3): ("-exact", "-glob", "-regexp"),
(("MOM_force",), 1): ("Always", "Once", "Off"),
(("MOM_suppress",), 1): ("Always", "Once", "Off"),
(("MOM_do_template",), 2): ("CREATE", "BUFFER"),
(("close",), 2): ("read", "write"),
(("open",), 2): ("r", "r+", "w", "w+", "a", "a+"),
(("package", "prefer"), 2): ("latest", "stable"),
@@ -475,11 +476,14 @@ DYNAMIC_COMPLETION_RULES = (
DynamicCompletionRule(("rename",), frozenset({1}), DynamicCompletionKind.PROCEDURE),
# Namespace-taking commands.
DynamicCompletionRule(("MOM_do_template",), frozenset({1}), DynamicCompletionKind.BLOCK_TEMPLATE),
DynamicCompletionRule(("MOM_do_template",), frozenset({2}), DynamicCompletionKind.VALUE),
DynamicCompletionRule(("MOM_ask_address_value",), frozenset({1}), DynamicCompletionKind.ADDRESS),
DynamicCompletionRule(("MOM_force",), frozenset({1}), DynamicCompletionKind.VALUE),
DynamicCompletionRule(("MOM_force",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
DynamicCompletionRule(("MOM_suppress",), frozenset({1}), DynamicCompletionKind.VALUE),
DynamicCompletionRule(("MOM_suppress",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
DynamicCompletionRule(("MOM_disable_address",), frozenset(range(1, 33)), DynamicCompletionKind.ADDRESS),
DynamicCompletionRule(("MOM_enable_address",), _REPEATED_SUBCOMMAND_ARGUMENTS, DynamicCompletionKind.ADDRESS),
DynamicCompletionRule(("namespace", "children"), frozenset({2}), DynamicCompletionKind.NAMESPACE),
DynamicCompletionRule(
("namespace", "delete"),