feat(lsp): add context-aware completion ranking and document highlights
Adds context-aware completion ranking and document highlights for Tcl. Adds completion_context to distinguish variables and commands. Wires per-file completion snapshots and ranking into the flow. Adds document_highlight provider support and tests for highlights. - Context-aware ranking of completion items using per-file snapshots - Document highlight provider wired into initialization and tests - Tests for completion context, ranking, and document highlights
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
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 lsp_server
|
||||
import lsprotocol.types as lsp # type: ignore
|
||||
from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from pygls.workspace import Workspace
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
from tools.completion_items import (
|
||||
COMMAND_KINDS,
|
||||
VARIABLE_KINDS,
|
||||
CompletionContext,
|
||||
completion_context,
|
||||
)
|
||||
|
||||
|
||||
def _position_after(source: str, token: str, occurrence: int = 0) -> lsp.Position:
|
||||
offset = -1
|
||||
for _ in range(occurrence + 1):
|
||||
offset = source.index(token, offset + 1)
|
||||
offset += len(token)
|
||||
before = source[:offset]
|
||||
return lsp.Position(
|
||||
line=before.count("\n"),
|
||||
character=offset - (before.rfind("\n") + 1),
|
||||
)
|
||||
|
||||
|
||||
def _document(path: Path, source: str) -> TextDocument:
|
||||
return TextDocument(
|
||||
uri=path.as_uri(),
|
||||
source=source,
|
||||
version=1,
|
||||
language_id="tcl",
|
||||
)
|
||||
|
||||
|
||||
def _completion_server(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> tuple[TclLanguageServer, TextDocument, str]:
|
||||
declared_builtin = standard_items.nx_variables[0].label
|
||||
current_source = (
|
||||
"set globalValue 1\n"
|
||||
"proc localProc {} { return }\n"
|
||||
"proc caller {argument} {\n"
|
||||
f" global {declared_builtin}\n"
|
||||
" set localValue 2\n"
|
||||
" puts $local\n"
|
||||
" localP\n"
|
||||
"}\n"
|
||||
)
|
||||
workspace_source = """set ::workspaceValue 1
|
||||
proc workspaceProc {} { return }
|
||||
"""
|
||||
current = _document(tmp_path / "current.tcl", current_source)
|
||||
workspace = _document(tmp_path / "workspace.tcl", workspace_source)
|
||||
server = TclLanguageServer(name="completion-test", version="1", max_workers=1)
|
||||
server.protocol._workspace = Workspace( # pylint: disable=protected-access
|
||||
root_uri=None,
|
||||
sync_kind=lsp.TextDocumentSyncKind.Incremental,
|
||||
workspace_folders=[],
|
||||
position_encoding=lsp.PositionEncodingKind.Utf16,
|
||||
)
|
||||
server.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(
|
||||
uri=current.uri,
|
||||
language_id="tcl",
|
||||
version=1,
|
||||
text=current_source,
|
||||
)
|
||||
)
|
||||
assert server.update_poco_completion_for_file(current)
|
||||
assert server.update_poco_completion_for_file(workspace)
|
||||
monkeypatch.setattr(lsp_server, "LSP_SERVER", server)
|
||||
return server, current, current_source
|
||||
|
||||
|
||||
def _complete(document: TextDocument, position: lsp.Position):
|
||||
return lsp_server.on_completion(
|
||||
lsp.CompletionParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
|
||||
position=position,
|
||||
)
|
||||
).items
|
||||
|
||||
|
||||
def test_variable_completion_filters_and_ranks_candidates(tmp_path: Path, monkeypatch):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
items = _complete(current, _position_after(source, "$local"))
|
||||
by_label = {item.label: item for item in items}
|
||||
|
||||
assert items
|
||||
assert all(item.kind in VARIABLE_KINDS for item in items)
|
||||
assert "localValue" in by_label
|
||||
assert "globalValue" in by_label
|
||||
assert "workspaceValue" in by_label
|
||||
assert "localProc" not in by_label
|
||||
assert "workspaceProc" not in by_label
|
||||
assert "puts" not in by_label
|
||||
|
||||
declared_builtin = standard_items.nx_variables[0]
|
||||
other_builtin = standard_items.nx_variables[1]
|
||||
assert declared_builtin.label in by_label
|
||||
assert other_builtin.label in by_label
|
||||
assert by_label[declared_builtin.label].documentation == (
|
||||
declared_builtin.documentation
|
||||
)
|
||||
assert by_label["localValue"].sort_text.startswith("000:")
|
||||
assert by_label["globalValue"].sort_text.startswith("100:")
|
||||
assert by_label["workspaceValue"].sort_text.startswith("200:")
|
||||
assert by_label[declared_builtin.label].sort_text.startswith("100:")
|
||||
assert by_label[other_builtin.label].sort_text.startswith("300:")
|
||||
|
||||
|
||||
def test_command_completion_filters_and_ranks_candidates(tmp_path: Path, monkeypatch):
|
||||
_, current, source = _completion_server(tmp_path, monkeypatch)
|
||||
items = _complete(current, _position_after(source, "localP", occurrence=1))
|
||||
by_label = {item.label: item for item in items}
|
||||
|
||||
assert items
|
||||
assert all(item.kind in COMMAND_KINDS for item in items)
|
||||
assert "localProc" in by_label
|
||||
assert "workspaceProc" in by_label
|
||||
assert "MOM_abort" in by_label
|
||||
assert "puts" in by_label
|
||||
assert "localValue" not in by_label
|
||||
assert "globalValue" not in by_label
|
||||
assert "workspaceValue" not in by_label
|
||||
assert by_label["localProc"].sort_text.startswith("100:")
|
||||
assert by_label["workspaceProc"].sort_text.startswith("200:")
|
||||
assert by_label["MOM_abort"].sort_text.startswith("300:")
|
||||
|
||||
|
||||
def test_completion_context_handles_nested_commands_and_utf16():
|
||||
assert (
|
||||
completion_context(["set result [work"], lsp.Position(line=0, character=16))
|
||||
== CompletionContext.COMMAND
|
||||
)
|
||||
assert (
|
||||
completion_context(["😀 puts $value"], lsp.Position(line=0, character=14))
|
||||
== CompletionContext.VARIABLE
|
||||
)
|
||||
assert (
|
||||
completion_context(["puts value"], lsp.Position(line=0, character=10))
|
||||
== CompletionContext.GENERAL
|
||||
)
|
||||
@@ -401,3 +401,62 @@ def test_call_hierarchy_item_uses_whole_proc_range(tmp_path: Path):
|
||||
assert items[0].selection_range.start == lsp.Position(line=0, character=5)
|
||||
assert items[0].range.start == lsp.Position(line=0, character=0)
|
||||
assert items[0].range.end.line == 2
|
||||
|
||||
|
||||
def test_document_highlight_marks_local_reads_and_writes(tmp_path: Path, monkeypatch):
|
||||
source = """proc first {} {
|
||||
set value 1
|
||||
puts $value
|
||||
incr value
|
||||
}
|
||||
proc second {} {
|
||||
set value 2
|
||||
puts $value
|
||||
}
|
||||
"""
|
||||
document = _document(tmp_path / "highlights.tcl", source)
|
||||
server = TclLanguageServer(name="highlight-test", version="1", max_workers=1)
|
||||
assert server.update_poco_completion_for_file(document)
|
||||
monkeypatch.setattr(lsp_server, "LSP_SERVER", server)
|
||||
position = _position(source, "$value")
|
||||
|
||||
highlights = lsp_server.document_highlight(
|
||||
lsp.DocumentHighlightParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
|
||||
position=lsp.Position(position.line, position.character + 1),
|
||||
)
|
||||
)
|
||||
|
||||
assert len(highlights) == 3
|
||||
assert [highlight.kind for highlight in highlights] == [
|
||||
lsp.DocumentHighlightKind.Write,
|
||||
lsp.DocumentHighlightKind.Read,
|
||||
lsp.DocumentHighlightKind.Write,
|
||||
]
|
||||
assert all(_range_text(source, highlight.range) == "value" for highlight in highlights)
|
||||
|
||||
|
||||
def test_document_highlight_marks_proc_definition_and_calls(tmp_path: Path, monkeypatch):
|
||||
source = """proc target {} { return }
|
||||
proc caller {} {
|
||||
target
|
||||
target
|
||||
}
|
||||
"""
|
||||
document = _document(tmp_path / "proc_highlights.tcl", source)
|
||||
server = TclLanguageServer(name="highlight-test", version="1", max_workers=1)
|
||||
assert server.update_poco_completion_for_file(document)
|
||||
monkeypatch.setattr(lsp_server, "LSP_SERVER", server)
|
||||
|
||||
highlights = lsp_server.document_highlight(
|
||||
lsp.DocumentHighlightParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=document.uri),
|
||||
position=_position(source, "target"),
|
||||
)
|
||||
)
|
||||
|
||||
assert len(highlights) == 3
|
||||
assert all(
|
||||
highlight.kind == lsp.DocumentHighlightKind.Text
|
||||
for highlight in highlights
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user