"""Navigation between Tcl code and the block templates and addresses of .def files.""" from __future__ import annotations from pathlib import Path import lsprotocol.types as lsp from tools.def_symbols import ADDRESS, BLOCK_TEMPLATE, FORMAT, DefDeclaration, DefDocument from tools.navigation import FileSymbolIndex, SymbolOccurrence DefTarget = tuple[str, str] _KIND_LABELS = {BLOCK_TEMPLATE: "Block template", ADDRESS: "Address", FORMAT: "Format"} # Address properties in display order; others follow as declared. _ADDRESS_PROPERTIES = ( ("FORMAT", "Format"), ("LEADER", "Leader"), ("TRAILER", "Trailer"), ("MIN", "Min"), ("MAX", "Max"), ("FORCE", "Modality"), ("ZERO_FORMAT", "Zero format"), ("INCREMENTAL", "Incremental"), ("OMIT", "Omit"), ) _MODALITY = { "OFF": "modal, output only on change", "ONCE": "output once, then modal", "ALWAYS": "non-modal, always output", } def _range(line: int, start: int, end: int) -> lsp.Range: return lsp.Range( start=lsp.Position(line=line, character=start), end=lsp.Position(line=line, character=end), ) def _contains(line: int, start: int, end: int, position: lsp.Position) -> bool: return position.line == line and start <= position.character <= end def def_symbol_at(document: DefDocument, position: lsp.Position) -> tuple[DefTarget, lsp.Range, bool] | None: """Return the block template or address at ``position`` of a .def document. The flag tells whether the position is on a declaration. """ for declaration in document.declarations: if declaration.kind != FORMAT and _contains(declaration.line, declaration.start, declaration.end, position): return (declaration.kind, declaration.name), _range(declaration.line, declaration.start, declaration.end), True for reference in document.references: if _contains(reference.line, reference.start, reference.end, position): return (reference.kind, reference.name), _range(reference.line, reference.start, reference.end), False return None def _uri(path: str, uris: dict[str, str]) -> str: return uris.get(path) or Path(path).as_uri() def def_declarations(documents: dict[str, DefDocument], target: DefTarget) -> list[tuple[str, DefDeclaration]]: kind, name = target return [ (path, declaration) for path, document in documents.items() for declaration in document.declarations if declaration.kind == kind and declaration.name == name ] def def_definition_locations( documents: dict[str, DefDocument], target: DefTarget, uris: dict[str, str] | None = None ) -> list[lsp.Location]: uris = uris or {} return [ lsp.Location(uri=_uri(path, uris), range=_range(declaration.line, declaration.start, declaration.end)) for path, declaration in def_declarations(documents, target) ] def effective_def_locations( documents: dict[str, DefDocument], target: DefTarget, uris: dict[str, str] | None = None ) -> list[lsp.Location]: """The declaration NX uses: ``documents`` follow the PSC layer order, the last one wins.""" return def_definition_locations(documents, target, uris)[-1:] def def_reference_locations( documents: dict[str, DefDocument], target: DefTarget, include_declaration: bool, uris: dict[str, str] | None = None, ) -> list[lsp.Location]: """Return .def occurrences: declarations and addresses used in block templates.""" kind, name = target uris = uris or {} locations = def_definition_locations(documents, target, uris) if include_declaration else [] for path, document in documents.items(): for reference in document.references: if reference.kind == kind and reference.name == name: locations.append( lsp.Location(uri=_uri(path, uris), range=_range(reference.line, reference.start, reference.end)) ) return locations def tcl_def_occurrences( indexes: dict[str, FileSymbolIndex], target: DefTarget ) -> list[tuple[FileSymbolIndex, SymbolOccurrence]]: kind, name = target return [ (index, occurrence) for index in indexes.values() for occurrence in index.occurrences if occurrence.identity.kind == kind and occurrence.identity.name == name ] def all_def_target_locations( documents: dict[str, DefDocument], indexes: dict[str, FileSymbolIndex], target: DefTarget, include_declaration: bool, uris: dict[str, str] | None = None, ) -> list[lsp.Location]: locations = def_reference_locations(documents, target, include_declaration, uris) locations.extend( lsp.Location(uri=index.uri, range=occurrence.range) for index, occurrence in tcl_def_occurrences(indexes, target) ) unique = {} for location in locations: key = (location.uri, location.range.start.line, location.range.start.character) unique.setdefault(key, location) return sorted(unique.values(), key=lambda item: (item.uri, item.range.start.line, item.range.start.character)) def def_rename_edits( documents: dict[str, DefDocument], indexes: dict[str, FileSymbolIndex], target: DefTarget, new_name: str, uris: dict[str, str] | None = None, ) -> lsp.WorkspaceEdit | None: """Rename a block template or address in all .def and Tcl files, if it is declared.""" if not def_declarations(documents, target): return None changes: dict[str, list[lsp.TextEdit]] = {} for location in all_def_target_locations(documents, indexes, target, True, uris): changes.setdefault(location.uri, []).append(lsp.TextEdit(range=location.range, new_text=new_name)) for edits in changes.values(): edits.sort(key=lambda edit: (edit.range.start.line, edit.range.start.character), reverse=True) return lsp.WorkspaceEdit(changes=changes) def _escape_cell(value: str) -> str: return value.replace("|", "\\|") or " " def _address_table(declaration: DefDeclaration, formats: dict[str, DefDeclaration]) -> str: properties = dict(declaration.properties) rows = [] for key, label in _ADDRESS_PROPERTIES: value = properties.pop(key, None) if value is None: continue cell = f"`{value}`" if value else "" if key == "FORMAT" and value in formats: cell += f" → `{dict(formats[value].properties).get('FORMAT', '')}`" if key == "FORCE": cell += f" ({_MODALITY[value.upper()]})" if value.upper() in _MODALITY else "" rows.append(f"| {label} | {_escape_cell(cell)} |") rows.extend(f"| {key.title()} | {_escape_cell(f'`{value}`' if value else '')} |" for key, value in properties.items()) if not rows: return "_No properties_" return "\n".join(["| Property | Value |", "|---|---|", *rows]) def def_hover_markdown(documents: dict[str, DefDocument], target: DefTarget) -> str | None: """Describe the effective (last loaded) declaration and name the ones it overrides.""" declarations = def_declarations(documents, target) if not declarations: return None formats = { declaration.name: declaration for document in documents.values() for declaration in document.declarations if declaration.kind == FORMAT } kind, name = target path, declaration = declarations[-1] header = f"**{_KIND_LABELS[kind]}** `{name}` — {Path(path).name}:{declaration.line + 1}" if kind == ADDRESS: body = _address_table(declaration, formats) else: body = f"```def\n{declaration.text}\n```" markdown = f"{header}\n\n{body}" if len(declarations) > 1: overridden = ", ".join(f"{Path(other).name}:{item.line + 1}" for other, item in declarations[:-1]) markdown += f"\n\n---\n\n_Overrides {overridden}_" return markdown