add functions to completion

This commit is contained in:
Christoph Brandau
2025-08-06 15:26:53 +02:00
parent f5c61267e3
commit 6bded783a1
5 changed files with 86 additions and 78 deletions
+2 -1
View File
@@ -2,6 +2,7 @@
"ruff.configuration": { "ruff.configuration": {
"lint": { "lint": {
"extend-ignore": ["F821", "E402"] "extend-ignore": ["F821", "E402"]
} },
"line-length": 200
} }
} }
+1 -1
View File
@@ -32,7 +32,7 @@ if debugger_path:
# This will ensure that execution is paused as soon as the debugger # This will ensure that execution is paused as soon as the debugger
# connects to VS Code. If you don't want to pause here comment this # connects to VS Code. If you don't want to pause here comment this
# line and set breakpoints as appropriate. # line and set breakpoints as appropriate.
debugpy.breakpoint() # debugpy.breakpoint()
SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py") SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py")
# NOTE: Set breakpoint in `lsp_server.py` before continuing. # NOTE: Set breakpoint in `lsp_server.py` before continuing.
+26 -46
View File
@@ -47,9 +47,9 @@ from tclint.violations import Violation
from plugins.poco_plugin import commands from plugins.poco_plugin import commands
from tools import checks, parser from tools import checks, parser
from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier from tools.semantic_tokens import _Highlighter, TOKEN_TYPES, TokenModifier
from tools.completion_items import completion from tools.completion_items import completion, remove_existing_items, remove_shared_keys
from tools.inlay_hint import InlayHintGenerator from tools.inlay_hint import InlayHintGenerator
from tools.file_sourcing import get_all_psc_files, read_psc_file, SourcedFile from tools.file_sourcing import get_all_psc_files, read_psc_file
DIAGNOSTIC_SOURCE = "nx-post-support" DIAGNOSTIC_SOURCE = "nx-post-support"
@@ -62,6 +62,7 @@ class TclLanguageServer(server.LanguageServer):
self.parser._commands.update(command) self.parser._commands.update(command)
self.diagnostics = {} self.diagnostics = {}
self.poco_completion: dict = {} self.poco_completion: dict = {}
self.proc_signatures: dict = {}
def format( def format(
self, self,
@@ -123,12 +124,8 @@ class TclLanguageServer(server.LanguageServer):
for violation in violations: for violation in violations:
message = violation.message message = violation.message
severity = lsp.DiagnosticSeverity.Warning severity = lsp.DiagnosticSeverity.Warning
start = lsp.Position( start = lsp.Position(line=violation.start[0] - 1, character=violation.start[1] - 1)
line=violation.start[0] - 1, character=violation.start[1] - 1 end = lsp.Position(line=violation.end[0] - 1, character=violation.end[1] - 1)
)
end = lsp.Position(
line=violation.end[0] - 1, character=violation.end[1] - 1
)
diagnostics.append( diagnostics.append(
lsp.Diagnostic( lsp.Diagnostic(
@@ -166,9 +163,7 @@ RUNNER = pathlib.Path(__file__).parent / "lsp_runner.py"
MAX_WORKERS = 5 MAX_WORKERS = 5
LSP_SERVER = TclLanguageServer( LSP_SERVER = TclLanguageServer(name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS)
name="NX Postprocessor Support", version="0.0.1", max_workers=MAX_WORKERS
)
# ********************************************************** # **********************************************************
# Tool specific code goes below this. # Tool specific code goes below this.
@@ -243,12 +238,7 @@ def on_completion(params: lsp.CompletionParams) -> list[lsp.CompletionItem]:
for key, value in LSP_SERVER.poco_completion.items(): for key, value in LSP_SERVER.poco_completion.items():
poco.extend(value) poco.extend(value)
items = ( items = standard_items.tcl_keyword_list + standard_items.nx_procs + standard_items.nx_variables + poco
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)
@@ -267,10 +257,12 @@ def inlay_hints(params: lsp.InlayHintParams):
tree = LSP_SERVER.parser.parse(document.source) tree = LSP_SERVER.parser.parse(document.source)
# collect Inlay Hints # collect Inlay Hints
generator = InlayHintGenerator(completion.proc_signatures) hints = []
tree.accept(generator, recurse=True) for key, value in LSP_SERVER.proc_signatures.items():
generator = InlayHintGenerator(LSP_SERVER.proc_signatures[key])
return generator.hints tree.accept(generator, recurse=True)
hints += generator.hints
return hints
@LSP_SERVER.feature( @LSP_SERVER.feature(
@@ -285,7 +277,7 @@ def semantic_tokens(params: lsp.SemanticTokensParams):
data = [] data = []
plugins = [] plugins = []
hl = _Highlighter(plugins, log_to_output=log_to_output) hl = _Highlighter(plugins, LSP_SERVER.poco_completion)
tree = LSP_SERVER.parser.parse(document.source) tree = LSP_SERVER.parser.parse(document.source)
tree.accept(hl, recurse=True) tree.accept(hl, recurse=True)
@@ -338,9 +330,7 @@ def hover(params: lsp.HoverParams) -> lsp.Hover:
label = match.get("label", "") label = match.get("label", "")
parameters = match.get("parameters", []) parameters = match.get("parameters", [])
param_lines = ( param_lines = "\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
"\n".join(f"- `{p['name']}`: {p['desc']}" for p in parameters) or "_None_"
)
example_data = match.get("example", []) example_data = match.get("example", [])
example_md = "\n".join(f"{line}" for line in example_data) example_md = "\n".join(f"{line}" for line in example_data)
@@ -424,12 +414,8 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
settings = params.initialization_options["settings"] settings = params.initialization_options["settings"]
_update_workspace_settings(settings) _update_workspace_settings(settings)
log_to_output( log_to_output(f"Settings used to run Server:\r\n{json.dumps(settings, indent=4, ensure_ascii=False)}\r\n")
f"Settings used to run Server:\r\n{json.dumps(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")
)
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=TokenModifier,
@@ -437,9 +423,7 @@ def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
return lsp.InitializeResult( return lsp.InitializeResult(
capabilities=lsp.ServerCapabilities( capabilities=lsp.ServerCapabilities(
document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True), document_formatting_provider=GLOBAL_SETTINGS.get("formatter", True),
semantic_tokens_provider=lsp.SemanticTokensOptions( semantic_tokens_provider=lsp.SemanticTokensOptions(legend=semantic_tokens_legend, full=True, range=False),
legend=semantic_tokens_legend, full=True, range=False
),
) )
) )
@@ -453,21 +437,19 @@ def initialized(params: lsp.InitializedParams):
for sourced_layer in poco_files: for sourced_layer in poco_files:
completion.reset() completion.reset()
try: try:
file_root = pathlib.Path(root).joinpath( file_root = pathlib.Path(root).joinpath(sourced_layer.subfolder if sourced_layer.subfolder else "")
sourced_layer.subfolder if sourced_layer.subfolder else ""
)
for tcl_file in sourced_layer.files: for tcl_file in sourced_layer.files:
filepath = pathlib.Path(file_root).joinpath(f"{tcl_file}.tcl") filepath = pathlib.Path(file_root).joinpath(f"{tcl_file}.tcl")
if not filepath.exists(): if not filepath.exists():
continue continue
completion.reset() completion.reset()
source = filepath.read_text(encoding="utf-8") document = LSP_SERVER.workspace.get_text_document(filepath.as_uri()) # filepath.read_text(encoding="utf-8")
tree = LSP_SERVER.parser.parse(source) tree = LSP_SERVER.parser.parse(document.source)
tree.accept(completion, recurse=True) tree.accept(completion, recurse=True)
remove_existing_items(completion.custom_functions, LSP_SERVER.poco_completion)
LSP_SERVER.poco_completion[str(filepath)] = ( LSP_SERVER.poco_completion[str(filepath)] = completion.custom_functions
completion.custom_functions remove_shared_keys(LSP_SERVER.proc_signatures, completion.proc_signatures)
) LSP_SERVER.proc_signatures[str(filepath)] = completion.proc_signatures
except Exception as e: except Exception as e:
log_to_output(f"Fehler beim Parsen von {filepath}: {e}") log_to_output(f"Fehler beim Parsen von {filepath}: {e}")
@@ -563,9 +545,7 @@ def _get_settings_by_document(document: workspace.Document | None):
# ***************************************************** # *****************************************************
# Logging and notification. # Logging and notification.
# ***************************************************** # *****************************************************
def log_to_output( def log_to_output(message: str, msg_type: lsp.MessageType = lsp.MessageType.Log) -> None:
message: str, msg_type: lsp.MessageType = lsp.MessageType.Log
) -> None:
LSP_SERVER.show_message_log(message, msg_type) LSP_SERVER.show_message_log(message, msg_type)
+42 -8
View File
@@ -1,5 +1,6 @@
from tclint.syntax_tree import Visitor, Command, BareWord, List from tclint.syntax_tree import Visitor, Command, BareWord, List
import lsprotocol.types as lsp import lsprotocol.types as lsp
from common.load_data import standard_items
class CompletionItems: class CompletionItems:
@@ -38,14 +39,18 @@ class _Completion(Visitor):
if routine.contents == "proc" and command.args: if routine.contents == "proc" and command.args:
first_arg = command.args[0] first_arg = command.args[0]
if hasattr(first_arg, "value") and not any( if not first_arg.value:
item.label == first_arg.value for item in self._custom_functions return
):
self._custom_functions.append( if any(item.label == first_arg.value for item in standard_items.nx_procs):
lsp.CompletionItem( return
label=first_arg.value, kind=lsp.CompletionItemKind.Function
) try:
) self._custom_functions.remove(first_arg.value)
except ValueError:
pass
self._custom_functions.append(lsp.CompletionItem(label=first_arg.value, kind=lsp.CompletionItemKind.Function))
if len(command.args) < 2: if len(command.args) < 2:
return return
@@ -65,4 +70,33 @@ class _Completion(Visitor):
self._proc_signatures[first_arg.value] = param_names self._proc_signatures[first_arg.value] = param_names
def remove_existing_items(items: list[lsp.CompletionItem], store: dict) -> None:
"""
Entfernt alle CompletionItems aus dem store, deren label in der items-Liste vorkommt.
Änderungen erfolgen in-place.
"""
labels_to_remove = {item.label for item in items}
for key in list(store.keys()):
filtered = [ci for ci in store[key] if ci.label not in labels_to_remove]
if filtered:
store[key] = filtered
else:
del store[key]
def remove_shared_keys(nested_dict: dict[str, dict[str, list]], flat_dict: dict[str, list]) -> None:
"""
Entfernt alle Keys aus nested_dict[file][func], wenn func auch in flat_dict vorhanden ist.
Änderungen erfolgen in-place.
"""
for file_path, func_dict in list(nested_dict.items()):
for func_name in list(func_dict.keys()):
if func_name in flat_dict:
del nested_dict[file_path][func_name]
if not nested_dict[file_path]:
del nested_dict[file_path]
completion = _Completion() completion = _Completion()
+15 -22
View File
@@ -5,6 +5,7 @@ from tclint.commands import get_commands
import attrs import attrs
from common.load_data import standard_items from common.load_data import standard_items
from tools.completion_items import completion from tools.completion_items import completion
import lsprotocol.types as lsp
class TokenModifier(enum.IntFlag): class TokenModifier(enum.IntFlag):
@@ -40,10 +41,10 @@ TOKEN_TYPES = [
class _Highlighter(Visitor): class _Highlighter(Visitor):
def __init__(self, plugins, log_to_output): def __init__(self, plugins, custom_functions: dict[str : list[lsp.CompletionItem]]):
self._commands = get_commands(plugins) self._commands = get_commands(plugins)
self._tokens = [] self._tokens = []
self.log_to_output = log_to_output self.custom_functions = custom_functions
def _get_token_info(self, node): def _get_token_info(self, node):
"""Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren.""" """Hilfsmethode um Token-Informationen aus verschiedenen Node-Typen zu extrahieren."""
@@ -58,11 +59,7 @@ class _Highlighter(Visitor):
# CompoundBareWord: versuche erstes Segment # CompoundBareWord: versuche erstes Segment
if hasattr(node, "children") and node.children: if hasattr(node, "children") and node.children:
first_segment = node.children[0] first_segment = node.children[0]
if ( if hasattr(first_segment, "value") and first_segment.value is not None and hasattr(first_segment, "pos"):
hasattr(first_segment, "value")
and first_segment.value is not None
and hasattr(first_segment, "pos")
):
line, col = first_segment.pos line, col = first_segment.pos
return (line - 1, col - 1), len(first_segment.value) return (line - 1, col - 1), len(first_segment.value)
@@ -84,13 +81,15 @@ class _Highlighter(Visitor):
pass pass
def visit_bare_word(self, word: BareWord): def visit_bare_word(self, word: BareWord):
if any(item.label == word.value for item in standard_items.nx_procs) or any( name = word.value
item.label == word.value for item in completion.custom_functions
): in_standard = any(item.label == name for item in standard_items.nx_procs)
in_custom = any(item.label == name for items in self.custom_functions.values() for item in items)
if in_standard or in_custom:
line, col = word.pos line, col = word.pos
self._tokens.append( self._tokens.append((((line - 1, col - 1), len(name), "function", [])))
(((line - 1, col - 1), len(word.value), "function", []))
)
def visit_command(self, command: Command): def visit_command(self, command: Command):
routine = command.routine routine = command.routine
@@ -158,9 +157,7 @@ class _Highlighter(Visitor):
# Parameter mit Default-Wert ist meist eine List (z.B. {arg default}) # Parameter mit Default-Wert ist meist eine List (z.B. {arg default})
elif hasattr(child, "children") and len(child.children) >= 1: elif hasattr(child, "children") and len(child.children) >= 1:
name_node = child.children[0] name_node = child.children[0]
if hasattr(name_node, "value") and hasattr( if hasattr(name_node, "value") and hasattr(name_node, "pos"):
name_node, "pos"
):
line, col = name_node.pos line, col = name_node.pos
self._tokens.append( self._tokens.append(
( (
@@ -174,9 +171,7 @@ class _Highlighter(Visitor):
first_arg = command.args[1] first_arg = command.args[1]
if hasattr(first_arg, "pos") and first_arg.value is not None: if hasattr(first_arg, "pos") and first_arg.value is not None:
line, col = first_arg.pos line, col = first_arg.pos
self._tokens.append( self._tokens.append((((line - 1, col - 1), len(first_arg.value), "class", [])))
(((line - 1, col - 1), len(first_arg.value), "class", []))
)
def tokens(self) -> list[Token]: def tokens(self) -> list[Token]:
"""Encode tokens as described in """Encode tokens as described in
@@ -185,9 +180,7 @@ class _Highlighter(Visitor):
tokens = [] tokens = []
last_line = 0 last_line = 0
last_col = 0 last_col = 0
for (line, col), length, tok_type, tok_modifier in sorted( for (line, col), length, tok_type, tok_modifier in sorted(self._tokens, key=lambda x: x[0]):
self._tokens, key=lambda x: x[0]
):
line_delta = line - last_line line_delta = line - last_line
col_delta = col col_delta = col
if line == last_line: if line == last_line: