test rust lsp
This commit is contained in:
@@ -1,115 +0,0 @@
|
||||
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
|
||||
@@ -1,116 +0,0 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure server/src is on the path for imports
|
||||
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, goto_definition # type: ignore
|
||||
|
||||
|
||||
def _loc_to_tuple(loc: lsp.Location) -> tuple[str, int, int, int, int]:
|
||||
"""Helper to normalize Location into a tuple for easy asserts."""
|
||||
return (
|
||||
loc.uri,
|
||||
loc.range.start.line,
|
||||
loc.range.start.character,
|
||||
loc.range.end.line,
|
||||
loc.range.end.character,
|
||||
)
|
||||
|
||||
|
||||
def _extract_first_location(result) -> lsp.Location | None:
|
||||
if result is None:
|
||||
return None
|
||||
if isinstance(result, list):
|
||||
return result[0] if result else None
|
||||
return result
|
||||
|
||||
|
||||
def test_goto_definition_same_file(tmp_path: Path):
|
||||
source_lines = [
|
||||
"proc add {a b} {",
|
||||
" return [expr {$a + $b}]",
|
||||
"}",
|
||||
"",
|
||||
"set x [add 1 2]",
|
||||
]
|
||||
source = "\n".join(source_lines)
|
||||
|
||||
uri = Path(tmp_path / "same.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)
|
||||
)
|
||||
|
||||
# Position on the word 'add' in the last line
|
||||
line_idx = 4
|
||||
char_idx = source_lines[line_idx].index("add") + 1 # somewhere inside token
|
||||
|
||||
params = lsp.DefinitionParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=uri),
|
||||
position=lsp.Position(line=line_idx, character=char_idx),
|
||||
)
|
||||
|
||||
result = goto_definition(params)
|
||||
loc = _extract_first_location(result)
|
||||
|
||||
assert loc is not None
|
||||
assert loc.uri == uri
|
||||
# Definition should be on line 0 at the token 'add'
|
||||
start = loc.range.start
|
||||
end = loc.range.end
|
||||
assert start.line == 0
|
||||
assert end.line == 0
|
||||
assert source_lines[0][start.character : end.character] == "add"
|
||||
|
||||
|
||||
def test_goto_definition_cross_file(tmp_path: Path):
|
||||
# File A declares the proc
|
||||
a_lines = [
|
||||
"proc myproc {arg} {",
|
||||
" return $arg",
|
||||
"}",
|
||||
]
|
||||
a_src = "\n".join(a_lines)
|
||||
a_path = tmp_path / "a.tcl"
|
||||
a_uri = a_path.as_uri()
|
||||
LSP_SERVER.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(uri=a_uri, language_id="tcl", version=1, text=a_src)
|
||||
)
|
||||
|
||||
# Update indices for file A so proc_signatures gets populated
|
||||
doc_a = LSP_SERVER.workspace.get_text_document(a_uri)
|
||||
LSP_SERVER.update_poco_completion_for_file(doc_a)
|
||||
|
||||
# File B calls the proc
|
||||
b_lines = [
|
||||
"set y [myproc 42]",
|
||||
]
|
||||
b_src = "\n".join(b_lines)
|
||||
b_uri = (tmp_path / "b.tcl").as_uri()
|
||||
LSP_SERVER.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(uri=b_uri, language_id="tcl", version=1, text=b_src)
|
||||
)
|
||||
|
||||
call_line = 0
|
||||
call_char = b_lines[0].index("myproc") + 2
|
||||
|
||||
params = lsp.DefinitionParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=b_uri),
|
||||
position=lsp.Position(line=call_line, character=call_char),
|
||||
)
|
||||
|
||||
result = goto_definition(params)
|
||||
loc = _extract_first_location(result)
|
||||
|
||||
assert loc is not None
|
||||
assert loc.uri == a_uri
|
||||
start = loc.range.start
|
||||
end = loc.range.end
|
||||
assert start.line == 0
|
||||
assert a_lines[start.line][start.character : end.character] == "myproc"
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure server/src is on the path for imports
|
||||
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 tools.proc_docs import build_proc_docs, is_proc_declaration_position
|
||||
from lsp_server import LSP_SERVER, hover # type: ignore
|
||||
|
||||
|
||||
def test_build_proc_docs_extracts_block_and_tags():
|
||||
source = (
|
||||
"#____________________________________________________________________________________________\n"
|
||||
"# <Documentation>\n"
|
||||
"# This procedure creates a new directory if it does not exist.\n"
|
||||
"# <Arguments>\n"
|
||||
"# directory\n"
|
||||
"#\tThe full pathname of the directory to be created.\n"
|
||||
"# <Returnvalue>\n"
|
||||
"# 0 - directory created or already exists\n"
|
||||
"# 1 - error\n"
|
||||
"#______________________________________________________________________________________________\n"
|
||||
"proc LIB_FH_create_directory {directory} {\n"
|
||||
" return 0\n"
|
||||
"}\n"
|
||||
)
|
||||
|
||||
from tools.parser import CustomParser
|
||||
|
||||
tree = CustomParser().parse(source)
|
||||
docs = build_proc_docs(tree, source)
|
||||
|
||||
assert "LIB_FH_create_directory" in docs
|
||||
md = docs["LIB_FH_create_directory"]
|
||||
# Tags become markdown headings
|
||||
assert "### Documentation" in md
|
||||
assert "### Arguments" in md
|
||||
assert "### Return value" in md
|
||||
# Content preserved
|
||||
assert "creates a new directory" in md
|
||||
|
||||
|
||||
def test_hover_shows_doc_on_usage_but_not_on_declaration(tmp_path: Path):
|
||||
# Build a TCL file with a documented proc and a usage
|
||||
source_lines = [
|
||||
"#_________________________________________________________________________________________________",
|
||||
"# <Documentation>",
|
||||
"# Function to delete the file",
|
||||
"#_________________________________________________________________________________________________",
|
||||
"proc SERVICE_remove_file {file} {",
|
||||
" if {![SERVICE_check_file_exists $file]} {return}",
|
||||
" MOM_remove_file $file",
|
||||
"}",
|
||||
"",
|
||||
"proc SERVICE_check_file_exists {file} {",
|
||||
" if {[file exists $file]} {return 1}",
|
||||
" return 0",
|
||||
"}",
|
||||
"",
|
||||
"# usage below",
|
||||
"SERVICE_remove_file \"C:/tmp/x\"",
|
||||
]
|
||||
source = "\n".join(source_lines)
|
||||
|
||||
# Register document with server
|
||||
uri = Path(tmp_path / "test.tcl").as_uri()
|
||||
LSP_SERVER.workspace.put_text_document(
|
||||
lsp.TextDocumentItem(uri=uri, language_id="tcl", version=1, text=source)
|
||||
)
|
||||
|
||||
# Force server to parse and build proc docs
|
||||
doc = LSP_SERVER.workspace.get_text_document(uri)
|
||||
LSP_SERVER.update_poco_completion_for_file(doc)
|
||||
|
||||
# 1) Hover on usage -> should return docs
|
||||
usage_line = source_lines.index("SERVICE_remove_file \"C:/tmp/x\"")
|
||||
char_index = source_lines[usage_line].find("SERVICE_remove_file") + 5 # inside the token
|
||||
|
||||
params = lsp.HoverParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=uri),
|
||||
position=lsp.Position(line=usage_line, character=char_index),
|
||||
)
|
||||
result = hover(params)
|
||||
assert result is not None
|
||||
assert "Function to delete the file" in result.contents.value # type: ignore[attr-defined]
|
||||
|
||||
# 2) Hover on declaration name -> should be None
|
||||
decl_line = source_lines.index("proc SERVICE_remove_file {file} {")
|
||||
decl_char = source_lines[decl_line].find("SERVICE_remove_file") + 2
|
||||
assert is_proc_declaration_position(source, decl_line, decl_char)
|
||||
|
||||
params_decl = lsp.HoverParams(
|
||||
text_document=lsp.TextDocumentIdentifier(uri=uri),
|
||||
position=lsp.Position(line=decl_line, character=decl_char),
|
||||
)
|
||||
none_result = hover(params_decl)
|
||||
assert none_result is None
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure server/src is on sys.path for imports
|
||||
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 tools.parser import CustomParser
|
||||
from tools.proc_docs import build_proc_docs
|
||||
|
||||
|
||||
def test_example_is_marked_as_tcl_code_block():
|
||||
source_lines = [
|
||||
"#____________________________________________________________________________________________",
|
||||
"# <Documentation>",
|
||||
"# This procedure creates a new directory if it does not exist.",
|
||||
"# <Arguments>",
|
||||
"# directory",
|
||||
"#\tThe full pathname of the directory to be created.",
|
||||
"# <Returnvalue>",
|
||||
"# 0 - directory created or already exists",
|
||||
"# 1 - error",
|
||||
"# <Example>",
|
||||
"# name: Example 1",
|
||||
"# code: LIB_FH_create_directory \"C:/Temp/Test\"",
|
||||
"# desc: If error = 0, the directory is created.",
|
||||
"proc LIB_FH_create_directory {directory} {",
|
||||
" return 0",
|
||||
"}",
|
||||
]
|
||||
source = "\n".join(source_lines)
|
||||
|
||||
tree = CustomParser().parse(source)
|
||||
docs = build_proc_docs(tree, source)
|
||||
|
||||
assert "LIB_FH_create_directory" in docs
|
||||
md = docs["LIB_FH_create_directory"]
|
||||
|
||||
# Headings preserved
|
||||
assert "### Documentation" in md
|
||||
assert "### Arguments" in md
|
||||
assert "### Return value" in md
|
||||
assert "### Example" in md
|
||||
|
||||
# Code fence with tcl language hint and the code line present
|
||||
assert "```tcl" in md
|
||||
assert "LIB_FH_create_directory \"C:/Temp/Test\"" in md
|
||||
assert md.strip().endswith("```")
|
||||
|
||||
|
||||
def test_internal_example_is_marked_as_tcl_code_block():
|
||||
source_lines = [
|
||||
"# <Internal Documentation>",
|
||||
"# Helper utility",
|
||||
"# <Internal Example>",
|
||||
"# code: puts \"hello\"",
|
||||
"proc helper {} {",
|
||||
" return",
|
||||
"}",
|
||||
]
|
||||
source = "\n".join(source_lines)
|
||||
|
||||
tree = CustomParser().parse(source)
|
||||
docs = build_proc_docs(tree, source)
|
||||
|
||||
assert "helper" in docs
|
||||
md = docs["helper"]
|
||||
|
||||
assert "### Internal Documentation" in md
|
||||
assert "### Internal Example" in md
|
||||
assert "```tcl" in md and "puts \"hello\"" in md and md.strip().endswith("```")
|
||||
|
||||
Reference in New Issue
Block a user