add outline to lsp
This commit is contained in:
@@ -179,11 +179,7 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||||||
|
|
||||||
context.subscriptions.push(formatDefProvider)
|
context.subscriptions.push(formatDefProvider)
|
||||||
|
|
||||||
const tclOutlineProvider = vscode.languages.registerDocumentSymbolProvider(
|
// Outline now provided by the language server (Document Symbols). Removing TS provider.
|
||||||
{ scheme: "file", language: "tcl" },
|
|
||||||
{ provideDocumentSymbols: tclDocumentSymbolProvider }
|
|
||||||
)
|
|
||||||
context.subscriptions.push(tclOutlineProvider)
|
|
||||||
|
|
||||||
// Diagnostics collection
|
// Diagnostics collection
|
||||||
const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl")
|
const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl")
|
||||||
|
|||||||
+181
-1
@@ -144,9 +144,189 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
|
|||||||
# return symbols
|
# return symbols
|
||||||
|
|
||||||
|
|
||||||
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
|
||||||
|
def document_symbols(params: lsp.DocumentSymbolParams):
|
||||||
|
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
lines = doc.source.split("\n")
|
||||||
|
|
||||||
|
# Regex patterns similar to the previous TS implementation
|
||||||
|
ns_re = re.compile(r"^\s*namespace\s+eval\s+([^\s\{]+)")
|
||||||
|
proc_re = re.compile(r"^\s*proc\s+([^\s\{]+)\s+\{.*\}\s+\{")
|
||||||
|
set_re = re.compile(r"^\s*set\s+([^\s\}]+)")
|
||||||
|
event_start_re = re.compile(r"^\s*LIB_GE_command_buffer_edit_(prepend|append|insert|replace)\b")
|
||||||
|
event_close_inline_re = re.compile(r"^\s*\}\s*(\S+)\s*.*$")
|
||||||
|
event_close_brace_only_re = re.compile(r"^\s*\}\s*$")
|
||||||
|
event_name_line_re = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\b")
|
||||||
|
pending_event = None
|
||||||
|
|
||||||
|
class Scope:
|
||||||
|
def __init__(self, name: str, symbol: lsp.DocumentSymbol, start_line: int):
|
||||||
|
self.name = name
|
||||||
|
self.symbol = symbol
|
||||||
|
self.start_line = start_line
|
||||||
|
self.brace_count = 0
|
||||||
|
|
||||||
|
root_symbol = lsp.DocumentSymbol(
|
||||||
|
name="root",
|
||||||
|
detail="",
|
||||||
|
kind=lsp.SymbolKind.Namespace,
|
||||||
|
range=lsp.Range(start=lsp.Position(0, 0), end=lsp.Position(len(lines), 0)),
|
||||||
|
selection_range=lsp.Range(start=lsp.Position(0, 0), end=lsp.Position(0, 0)),
|
||||||
|
children=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
scope_stack: list[Scope] = [Scope("", root_symbol, 0)]
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
# If inside an event, update its brace count for this line
|
||||||
|
if "pending_event" in locals() and pending_event is not None:
|
||||||
|
pe_open = line.count("{")
|
||||||
|
pe_close = line.count("}")
|
||||||
|
pending_event["brace_count"] = pending_event.get("brace_count", 1) + pe_open - pe_close
|
||||||
|
# If this line closed the outer event block, finalize the event
|
||||||
|
if pending_event["brace_count"] <= 0:
|
||||||
|
# Try inline name on the same line
|
||||||
|
close_inline = event_close_inline_re.match(line)
|
||||||
|
if close_inline:
|
||||||
|
event_name = close_inline.group(1)
|
||||||
|
name_line_index = i
|
||||||
|
else:
|
||||||
|
# Look ahead to next non-empty line for the name
|
||||||
|
j = i + 1
|
||||||
|
while j < len(lines) and lines[j].strip() == "":
|
||||||
|
j += 1
|
||||||
|
event_name = None
|
||||||
|
name_line_index = i
|
||||||
|
if j < len(lines):
|
||||||
|
name_line = lines[j]
|
||||||
|
name_match = event_name_line_re.match(name_line)
|
||||||
|
if name_match:
|
||||||
|
event_name = name_match.group(1)
|
||||||
|
name_line_index = j
|
||||||
|
if event_name:
|
||||||
|
start_line = pending_event["start"]
|
||||||
|
edit_type = pending_event["edit_type"]
|
||||||
|
children = pending_event.get("children", [])
|
||||||
|
detail = f"Event ({edit_type})"
|
||||||
|
event_symbol = lsp.DocumentSymbol(
|
||||||
|
name=event_name,
|
||||||
|
detail=detail,
|
||||||
|
kind=lsp.SymbolKind.Event,
|
||||||
|
range=lsp.Range(start=lsp.Position(start_line, 0), end=lsp.Position(name_line_index, len(lines[name_line_index]))),
|
||||||
|
selection_range=lsp.Range(start=lsp.Position(name_line_index, 0), end=lsp.Position(name_line_index, len(lines[name_line_index]))),
|
||||||
|
children=children or [],
|
||||||
|
)
|
||||||
|
if scope_stack[-1].symbol.children is None:
|
||||||
|
scope_stack[-1].symbol.children = []
|
||||||
|
scope_stack[-1].symbol.children.append(event_symbol)
|
||||||
|
# Clear event tracking and continue
|
||||||
|
pending_event = None
|
||||||
|
continue
|
||||||
|
ns_match = ns_re.match(line)
|
||||||
|
proc_match = proc_re.match(line)
|
||||||
|
set_match = set_re.match(line)
|
||||||
|
|
||||||
|
# Namespace
|
||||||
|
if ns_match:
|
||||||
|
ns_name = ns_match.group(1)
|
||||||
|
start = lsp.Position(i, 0)
|
||||||
|
end = lsp.Position(i, len(line))
|
||||||
|
sel_start_char = line.find(ns_name)
|
||||||
|
sel_end_char = sel_start_char + len(ns_name) if sel_start_char >= 0 else len(line)
|
||||||
|
ns_symbol = lsp.DocumentSymbol(
|
||||||
|
name=ns_name,
|
||||||
|
detail="Namespace",
|
||||||
|
kind=lsp.SymbolKind.Namespace,
|
||||||
|
range=lsp.Range(start=start, end=end),
|
||||||
|
selection_range=lsp.Range(
|
||||||
|
start=lsp.Position(i, max(sel_start_char, 0)),
|
||||||
|
end=lsp.Position(i, max(sel_end_char, 0)),
|
||||||
|
),
|
||||||
|
children=[],
|
||||||
|
)
|
||||||
|
scope = Scope(ns_name, ns_symbol, i)
|
||||||
|
if scope_stack[-1].symbol.children is None:
|
||||||
|
scope_stack[-1].symbol.children = []
|
||||||
|
scope_stack[-1].symbol.children.append(ns_symbol)
|
||||||
|
scope_stack.append(scope)
|
||||||
|
|
||||||
|
# Proc
|
||||||
|
elif proc_match:
|
||||||
|
proc_name = proc_match.group(1)
|
||||||
|
start = lsp.Position(i, 0)
|
||||||
|
end = lsp.Position(i, len(line))
|
||||||
|
sel_start_char = line.find(proc_name)
|
||||||
|
sel_end_char = sel_start_char + len(proc_name) if sel_start_char >= 0 else len(line)
|
||||||
|
proc_symbol = lsp.DocumentSymbol(
|
||||||
|
name=proc_name,
|
||||||
|
detail="Procedure",
|
||||||
|
kind=lsp.SymbolKind.Function,
|
||||||
|
range=lsp.Range(start=start, end=end),
|
||||||
|
selection_range=lsp.Range(
|
||||||
|
start=lsp.Position(i, max(sel_start_char, 0)),
|
||||||
|
end=lsp.Position(i, max(sel_end_char, 0)),
|
||||||
|
),
|
||||||
|
children=[],
|
||||||
|
)
|
||||||
|
scope = Scope(proc_name, proc_symbol, i)
|
||||||
|
if scope_stack[-1].symbol.children is None:
|
||||||
|
scope_stack[-1].symbol.children = []
|
||||||
|
scope_stack[-1].symbol.children.append(proc_symbol)
|
||||||
|
scope_stack.append(scope)
|
||||||
|
|
||||||
|
# set variable
|
||||||
|
elif set_match:
|
||||||
|
var_name = set_match.group(1)
|
||||||
|
start = lsp.Position(i, 0)
|
||||||
|
end = lsp.Position(i, len(line))
|
||||||
|
sel_start_char = line.find(var_name)
|
||||||
|
sel_end_char = sel_start_char + len(var_name) if sel_start_char >= 0 else len(line)
|
||||||
|
var_symbol = lsp.DocumentSymbol(
|
||||||
|
name=var_name,
|
||||||
|
detail="Variable",
|
||||||
|
kind=lsp.SymbolKind.Variable,
|
||||||
|
range=lsp.Range(start=start, end=end),
|
||||||
|
selection_range=lsp.Range(
|
||||||
|
start=lsp.Position(i, max(sel_start_char, 0)),
|
||||||
|
end=lsp.Position(i, max(sel_end_char, 0)),
|
||||||
|
),
|
||||||
|
children=None,
|
||||||
|
)
|
||||||
|
# Attach to current scope or pending event as child
|
||||||
|
if "pending_event" in locals() and pending_event is not None:
|
||||||
|
pending_event["children"].append(var_symbol)
|
||||||
|
else:
|
||||||
|
if scope_stack[-1].symbol.children is None:
|
||||||
|
scope_stack[-1].symbol.children = []
|
||||||
|
scope_stack[-1].symbol.children.append(var_symbol)
|
||||||
|
|
||||||
|
# Event start (buffer edit)
|
||||||
|
if event_start_re.match(line):
|
||||||
|
m = event_start_re.match(line)
|
||||||
|
if m:
|
||||||
|
edit_type = m.group(1)
|
||||||
|
pending_event = {"start": i, "edit_type": edit_type, "children": []}
|
||||||
|
continue
|
||||||
|
# Brace balancing for scopes (namespace/proc)
|
||||||
|
open_count = line.count("{")
|
||||||
|
close_count = line.count("}")
|
||||||
|
scope_stack[-1].brace_count += open_count - close_count
|
||||||
|
|
||||||
|
# Close finished scopes
|
||||||
|
while len(scope_stack) > 1 and scope_stack[-1].brace_count <= 0:
|
||||||
|
finished = scope_stack.pop()
|
||||||
|
# Set the full range from startLine to current line
|
||||||
|
finished.symbol.range = lsp.Range(
|
||||||
|
start=lsp.Position(finished.start_line, 0),
|
||||||
|
end=lsp.Position(i, len(line)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return top-level children
|
||||||
|
return root_symbol.children
|
||||||
|
|
||||||
|
|
||||||
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
|
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
|
||||||
def inlay_hints(params: lsp.InlayHintParams):
|
def inlay_hints(params: lsp.InlayHintParams):
|
||||||
log_to_output(str(GLOBAL_SETTINGS.get("inlayHint", False)))
|
|
||||||
if not GLOBAL_SETTINGS.get("inlayHint", False):
|
if not GLOBAL_SETTINGS.get("inlayHint", False):
|
||||||
return []
|
return []
|
||||||
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
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 lsprotocol.types as lsp # type: ignore
|
||||||
|
from lsp_server import LSP_SERVER, document_symbols # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def test_document_symbols_namespace_proc_set_hierarchy(tmp_path: Path):
|
||||||
|
source_lines = [
|
||||||
|
"set top_var 1",
|
||||||
|
"namespace eval myns {",
|
||||||
|
" set ns_var 2",
|
||||||
|
" proc add {a b} {",
|
||||||
|
" set sum [expr {$a + $b}]",
|
||||||
|
" return $sum",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
"proc top_proc {} {",
|
||||||
|
" set x 3",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
uri = Path(tmp_path / "sym.tcl").as_uri()
|
||||||
|
# Put a text document into the workspace
|
||||||
|
LSP_SERVER.workspace.put_text_document(lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source))
|
||||||
|
|
||||||
|
# Request document symbols
|
||||||
|
params = lsp.DocumentSymbolParams(text_document=lsp.TextDocumentIdentifier(uri=uri))
|
||||||
|
symbols = document_symbols(params)
|
||||||
|
|
||||||
|
# Expect at least 2 top-level children: root contains 'set top_var' (variable) and 'namespace myns' and 'proc top_proc'
|
||||||
|
names_kinds = {(s.name, s.kind) for s in symbols}
|
||||||
|
assert ("root", lsp.SymbolKind.Namespace) not in names_kinds # root should not be included itself
|
||||||
|
|
||||||
|
# Find namespace symbol
|
||||||
|
ns = next(s for s in symbols if s.name == "myns")
|
||||||
|
assert ns.kind == lsp.SymbolKind.Namespace
|
||||||
|
assert ns.children is not None
|
||||||
|
|
||||||
|
# Inside namespace: has variable and proc
|
||||||
|
child_names = {c.name for c in ns.children}
|
||||||
|
assert "ns_var" in child_names
|
||||||
|
assert "add" in child_names
|
||||||
|
|
||||||
|
# top-level variable and proc also present
|
||||||
|
top_names = {s.name for s in symbols}
|
||||||
|
assert "top_var" in top_names
|
||||||
|
assert "top_proc" in top_names
|
||||||
|
|
||||||
|
# Check that proc add has no children (we're not extracting params as children here)
|
||||||
|
add = next(c for c in ns.children if c.name == "add")
|
||||||
|
assert add.kind == lsp.SymbolKind.Function
|
||||||
|
assert add.children == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_buffer_edit_events_are_symbols_with_type_and_name(tmp_path: Path):
|
||||||
|
source_lines = [
|
||||||
|
"LIB_GE_command_buffer_edit_insert LIB_ROTARY_positioning_first_move_pos ROTARY_POSITIONING_FIRST_MOVE_POS {",
|
||||||
|
" MOM_enable_address Z M_coolant_off D M_coolant_1 M_coolant_2 H_pressure",
|
||||||
|
"}",
|
||||||
|
" Coolant after @DECOMPOSEZUL",
|
||||||
|
"",
|
||||||
|
"LIB_GE_command_buffer_edit_append MOM_start_of_path_LIB MOM_start_of_path_LIB_ENTRY_end {",
|
||||||
|
" MOM_force once M_coolant_1 M_coolant_2 H_pressure",
|
||||||
|
"}",
|
||||||
|
" force_coolant",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
uri = Path(tmp_path / "events.tcl").as_uri()
|
||||||
|
LSP_SERVER.workspace.put_text_document(lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source))
|
||||||
|
params = lsp.DocumentSymbolParams(text_document=lsp.TextDocumentIdentifier(uri=uri))
|
||||||
|
symbols = document_symbols(params)
|
||||||
|
|
||||||
|
# Find event symbols
|
||||||
|
events = [s for s in symbols if s.kind == lsp.SymbolKind.Event]
|
||||||
|
assert events, "Expected at least one event symbol"
|
||||||
|
|
||||||
|
# Verify names and details
|
||||||
|
names = [e.name for e in events]
|
||||||
|
assert "Coolant" in names or "force_coolant" in names
|
||||||
|
for e in events:
|
||||||
|
assert e.detail.startswith("Event (")
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_children_include_set_variable(tmp_path: Path):
|
||||||
|
source_lines = [
|
||||||
|
"LIB_GE_command_buffer_edit_append MOM_rapid_move_LIB MOM_rapid_move_LIB_ENTRY_start {",
|
||||||
|
" if {[info exists ::mom_lift_off_output] && $::kapp_vars(retract_start) == 0} {",
|
||||||
|
" kapp_retract_subpgm",
|
||||||
|
" }",
|
||||||
|
" set ::kapp_vars(retract_start) 0",
|
||||||
|
"}",
|
||||||
|
" KappRetractSubPgm",
|
||||||
|
]
|
||||||
|
source = "\n".join(source_lines)
|
||||||
|
|
||||||
|
uri = Path(tmp_path / "event_children.tcl").as_uri()
|
||||||
|
LSP_SERVER.workspace.put_text_document(lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source))
|
||||||
|
params = lsp.DocumentSymbolParams(text_document=lsp.TextDocumentIdentifier(uri=uri))
|
||||||
|
symbols = document_symbols(params)
|
||||||
|
|
||||||
|
events = [s for s in symbols if s.kind == lsp.SymbolKind.Event and s.name == "KappRetractSubPgm"]
|
||||||
|
assert events, "Expected event symbol for KappRetractSubPgm"
|
||||||
|
ev = events[0]
|
||||||
|
assert ev.children is not None
|
||||||
|
# Ensure the set variable is a child of the event
|
||||||
|
child_names = {c.name for c in ev.children}
|
||||||
|
assert "::kapp_vars(retract_start)" in child_names
|
||||||
Reference in New Issue
Block a user