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:
+66
-41
@@ -44,6 +44,7 @@ from common.load_data import standard_items
|
||||
from lsp_tclserver import TclLanguageServer
|
||||
from pygls import uris
|
||||
from pygls.workspace.text_document import TextDocument
|
||||
from tools.completion_items import completion_context, ranked_completion_items
|
||||
from tools.folding_ranges import build_folding_ranges
|
||||
from tools.inlay_hint import (
|
||||
InlayHintGenerator,
|
||||
@@ -54,6 +55,7 @@ from tools.navigation import (
|
||||
call_hierarchy_identity,
|
||||
call_hierarchy_items,
|
||||
definition_identities,
|
||||
document_highlights,
|
||||
incoming_call_hierarchy,
|
||||
matching_occurrences,
|
||||
outgoing_call_hierarchy,
|
||||
@@ -87,9 +89,9 @@ STATIC_COMPLETION_ITEMS = tuple(
|
||||
+ standard_items.nx_procs
|
||||
+ standard_items.nx_variables
|
||||
)
|
||||
STATIC_COMPLETION_KEYS = frozenset(
|
||||
(item.label, getattr(item, "kind", None)) for item in STATIC_COMPLETION_ITEMS
|
||||
)
|
||||
STATIC_VARIABLE_ITEMS = {
|
||||
item.label: item for item in standard_items.nx_variables
|
||||
}
|
||||
BUILTIN_INLAY_SIGNATURES = build_builtin_inlay_signatures(
|
||||
standard_items.json_data.get("MOM_procs", [])
|
||||
)
|
||||
@@ -246,55 +248,63 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
|
||||
return lsp.FullDocumentDiagnosticReport(items=diagnostics, result_id=result_id)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
|
||||
@LSP_SERVER.feature(
|
||||
lsp.TEXT_DOCUMENT_COMPLETION,
|
||||
lsp.CompletionOptions(trigger_characters=["$"]),
|
||||
)
|
||||
def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
|
||||
from tools.completion_items import BUILTIN_VAR_LABELS
|
||||
|
||||
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||
|
||||
workspace_items = LSP_SERVER.completion_items_snapshot()
|
||||
tree = LSP_SERVER.get_tree(doc)
|
||||
globals_set, procs_locals, proc_ranges = LSP_SERVER.variable_index_for_document(
|
||||
doc, tree
|
||||
)
|
||||
|
||||
# Always include globals (excluding built-ins)
|
||||
dynamic_items = []
|
||||
for name in sorted(globals_set):
|
||||
if name not in BUILTIN_VAR_LABELS:
|
||||
dynamic_items.append(
|
||||
lsp.CompletionItem(label=name, kind=lsp.CompletionItemKind.Variable)
|
||||
position = params.position
|
||||
local_names: set[str] = set()
|
||||
for proc_range in proc_ranges:
|
||||
end_line = proc_range.end_line or proc_range.start_line
|
||||
if proc_range.start_line <= position.line <= end_line:
|
||||
local_names.update(procs_locals.get(proc_range.name, set()))
|
||||
break
|
||||
|
||||
candidates: list[tuple[int, lsp.CompletionItem]] = []
|
||||
for name in sorted(local_names - globals_set):
|
||||
candidates.append(
|
||||
(
|
||||
0,
|
||||
lsp.CompletionItem(
|
||||
label=name,
|
||||
kind=lsp.CompletionItemKind.Variable,
|
||||
detail="Local variable",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Include proc-local variables when cursor is inside that proc
|
||||
pos = params.position
|
||||
if pos is not None:
|
||||
for pr in proc_ranges:
|
||||
if pr.start_line <= pos.line <= (pr.end_line or pr.start_line):
|
||||
for name in sorted(procs_locals.get(pr.name, set())):
|
||||
# Exclude built-ins and globals to avoid duplication
|
||||
if name not in BUILTIN_VAR_LABELS and name not in globals_set:
|
||||
dynamic_items.append(
|
||||
lsp.CompletionItem(
|
||||
label=name, kind=lsp.CompletionItemKind.Variable
|
||||
)
|
||||
)
|
||||
break
|
||||
for name in sorted(globals_set):
|
||||
item = STATIC_VARIABLE_ITEMS.get(name)
|
||||
if item is None:
|
||||
item = lsp.CompletionItem(
|
||||
label=name,
|
||||
kind=lsp.CompletionItemKind.Variable,
|
||||
detail="Workspace variable",
|
||||
)
|
||||
candidates.append(
|
||||
(
|
||||
100,
|
||||
item,
|
||||
)
|
||||
)
|
||||
|
||||
# Merge with de-duplication. Each file keeps its complete index, so a proc
|
||||
# declared in multiple files must only appear once in the completion list.
|
||||
merged: list[lsp.CompletionItem] = list(STATIC_COMPLETION_ITEMS)
|
||||
seen_items: set[tuple[str, lsp.CompletionItemKind | None]] = set(
|
||||
STATIC_COMPLETION_KEYS
|
||||
)
|
||||
for it in (*workspace_items, *dynamic_items):
|
||||
key = (it.label, getattr(it, "kind", None))
|
||||
if key in seen_items:
|
||||
continue
|
||||
seen_items.add(key)
|
||||
merged.append(it)
|
||||
filepath = str(pathlib.Path(uris.to_fs_path(doc.uri)))
|
||||
items_by_file = LSP_SERVER.completion_items_by_file_snapshot()
|
||||
for item_path in sorted(items_by_file, key=lambda path: path.casefold()):
|
||||
priority = 100 if LSP_SERVER.paths_equal(item_path, filepath) else 200
|
||||
candidates.extend((priority, item) for item in items_by_file[item_path])
|
||||
|
||||
return lsp.CompletionList(is_incomplete=False, items=merged)
|
||||
candidates.extend((300, item) for item in STATIC_COMPLETION_ITEMS)
|
||||
context = completion_context(LSP_SERVER.get_lines(doc), position)
|
||||
items = ranked_completion_items(candidates, context)
|
||||
return lsp.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(
|
||||
@@ -548,6 +558,20 @@ def references(params: lsp.ReferenceParams) -> list[lsp.Location]:
|
||||
return _sorted_locations(locations)
|
||||
|
||||
|
||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT)
|
||||
def document_highlight(params: lsp.DocumentHighlightParams):
|
||||
context = _navigation_context(params.text_document.uri, params.position)
|
||||
if context is None:
|
||||
return []
|
||||
|
||||
indexes, definitions, _, identity = context
|
||||
filepath = str(pathlib.Path(uris.to_fs_path(params.text_document.uri)))
|
||||
index = indexes.get(filepath)
|
||||
if index is None:
|
||||
return []
|
||||
return document_highlights(index, identity, definitions)
|
||||
|
||||
|
||||
def _is_renamable(
|
||||
identity: SymbolIdentity,
|
||||
indexes,
|
||||
@@ -744,6 +768,7 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
|
||||
legend=semantic_tokens_legend, full=True, range=False
|
||||
),
|
||||
definition_provider=True,
|
||||
document_highlight_provider=True,
|
||||
references_provider=True,
|
||||
rename_provider=lsp.RenameOptions(prepare_provider=True),
|
||||
workspace_symbol_provider=True,
|
||||
|
||||
Reference in New Issue
Block a user