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:
@@ -1,3 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
from collections.abc import Iterable, Sequence
|
||||
from enum import Enum
|
||||
|
||||
import lsprotocol.types as lsp
|
||||
from common.load_data import standard_items
|
||||
from tclint.syntax_tree import BareWord, Command, List, Visitor
|
||||
@@ -6,6 +13,86 @@ BUILTIN_VAR_LABELS = {ci.label for ci in standard_items.nx_variables}
|
||||
BUILTIN_PROC_LABELS = {ci.label for ci in standard_items.nx_procs}
|
||||
|
||||
|
||||
class CompletionContext(Enum):
|
||||
VARIABLE = "variable"
|
||||
COMMAND = "command"
|
||||
GENERAL = "general"
|
||||
|
||||
|
||||
VARIABLE_KINDS = {
|
||||
lsp.CompletionItemKind.Variable,
|
||||
lsp.CompletionItemKind.Field,
|
||||
lsp.CompletionItemKind.Constant,
|
||||
}
|
||||
COMMAND_KINDS = {
|
||||
lsp.CompletionItemKind.Function,
|
||||
lsp.CompletionItemKind.Method,
|
||||
lsp.CompletionItemKind.Constructor,
|
||||
lsp.CompletionItemKind.Keyword,
|
||||
}
|
||||
|
||||
_VARIABLE_PREFIX_RE = re.compile(r"(?<!\\)\$(?:\{)?[A-Za-z0-9_:]*$")
|
||||
_COMMAND_PREFIX_RE = re.compile(r"(?:^|[;\[\{])\s*[^\s;\[\]\{\}]*$")
|
||||
|
||||
|
||||
def _codepoint_offset(line: str, utf16_offset: int) -> int:
|
||||
"""Translate an LSP UTF-16 character offset into a Python string offset."""
|
||||
if utf16_offset <= 0:
|
||||
return 0
|
||||
|
||||
units = 0
|
||||
for offset, character in enumerate(line):
|
||||
units += 2 if ord(character) > 0xFFFF else 1
|
||||
if units >= utf16_offset:
|
||||
return offset + 1
|
||||
return len(line)
|
||||
|
||||
|
||||
def completion_context(
|
||||
source_lines: Sequence[str], position: lsp.Position
|
||||
) -> CompletionContext:
|
||||
if position.line < 0 or position.line >= len(source_lines):
|
||||
return CompletionContext.GENERAL
|
||||
|
||||
line = source_lines[position.line]
|
||||
prefix = line[: _codepoint_offset(line, position.character)]
|
||||
if _VARIABLE_PREFIX_RE.search(prefix):
|
||||
return CompletionContext.VARIABLE
|
||||
if _COMMAND_PREFIX_RE.search(prefix):
|
||||
return CompletionContext.COMMAND
|
||||
return CompletionContext.GENERAL
|
||||
|
||||
|
||||
def _is_allowed(item: lsp.CompletionItem, context: CompletionContext) -> bool:
|
||||
if context == CompletionContext.VARIABLE:
|
||||
return item.kind in VARIABLE_KINDS
|
||||
if context == CompletionContext.COMMAND:
|
||||
return item.kind in COMMAND_KINDS
|
||||
return True
|
||||
|
||||
|
||||
def ranked_completion_items(
|
||||
candidates: Iterable[tuple[int, lsp.CompletionItem]],
|
||||
context: CompletionContext,
|
||||
) -> list[lsp.CompletionItem]:
|
||||
"""Filter, de-duplicate, and rank completion candidates for one request."""
|
||||
items = []
|
||||
seen: set[tuple[str, lsp.CompletionItemKind | None]] = set()
|
||||
for sequence, (priority, item) in enumerate(candidates):
|
||||
if not _is_allowed(item, context):
|
||||
continue
|
||||
|
||||
key = (item.label, item.kind)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
|
||||
ranked = copy.copy(item)
|
||||
ranked.sort_text = f"{priority:03d}:{item.label.casefold()}:{sequence:06d}"
|
||||
items.append(ranked)
|
||||
return items
|
||||
|
||||
|
||||
class CompletionItems:
|
||||
def __init__(self):
|
||||
self._custom_functions: list[lsp.CompletionItem] = []
|
||||
@@ -77,9 +164,17 @@ class CompletionCollector(Visitor):
|
||||
# Collect global variables declared with: global var1 var2 ...
|
||||
elif routine.contents == "global" and command.args:
|
||||
for arg in command.args:
|
||||
if isinstance(arg, BareWord) and getattr(arg, "value", None):
|
||||
if arg.value not in BUILTIN_VAR_LABELS:
|
||||
self._append_unique(lsp.CompletionItem(label=arg.value, kind=lsp.CompletionItemKind.Variable))
|
||||
if (
|
||||
isinstance(arg, BareWord)
|
||||
and getattr(arg, "value", None)
|
||||
and arg.value not in BUILTIN_VAR_LABELS
|
||||
):
|
||||
self._append_unique(
|
||||
lsp.CompletionItem(
|
||||
label=arg.value,
|
||||
kind=lsp.CompletionItemKind.Variable,
|
||||
)
|
||||
)
|
||||
|
||||
# Collect variables set with explicit global namespace: set ::var_name ...
|
||||
elif routine.contents == "set" and command.args:
|
||||
|
||||
@@ -522,6 +522,37 @@ def matching_occurrences(
|
||||
return matches
|
||||
|
||||
|
||||
def document_highlights(
|
||||
index: FileSymbolIndex,
|
||||
identity: SymbolIdentity,
|
||||
definitions: set[SymbolIdentity],
|
||||
) -> list[lsp.DocumentHighlight]:
|
||||
"""Return all occurrences of one symbol in the active document."""
|
||||
highlights = []
|
||||
for occurrence in index.occurrences:
|
||||
if resolve_identity(occurrence, definitions) != identity:
|
||||
continue
|
||||
|
||||
kind = lsp.DocumentHighlightKind.Text
|
||||
if identity.kind == "variable":
|
||||
kind = (
|
||||
lsp.DocumentHighlightKind.Write
|
||||
if occurrence.is_definition
|
||||
else lsp.DocumentHighlightKind.Read
|
||||
)
|
||||
highlights.append(lsp.DocumentHighlight(range=occurrence.range, kind=kind))
|
||||
|
||||
return sorted(
|
||||
highlights,
|
||||
key=lambda highlight: (
|
||||
highlight.range.start.line,
|
||||
highlight.range.start.character,
|
||||
highlight.range.end.line,
|
||||
highlight.range.end.character,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def workspace_symbols(
|
||||
indexes: dict[str, FileSymbolIndex], query: str
|
||||
) -> list[lsp.SymbolInformation]:
|
||||
|
||||
Reference in New Issue
Block a user