refactor(lsp): cache analysis results and debounce diagnostics
build_and_puplish.yml / build_and_publish (release) Successful in 29s
build_and_puplish.yml / build_and_publish (release) Successful in 29s
This change adds cached and incremental analysis for the LSP server to improve responsiveness. The client now debounces diagnostic updates to avoid excessive recomputation. The server introduces per-document line caches and various caches for completions, inlay hints, and metadata to support faster, incremental updates. - Debounce diagnostics on text changes to reduce noise. - Add caches for completions, inlay hints, and metadata. - Introduce incremental analysis with per-document line caches.
This commit is contained in:
@@ -3,17 +3,15 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from threading import Event
|
||||
|
||||
|
||||
THIS_DIR = Path(__file__).parent
|
||||
SRC_DIR = THIS_DIR.parent.parent / "src"
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
import lsprotocol.types as lsp # type: ignore
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
|
||||
import lsp_server
|
||||
import lsprotocol.types as lsp # type: ignore
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
|
||||
|
||||
def _server() -> TclLanguageServer:
|
||||
@@ -63,6 +61,7 @@ def test_close_replaces_unsaved_index_with_saved_file(tmp_path: Path, monkeypatc
|
||||
server = _server()
|
||||
server.update_poco_completion_for_file(document)
|
||||
server.get_tree(document)
|
||||
server.get_lines(document)
|
||||
monkeypatch.setattr(lsp_server, "LSP_SERVER", server)
|
||||
|
||||
lsp_server.did_close(
|
||||
@@ -75,6 +74,7 @@ def test_close_replaces_unsaved_index_with_saved_file(tmp_path: Path, monkeypatc
|
||||
assert "unsaved_proc" not in signatures[document.path]
|
||||
assert "saved_proc" in signatures[document.path]
|
||||
assert all(key[0] != document.uri for key in server._ast_cache)
|
||||
assert all(key[0] != document.uri for key in server._line_cache)
|
||||
|
||||
|
||||
def test_delete_and_rename_notifications_update_index(tmp_path: Path, monkeypatch):
|
||||
@@ -218,3 +218,52 @@ def test_delete_invalidates_in_flight_diagnostics(tmp_path: Path, monkeypatch):
|
||||
future.result(timeout=5)
|
||||
|
||||
assert server.diagnostic_snapshot(document.uri) is None
|
||||
|
||||
|
||||
def test_rapid_changes_analyze_only_latest_snapshot(tmp_path: Path, monkeypatch):
|
||||
server = _server()
|
||||
path = tmp_path / "debounced.tcl"
|
||||
first = _document(path, "set value 1", version=1)
|
||||
latest = _document(path, "set value 2", version=2)
|
||||
calls = []
|
||||
completed = Event()
|
||||
|
||||
monkeypatch.setattr(
|
||||
server,
|
||||
"compute_diagnostics",
|
||||
lambda document: calls.append(("diagnostics", document.version)),
|
||||
)
|
||||
|
||||
def record_index(document):
|
||||
calls.append(("index", document.version))
|
||||
completed.set()
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(server, "update_poco_completion_for_file", record_index)
|
||||
|
||||
server.schedule_document_analysis(first, delay_seconds=0.05)
|
||||
server.schedule_document_analysis(latest, delay_seconds=0.05)
|
||||
|
||||
assert completed.wait(timeout=2)
|
||||
assert calls == [("diagnostics", 2), ("index", 2)]
|
||||
|
||||
|
||||
def test_variable_and_workspace_request_caches_are_reused(tmp_path: Path):
|
||||
server = _server()
|
||||
document = _document(
|
||||
tmp_path / "cached.tcl",
|
||||
"proc cached_proc {argument} { set local_value $argument }",
|
||||
)
|
||||
|
||||
assert server.update_poco_completion_for_file(document)
|
||||
first_variables = server.variable_index_for_document(document)
|
||||
second_variables = server.variable_index_for_document(document)
|
||||
first_completions = server.completion_items_snapshot()
|
||||
second_completions = server.completion_items_snapshot()
|
||||
first_names = server.custom_function_names_snapshot()
|
||||
second_names = server.custom_function_names_snapshot()
|
||||
|
||||
assert first_variables is second_variables
|
||||
assert first_completions is second_completions
|
||||
assert first_names is second_names
|
||||
assert "cached_proc" in first_names
|
||||
|
||||
@@ -7,7 +7,6 @@ if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
import lsprotocol.types as lsp # type: ignore
|
||||
|
||||
from tools.inlay_hint import (
|
||||
InlayHintGenerator,
|
||||
InlayHintParameter,
|
||||
@@ -40,8 +39,7 @@ def _generate(
|
||||
) -> list[lsp.InlayHint]:
|
||||
tree = CustomParser().parse(source)
|
||||
generator = InlayHintGenerator(source, signatures, **options)
|
||||
tree.accept(generator, recurse=True)
|
||||
return generator.hints
|
||||
return generator.generate(tree)
|
||||
|
||||
|
||||
def _labels(hints: list[lsp.InlayHint]) -> list[str]:
|
||||
@@ -81,6 +79,23 @@ def test_only_hints_inside_requested_range_are_returned():
|
||||
assert hints[0].position.line == 2
|
||||
|
||||
|
||||
def test_range_walk_keeps_nested_commands_inside_proc_body():
|
||||
source = "proc wrapper {} {\n test_proc nested\n}"
|
||||
requested_range = lsp.Range(
|
||||
start=lsp.Position(line=1, character=0),
|
||||
end=lsp.Position(line=2, character=0),
|
||||
)
|
||||
|
||||
hints = _generate(
|
||||
source,
|
||||
{"test_proc": _signature("value")},
|
||||
requested_range=requested_range,
|
||||
)
|
||||
|
||||
assert _labels(hints) == ["value:"]
|
||||
assert hints[0].position.line == 1
|
||||
|
||||
|
||||
def test_matching_variable_name_can_be_suppressed():
|
||||
source = "test_proc $value $other"
|
||||
signature = _signature("value", "result")
|
||||
|
||||
Reference in New Issue
Block a user