perf(server): avoid unnecessary reparses and cache navigation definitions

Introduce several changes to reduce full-document reparses, lock contention and
redundant work when handling TclOO analysis and navigation:

- Add a cheap may_contain_classes pre-check and several cursor/receiver
  heuristics so completions, signature help and tcloo definitions skip the
  expensive marker reparse when the document cannot contain useful OO info.
- Allow passing an existing parsed tree into tcloo completion/signature/definition
  helpers; update callers to use the server's cached tree when available.
- Use a thread-local parser for request-time parse_source to avoid blocking the
  shared parser during background indexing, and add navigation_state() which
  returns cached definition identities (invalidated on index generation changes).
- Add a cheap name pre-filter (_may_resolve_to) for symbol matching and only
  run class highlighting when classes may exist.

These changes reduce contention and repeated parsing, improve responsiveness for
requests during background indexing, and cache navigation definition identities.
Tests were added/updated to assert caching and non-blocking behavior.
This commit is contained in:
Christoph Brandau
2026-09-23 12:04:36 +02:00
parent d9d619c1dc
commit b2e6e9d250
10 changed files with 276 additions and 40 deletions
@@ -267,3 +267,35 @@ def test_variable_and_workspace_request_caches_are_reused(tmp_path: Path):
assert first_completions is second_completions
assert first_names is second_names
assert "cached_proc" in first_names
def test_navigation_definitions_are_cached_until_the_index_changes(tmp_path: Path):
server = _server()
first = _document(tmp_path / "first.tcl", "proc first_proc {} {}")
assert server.update_poco_completion_for_file(first)
indexes, definitions = server.navigation_state()
assert server.navigation_state()[1] is definitions
assert {identity.name for identity in definitions} >= {"::first_proc"}
second = _document(tmp_path / "second.tcl", "proc second_proc {} {}")
assert server.update_poco_completion_for_file(second)
indexes, definitions = server.navigation_state()
assert second.path in indexes
assert {identity.name for identity in definitions} >= {"::first_proc", "::second_proc"}
def test_background_parse_does_not_wait_for_the_request_parser(tmp_path: Path):
server = _server()
document = _document(tmp_path / "background.tcl", "proc background_proc {} {}")
finished = Event()
def index():
assert server.update_poco_completion_for_file(document, cache_tree=False)
finished.set()
with server._parser_lock:
with ThreadPoolExecutor(max_workers=1) as executor:
executor.submit(index)
assert finished.wait(timeout=5)
assert "background_proc" in server.custom_function_names_snapshot()
@@ -98,3 +98,21 @@ def test_lsp_signature_help_with_unfinished_bracket(tmp_path, monkeypatch):
))
assert result.active_parameter == 1
assert result.signatures[0].label == "::MCS initValue i value"
def test_cached_tree_gives_same_signature_without_reparsing(monkeypatch):
import tools.tcloo_completion as tcloo
source = CLASS + "set mcs [MCS new test]\n$mcs initValue 0 "
position = lsp.Position(line=source.count("\n"), character=len(source.rsplit("\n", 1)[-1]))
expected = method_signature_help(source, position)
tree = CustomParser().parse(source)
parse_body = tcloo.parse_completion_source
def parse(text, pos=None):
# Braced class bodies are still parsed on demand, the document is not.
assert text != source, "unexpected full-document parse"
return parse_body(text, pos)
monkeypatch.setattr(tcloo, "parse_completion_source", parse)
assert method_signature_help(source, position, tree=tree) == expected
@@ -1,7 +1,7 @@
import lsprotocol.types as lsp
import pytest
from tools.tcloo_completion import tcloo_completions
from tools.tcloo_completion import ClassInfo, tcloo_completions
CLASS = """oo::class create MCS {
@@ -164,3 +164,68 @@ def test_namespaced_class_symbols_and_method_body_references():
assert set(declarations) == {"::geometry::MCS"}
assert [node.contents for node in references] == ["MCS", "MCS", "geometry::MCS", "::geometry::MCS"]
assert class_completion_items(tree)[0].label == "geometry::MCS"
@pytest.mark.parametrize("code", [
"set mcs [MCS new]\nputs |",
"set mcs [MCS new]\n se|",
"set mcs [MCS new]\nMOM_output_literal |",
])
def test_non_receivers_skip_the_completion_reparse(code, monkeypatch):
import tools.tcloo_completion as tcloo
def fail(*_args, **_kwargs):
raise AssertionError("unexpected full-document parse")
monkeypatch.setattr(tcloo, "parse_completion_source", fail)
assert complete(CLASS + code) is None
def test_documents_without_classes_skip_the_completion_reparse(monkeypatch):
import tools.tcloo_completion as tcloo
monkeypatch.setattr(tcloo, "parse_completion_source", lambda *_: pytest.fail("parsed"))
assert complete("set value [expr 1]\n$value |") is None
def test_external_class_receiver_still_completes():
source = "Logger |"
position = lsp.Position(line=0, character=len(source) - 1)
items = tcloo_completions([source.replace("|", "")], position, {"::Logger": ClassInfo()})
assert {item.label for item in items} == {"new", "create"}
def complete_with_tree(source):
from tclint.lexer import TclSyntaxError
from tools.parser import CustomParser
offset = source.index("|")
before = source[:offset]
position = lsp.Position(line=before.count("\n"), character=len(before.rsplit("\n", 1)[-1].encode("utf-16-le")) // 2)
text = source.replace("|", "")
try:
tree = CustomParser().parse(text)
except TclSyntaxError:
tree = None
return tcloo_completions(text.splitlines(), position, current_tree=lambda: tree)
@pytest.mark.parametrize("code", [
"set mcs [MCS new]\n$mcs |",
"MCS create instance\ninstance |",
"set mcs [[MCS new] initOrg 1 2 3]\n$mcs |",
"proc run {} {set mcs [MCS new]; $mcs |}",
"set mcs [MCS new]\nputs [$mcs |]",
"set mcs [MCS new]\nputs [$mcs |",
"[MCS new] |",
"set mcs [MCS new]\n$mcs initV|alue",
])
def test_cached_tree_keeps_method_completions(code):
assert "initValue" in {item.label for item in complete_with_tree(CLASS + code)}
def test_cached_tree_skips_reparse_outside_first_argument(monkeypatch):
import tools.tcloo_completion as tcloo
monkeypatch.setattr(tcloo, "parse_completion_source", lambda *_: pytest.fail("parsed"))
assert complete_with_tree(CLASS + "set mcs [MCS new]\nif {$mcs ne {}} |{ puts 1 }") is None