add outline to lsp

This commit is contained in:
Christoph Brandau
2025-08-11 18:28:23 +02:00
parent b3cac5c292
commit 0a26cb5c0f
3 changed files with 297 additions and 6 deletions
+181 -1
View File
@@ -144,9 +144,189 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
# 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)
def inlay_hints(params: lsp.InlayHintParams):
log_to_output(str(GLOBAL_SETTINGS.get("inlayHint", False)))
if not GLOBAL_SETTINGS.get("inlayHint", False):
return []
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)