feat(server): index PSC scripts and provide cross-file TclOO navigation/completions
Add PSC (.psc) indexing and share TclOO class metadata across files so class definitions discovered via PSC layers can be used for completions, signature help, inlay hints, and "go to definition". Key behavior changes: - Client file watcher now includes *.psc and .vscode launch paths/tests updated to use the postprocessor test folder; .gitignore updated to ignore that folder. - Server watches .psc changes and refreshes a PSC script index; new tools/tcloo_navigation.py exposes tcloo_definition used by the language server to resolve cross-file class/constructor/method definitions. - Language server uses class_snapshot(document.path) when producing TclOO completions, signature help, and inlay hints so resolved class metadata is available across files. Also includes related docs/changelog updates, minor code formatting cleanups, and added tests for PSC/TclOO behavior.
This commit is contained in:
@@ -2,6 +2,7 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import threading
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
@@ -15,6 +16,8 @@ from tclint.violations import Violation
|
||||
from tools import checks, parser
|
||||
from tools.completion_items import CompletionCollector
|
||||
from tools.tcloo_symbols import class_completion_items
|
||||
from tools.tcloo_completion import indexed_classes
|
||||
from tools.file_sourcing import get_all_psc_files, psc_script_files
|
||||
from tools.formatter import NxFormatter as Formatter
|
||||
from tools.inlay_hint import InlayHintSignature, build_custom_inlay_signatures
|
||||
from tools.navigation import FileSymbolIndex, build_file_symbol_index
|
||||
@@ -35,6 +38,10 @@ class TclLanguageServer(LanguageServer):
|
||||
self.poco_completion: dict = {}
|
||||
self.proc_signatures: dict = {}
|
||||
self.proc_docs: dict = {}
|
||||
self.class_indexes: dict = {}
|
||||
self.psc_script_paths: list[str] = []
|
||||
self._psc_files: dict[str, list[pathlib.Path]] = {}
|
||||
self._psc_lock = threading.RLock()
|
||||
self.navigation_indexes: dict[str, FileSymbolIndex] = {}
|
||||
self.variable_indexes: dict[
|
||||
str,
|
||||
@@ -171,6 +178,74 @@ class TclLanguageServer(LanguageServer):
|
||||
self._workspace_completion_cache = (self._index_generation, items)
|
||||
return items
|
||||
|
||||
def class_snapshot(self, current_path) -> dict:
|
||||
"""Share class metadata, without stale definitions from the active file.
|
||||
|
||||
PSC scripts override other workspace files in their listed load order.
|
||||
The request parser adds the current document's declarations last.
|
||||
"""
|
||||
with self._index_lock:
|
||||
indexes = {self._normalized_path(path): classes for path, classes in self.class_indexes.items()}
|
||||
paths = sorted(self.class_indexes, key=str.casefold)
|
||||
psc_paths = {self._normalized_path(path) for path in self.psc_script_paths}
|
||||
paths = [path for path in paths if self._normalized_path(path) not in psc_paths]
|
||||
paths.extend(self.psc_script_paths)
|
||||
classes = {}
|
||||
for path in paths:
|
||||
if not self.paths_equal(path, current_path):
|
||||
classes.update(indexes.get(self._normalized_path(path), {}))
|
||||
return classes
|
||||
|
||||
def refresh_psc_scripts(self, roots, report=LOGGER.warning):
|
||||
"""Index PSC dependencies through the same pipeline as workspace procs."""
|
||||
with self._psc_lock:
|
||||
discovered = {}
|
||||
for root in roots:
|
||||
for psc in get_all_psc_files(root):
|
||||
try:
|
||||
discovered[str(psc)] = psc_script_files(psc)
|
||||
except (OSError, ET.ParseError) as error:
|
||||
report(f"Could not read PSC {psc}: {error}")
|
||||
discovered[str(psc)] = self._psc_files.get(str(psc), [])
|
||||
paths = [str(path) for scripts in discovered.values() for path in scripts]
|
||||
with self._index_lock:
|
||||
previous = set(self.psc_script_paths)
|
||||
self.psc_script_paths = paths
|
||||
self._psc_files = discovered
|
||||
try:
|
||||
open_documents = {
|
||||
self._normalized_path(document.path): document
|
||||
for document in self.workspace.text_documents.values()
|
||||
}
|
||||
except RuntimeError:
|
||||
open_documents = {}
|
||||
for removed in previous - set(paths):
|
||||
path = pathlib.Path(removed)
|
||||
if (not any(self._is_same_or_child(path, root) for root in roots)
|
||||
and self._normalized_path(path) not in open_documents):
|
||||
self.remove_file_state(path.as_uri())
|
||||
for path_string in dict.fromkeys(paths):
|
||||
path = pathlib.Path(path_string)
|
||||
uri = path.as_uri()
|
||||
document = open_documents.get(self._normalized_path(path))
|
||||
if document is None and not path.is_file():
|
||||
self.remove_file_state(uri)
|
||||
report(f"PSC script not found: {path}")
|
||||
continue
|
||||
try:
|
||||
if document is None:
|
||||
data = path.read_bytes()
|
||||
try:
|
||||
source = data.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
# Older Windows NX layers use the ANSI code page.
|
||||
source = data.decode("cp1252")
|
||||
document = TextDocument(uri=uri, source=source, language_id="tcl")
|
||||
if not self.update_poco_completion_for_file(document, cache_tree=False):
|
||||
report(f"Could not index PSC script: {path}")
|
||||
except (OSError, UnicodeError) as error:
|
||||
report(f"Could not read PSC script {path}: {error}")
|
||||
|
||||
def completion_items_by_file_snapshot(
|
||||
self,
|
||||
) -> dict[str, tuple[lsp.CompletionItem, ...]]:
|
||||
@@ -455,6 +530,7 @@ class TclLanguageServer(LanguageServer):
|
||||
self.poco_completion.pop(filepath, None)
|
||||
self.proc_signatures.pop(filepath, None)
|
||||
self.proc_docs.pop(filepath, None)
|
||||
self.class_indexes.pop(filepath, None)
|
||||
self.navigation_indexes.pop(filepath, None)
|
||||
self.variable_indexes.pop(filepath, None)
|
||||
self._committed_index_versions.pop(filepath, None)
|
||||
@@ -486,6 +562,7 @@ class TclLanguageServer(LanguageServer):
|
||||
self.poco_completion,
|
||||
self.proc_signatures,
|
||||
self.proc_docs,
|
||||
self.class_indexes,
|
||||
self.navigation_indexes,
|
||||
self.variable_indexes,
|
||||
self._index_tokens,
|
||||
@@ -545,6 +622,7 @@ class TclLanguageServer(LanguageServer):
|
||||
)
|
||||
tree.accept(collector, recurse=True)
|
||||
collector.custom_functions.extend(class_completion_items(tree))
|
||||
classes = indexed_classes(tree, document.uri, document.source)
|
||||
docs = build_proc_docs(tree, document.source)
|
||||
navigation_index = build_file_symbol_index(
|
||||
filepath, document.uri, tree
|
||||
@@ -565,6 +643,7 @@ class TclLanguageServer(LanguageServer):
|
||||
self.poco_completion[filepath] = list(collector.custom_functions)
|
||||
self.proc_signatures[filepath] = dict(collector.proc_signatures)
|
||||
self.proc_docs[filepath] = docs
|
||||
self.class_indexes[filepath] = classes
|
||||
self.navigation_indexes[filepath] = navigation_index
|
||||
self.variable_indexes[filepath] = (document.version, variable_index)
|
||||
self._committed_index_versions[filepath] = document.version
|
||||
|
||||
Reference in New Issue
Block a user