diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py index 09b3286..aa33920 100644 --- a/server/src/lsp_server.py +++ b/server/src/lsp_server.py @@ -9,7 +9,7 @@ import os import pathlib import re import sys -from typing import Any, List, Optional, Tuple +from typing import Any, Optional import operator from functools import reduce @@ -49,7 +49,6 @@ from lsp_tclserver import TclLanguageServer WORKSPACE_SETTINGS = {} GLOBAL_SETTINGS = {} -RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py" 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_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. # ********************************************************** @@ -87,7 +84,7 @@ def did_save(params: lsp.DidSaveTextDocumentParams) -> None: @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.""" @@ -125,12 +122,9 @@ def document_diagnostic(params: lsp.DocumentDiagnosticParams): @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) - poco = [] - - for key, value in LSP_SERVER.poco_completion.items(): - poco.extend(value) + poco = [item for items in LSP_SERVER.poco_completion.values() for item in items] items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco 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) 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) - 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 + return build_document_symbols(doc.source) @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) tree = LSP_SERVER.parser.parse(document.source) - # collect Inlay Hints - hints = [] - for key, value in LSP_SERVER.proc_signatures.items(): - generator = InlayHintGenerator(LSP_SERVER.proc_signatures[key]) - tree.accept(generator, recurse=True) - hints += generator.hints - return hints + # Merge proc signatures across files and traverse once + merged_signatures = {} + for sigs in LSP_SERVER.proc_signatures.values(): + merged_signatures.update(sigs) + + generator = InlayHintGenerator(merged_signatures) + tree.accept(generator, recurse=True) + return generator.hints @LSP_SERVER.feature( @@ -364,7 +186,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams): [ token.line, token.offset, - token.lenght, + token.length, TOKEN_TYPES.index(token.tok_type), 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 # 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(): if token in procs: 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. # ********************************************************** @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""" @@ -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") semantic_tokens_legend = lsp.SemanticTokensLegend( token_types=TOKEN_TYPES, - token_modifiers=TokenModifier, + token_modifiers=[m.name for m in TokenModifier], ) return lsp.InitializeResult( capabilities=lsp.ServerCapabilities( diff --git a/server/src/tools/document_symbols.py b/server/src/tools/document_symbols.py new file mode 100644 index 0000000..7f976ef --- /dev/null +++ b/server/src/tools/document_symbols.py @@ -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 + diff --git a/server/src/tools/semantic_tokens.py b/server/src/tools/semantic_tokens.py index 9c5eb4f..b35c155 100644 --- a/server/src/tools/semantic_tokens.py +++ b/server/src/tools/semantic_tokens.py @@ -26,6 +26,11 @@ class Token: tok_type: str = "" tok_modifiers: List[TokenModifier] = attrs.field(factory=list) + @property + def length(self) -> int: + """Compatibility alias for misspelled 'lenght' field.""" + return self.lenght + TOKEN_TYPES = [ "keyword",