refacxtor the code

This commit is contained in:
Christoph Brandau
2025-08-12 07:15:18 +02:00
parent 72f894b05d
commit 8fb98d2f40
3 changed files with 214 additions and 198 deletions
+19 -197
View File
@@ -9,7 +9,7 @@ import os
import pathlib import pathlib
import re import re
import sys import sys
from typing import Any, List, Optional, Tuple from typing import Any, Optional
import operator import operator
from functools import reduce from functools import reduce
@@ -49,7 +49,6 @@ from lsp_tclserver import TclLanguageServer
WORKSPACE_SETTINGS = {} WORKSPACE_SETTINGS = {}
GLOBAL_SETTINGS = {} GLOBAL_SETTINGS = {}
RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py"
MAX_WORKERS = 5 MAX_WORKERS = 5
@@ -58,9 +57,7 @@ LSP_SERVER = TclLanguageServer(name="NX Postprocessor Support", version="0.0.1",
# ********************************************************** # **********************************************************
# Tool specific code goes below this. # Tool specific code goes below this.
# ********************************************************** # **********************************************************
TOOL_MODULE = "nx-post-support"
TOOL_DISPLAY = "NX Postprocessor Support"
TOOL_ARGS = [] # default arguments always passed to your tool.
# Delete "Linting features" section if your tool is NOT a linter. # Delete "Linting features" section if your tool is NOT a linter.
# ********************************************************** # **********************************************************
@@ -87,7 +84,7 @@ def did_save(params: lsp.DidSaveTextDocumentParams) -> None:
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
def did_close(params: lsp.DidCloseTextDocumentParams) -> None: def did_close(_: lsp.DidCloseTextDocumentParams) -> None:
"""LSP handler for textDocument/didClose request.""" """LSP handler for textDocument/didClose request."""
@@ -125,12 +122,9 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams):
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_COMPLETION)
def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]: def on_completion(params: lsp.CompletionParams) -> lsp.CompletionList:
_ = LSP_SERVER.workspace.get_text_document(params.text_document.uri) _ = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
poco = [] poco = [item for items in LSP_SERVER.poco_completion.values() for item in items]
for key, value in LSP_SERVER.poco_completion.items():
poco.extend(value)
items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco
return lsp.CompletionList(is_incomplete=False, items=items) return lsp.CompletionList(is_incomplete=False, items=items)
@@ -146,183 +140,10 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
@LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL) @LSP_SERVER.feature(lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL)
def document_symbols(params: lsp.DocumentSymbolParams): def document_symbols(params: lsp.DocumentSymbolParams):
from tools.document_symbols import build_document_symbols
doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri) doc = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
lines = doc.source.split("\n") return build_document_symbols(doc.source)
# 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)
@@ -332,13 +153,14 @@ def inlay_hints(params: lsp.InlayHintParams):
document = LSP_SERVER.workspace.get_text_document(params.text_document.uri) document = LSP_SERVER.workspace.get_text_document(params.text_document.uri)
tree = LSP_SERVER.parser.parse(document.source) tree = LSP_SERVER.parser.parse(document.source)
# collect Inlay Hints # Merge proc signatures across files and traverse once
hints = [] merged_signatures = {}
for key, value in LSP_SERVER.proc_signatures.items(): for sigs in LSP_SERVER.proc_signatures.values():
generator = InlayHintGenerator(LSP_SERVER.proc_signatures[key]) merged_signatures.update(sigs)
generator = InlayHintGenerator(merged_signatures)
tree.accept(generator, recurse=True) tree.accept(generator, recurse=True)
hints += generator.hints return generator.hints
return hints
@LSP_SERVER.feature( @LSP_SERVER.feature(
@@ -364,7 +186,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
[ [
token.line, token.line,
token.offset, token.offset,
token.lenght, token.length,
TOKEN_TYPES.index(token.tok_type), TOKEN_TYPES.index(token.tok_type),
reduce(operator.or_, token.tok_modifiers, 0), reduce(operator.or_, token.tok_modifiers, 0),
] ]
@@ -492,7 +314,7 @@ def goto_definition(params: lsp.DefinitionParams):
# 2) Search in indexed files from proc_signatures # 2) Search in indexed files from proc_signatures
# Build list of candidate files that declare this token as a proc # Build list of candidate files that declare this token as a proc
candidate_files: List[str] = [] candidate_files: list[str] = []
for file_path, procs in LSP_SERVER.proc_signatures.items(): for file_path, procs in LSP_SERVER.proc_signatures.items():
if token in procs: if token in procs:
candidate_files.append(file_path) candidate_files.append(file_path)
@@ -553,7 +375,7 @@ def formatting(params: lsp.DocumentFormattingParams) -> list[lsp.TextEdit] | Non
# Required Language Server Initialization and Exit handlers. # Required Language Server Initialization and Exit handlers.
# ********************************************************** # **********************************************************
@LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION) @LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
def did_change_configuration(params: lsp.DidChangeConfigurationParams): def did_change_configuration(_: lsp.DidChangeConfigurationParams):
"""LSP Handler for Config Changes""" """LSP Handler for Config Changes"""
@@ -573,7 +395,7 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
log_to_output(f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n") log_to_output(f"Global settings:\r\n{json.dumps(GLOBAL_SETTINGS, indent=4, ensure_ascii=False)}\r\n")
semantic_tokens_legend = lsp.SemanticTokensLegend( semantic_tokens_legend = lsp.SemanticTokensLegend(
token_types=TOKEN_TYPES, token_types=TOKEN_TYPES,
token_modifiers=TokenModifier, token_modifiers=[m.name for m in TokenModifier],
) )
return lsp.InitializeResult( return lsp.InitializeResult(
capabilities=lsp.ServerCapabilities( capabilities=lsp.ServerCapabilities(
+189
View File
@@ -0,0 +1,189 @@
from __future__ import annotations
import re
import lsprotocol.types as lsp
# Precompiled regex patterns for performance
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_NAME_LINE_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\b")
def build_document_symbols(source: str) -> list[lsp.DocumentSymbol]:
"""Parse a Tcl document source and return DocumentSymbols.
This mirrors the previous inline implementation in lsp_server, but is extracted
for readability and reuse.
"""
lines = source.split("\n")
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)]
pending_event: dict | None = None
for i, line in enumerate(lines):
# If inside an event, update its brace count for this line
if 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 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)
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
+5
View File
@@ -26,6 +26,11 @@ class Token:
tok_type: str = "" tok_type: str = ""
tok_modifiers: List[TokenModifier] = attrs.field(factory=list) tok_modifiers: List[TokenModifier] = attrs.field(factory=list)
@property
def length(self) -> int:
"""Compatibility alias for misspelled 'lenght' field."""
return self.lenght
TOKEN_TYPES = [ TOKEN_TYPES = [
"keyword", "keyword",