#![allow(clippy::mutable_key_type)]
mod common;
use lsp_types::{
CallHierarchyPrepareParams, CodeActionContext, CodeActionParams, CodeLensParams,
CompletionParams, CompletionTriggerKind, DocumentFormattingParams, DocumentLinkParams,
DocumentSymbolParams, FoldingRangeParams, FormattingOptions, GotoDefinitionParams,
HoverContents, HoverParams, InlayHintParams, Position, Range, ReferenceContext,
ReferenceParams, SemanticTokensParams, SignatureHelpParams, TextDocumentIdentifier,
TextDocumentPositionParams, Uri, WorkspaceSymbolParams,
};
use rumoca::lsp::{
WorkspaceState, compute_diagnostics, create_documents, get_semantic_token_legend,
handle_code_action, handle_code_lens, handle_completion_workspace, handle_document_links,
handle_document_symbols, handle_folding_range, handle_formatting, handle_goto_definition,
handle_hover, handle_inlay_hints, handle_prepare_call_hierarchy, handle_references,
handle_semantic_tokens, handle_signature_help, handle_workspace_symbol,
};
use common::lsp::test_uri;
#[test]
fn test_diagnostics_valid_model() {
let uri = test_uri();
let text = r#"model Test
Real x;
equation
der(x) = 1;
end Test;"#;
let mut workspace = WorkspaceState::new();
let diagnostics = compute_diagnostics(&uri, text, &mut workspace);
assert!(
diagnostics.is_empty(),
"Expected no diagnostics for valid model"
);
}
#[test]
fn test_diagnostics_syntax_error() {
let uri = test_uri();
let text = "model Test\n Real x\nend Test;";
let mut workspace = WorkspaceState::new();
let diagnostics = compute_diagnostics(&uri, text, &mut workspace);
assert!(
!diagnostics.is_empty(),
"Expected diagnostics for syntax error"
);
}
#[test]
fn test_diagnostics_inherited_variables() {
let uri = test_uri();
let text = r#"
block SportCub
extends RigidBody;
parameter Real g = 9.81;
equation
F_e = {0, 0, -m*g};
M_b = {0, 0, 0};
end SportCub;
model RigidBody
parameter Real m = 1.0;
Real F_e[3] "external force";
Real M_b[3] "external moment";
equation
F_e = m * {0, 0, 0};
M_b = {0, 0, 0};
end RigidBody;
"#;
let mut workspace = WorkspaceState::new();
let diagnostics = compute_diagnostics(&uri, text, &mut workspace);
let undefined_errors: Vec<_> = diagnostics
.iter()
.filter(|d| {
d.message.contains("Undefined")
&& (d.message.contains("F_e")
|| d.message.contains("M_b")
|| d.message.contains("m"))
})
.collect();
assert!(
undefined_errors.is_empty(),
"F_e, M_b, and m should be recognized as inherited from RigidBody, but got: {:?}",
undefined_errors
.iter()
.map(|d| &d.message)
.collect::<Vec<_>>()
);
}
#[test]
fn test_diagnostics_inherited_components_not_unused() {
let uri = test_uri();
let text = r#"
block SportCub
extends RigidBody;
parameter Real g = 9.81;
equation
F_e = {0, 0, -m*g};
M_b = {0, 0, 0};
end SportCub;
model RigidBody
parameter Real m = 1.0;
Real F_e[3] "external force";
Real M_b[3] "external moment";
Real J[3,3] "inertia matrix"; // Used in RigidBody's equations but not directly in SportCub
equation
J = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
F_e = m * {0, 0, 0};
M_b = J * {0, 0, 0}; // J is used here in base class
end RigidBody;
"#;
let mut workspace = WorkspaceState::new();
let diagnostics = compute_diagnostics(&uri, text, &mut workspace);
let j_unused_warnings: Vec<_> = diagnostics
.iter()
.filter(|d| d.message.contains("'J'") && d.message.contains("unused"))
.collect();
assert!(
j_unused_warnings.is_empty(),
"Inherited component J should NOT be flagged as unused in SportCub: {:?}",
j_unused_warnings
.iter()
.map(|d| &d.message)
.collect::<Vec<_>>()
);
}
#[test]
fn test_diagnostics_array_indexing_type_inference() {
let uri = test_uri();
let text = r#"
function quatToRot
input Real q[4] "quaternion";
output Real R[3,3] "rotation matrix";
algorithm
R[1,1] := 1 - 2*(q[3]^2 + q[4]^2);
R[1,2] := 2*(q[2]*q[3] - q[1]*q[4]);
end quatToRot;
"#;
let mut workspace = WorkspaceState::new();
let diagnostics = compute_diagnostics(&uri, text, &mut workspace);
let type_errors: Vec<_> = diagnostics
.iter()
.filter(|d| d.message.contains("not compatible") && d.message.contains("Real["))
.collect();
assert!(
type_errors.is_empty(),
"Array indexing should reduce type from Real[4] to Real, but got type errors: {:?}",
type_errors.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}
#[test]
fn test_document_symbols_model() {
let uri = test_uri();
let text = r#"model Test
parameter Real k = 1.0;
Real x(start = 0);
equation
der(x) = k * x;
end Test;"#;
let documents = create_documents(&uri, text);
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_document_symbols(&documents, params);
assert!(result.is_some(), "Expected document symbols");
if let Some(lsp_types::DocumentSymbolResponse::Nested(symbols)) = result {
assert!(!symbols.is_empty(), "Expected at least one symbol");
assert!(
symbols.iter().any(|s| s.name == "Test"),
"Expected Test model symbol"
);
}
}
#[test]
fn test_document_symbols_nested_classes() {
let uri = test_uri();
let text = r#"package MyPackage
model Inner
Real x;
end Inner;
end MyPackage;"#;
let documents = create_documents(&uri, text);
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_document_symbols(&documents, params);
assert!(result.is_some());
}
fn validate_symbol_ranges(symbol: &lsp_types::DocumentSymbol) -> Result<(), String> {
let range = &symbol.range;
let sel_range = &symbol.selection_range;
let start_ok = sel_range.start.line > range.start.line
|| (sel_range.start.line == range.start.line
&& sel_range.start.character >= range.start.character);
let end_ok = sel_range.end.line < range.end.line
|| (sel_range.end.line == range.end.line && sel_range.end.character <= range.end.character);
if !start_ok || !end_ok {
return Err(format!(
"Symbol '{}': selectionRange {:?} is not contained in range {:?}",
symbol.name, sel_range, range
));
}
if let Some(children) = &symbol.children {
for child in children {
validate_symbol_ranges(child)?;
}
}
Ok(())
}
#[test]
fn test_document_symbols_range_containment() {
let uri = test_uri();
let text = r#"class SimpleCircuit
Resistor R1(R=10);
Capacitor C(C=0.01);
Resistor R2(R=100);
Inductor L1(L=0.1);
VsourceAC AC;
Ground G;
equation
connect(AC.p, R1.p);
connect(R1.n, C.p);
connect(C.n, AC.n);
connect(R1.p, R2.p);
connect(R2.n, L1.p);
connect(L1.n, C.n);
connect(AC.n, G.p);
end SimpleCircuit;"#;
let documents = create_documents(&uri, text);
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_document_symbols(&documents, params);
assert!(result.is_some(), "Expected document symbols");
if let Some(lsp_types::DocumentSymbolResponse::Nested(symbols)) = result {
for symbol in &symbols {
if let Err(e) = validate_symbol_ranges(symbol) {
panic!("Range validation failed: {}", e);
}
}
}
}
#[test]
fn test_hover_on_type() {
let uri = test_uri();
let text = r#"model Test
Real x;
end Test;"#;
let documents = create_documents(&uri, text);
let params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 1,
character: 3,
}, },
work_done_progress_params: Default::default(),
};
let result = handle_hover(&documents, params);
assert!(result.is_some(), "Expected hover information for type");
}
#[test]
fn test_hover_on_variable() {
let uri = test_uri();
let text = r#"model Test
Real x;
end Test;"#;
let documents = create_documents(&uri, text);
let params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 1,
character: 7,
}, },
work_done_progress_params: Default::default(),
};
let result = handle_hover(&documents, params);
assert!(result.is_some(), "Expected hover information for variable");
}
#[test]
fn test_hover_on_inherited_variable() {
let uri = test_uri();
let text = r#"partial class TwoPin
Real v;
Real i;
equation
v = 1;
end TwoPin;
class Capacitor
extends TwoPin;
parameter Real C = 1.0;
equation
C * der(v) = i;
end Capacitor;"#;
let documents = create_documents(&uri, text);
let params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 11,
character: 10,
}, },
work_done_progress_params: Default::default(),
};
let result = handle_hover(&documents, params);
assert!(
result.is_some(),
"Expected hover information for inherited variable 'v'"
);
if let Some(hover) = result {
if let HoverContents::Markup(markup) = hover.contents {
assert!(
markup.value.contains("v") && markup.value.contains("Real"),
"Hover should show 'v: Real', got: {}",
markup.value
);
assert!(
markup.value.contains("TwoPin"),
"Hover should indicate inheritance from TwoPin, got: {}",
markup.value
);
} else {
panic!("Expected Markup hover contents");
}
}
}
#[test]
fn test_hover_on_class_shows_flattened_content() {
let uri = test_uri();
let text = r#"model RigidBody
parameter Real m = 1.0;
Real F_e[3] "external force";
Real M_b[3] "external moment";
equation
F_e = m * {0, 0, 0};
M_b = {0, 0, 0};
end RigidBody;
block SportCub
extends RigidBody;
parameter Real g = 9.81;
equation
F_e = {0, 0, -m*g};
M_b = {0, 0, 0};
end SportCub;"#;
let documents = create_documents(&uri, text);
let params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 9,
character: 8,
}, },
work_done_progress_params: Default::default(),
};
let result = handle_hover(&documents, params);
assert!(
result.is_some(),
"Expected hover information for class SportCub"
);
if let Some(hover) = result {
if let HoverContents::Markup(markup) = hover.contents {
eprintln!("Hover content:\n{}", markup.value);
assert!(
markup.value.contains("F_e"),
"Hover should show inherited F_e from RigidBody, got:\n{}",
markup.value
);
assert!(
markup.value.contains("M_b"),
"Hover should show inherited M_b from RigidBody, got:\n{}",
markup.value
);
assert!(
markup.value.contains("m"),
"Hover should show inherited parameter m from RigidBody, got:\n{}",
markup.value
);
} else {
panic!("Expected Markup hover contents");
}
}
}
#[test]
fn test_goto_definition_variable() {
let uri = test_uri();
let text = r#"model Test
Real x;
equation
der(x) = 1;
end Test;"#;
let documents = create_documents(&uri, text);
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 1,
character: 7,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_goto_definition(&documents, params);
let _ = result; }
#[test]
fn test_goto_definition_class() {
let uri = test_uri();
let text = r#"model Test
Real x;
end Test;"#;
let documents = create_documents(&uri, text);
let params = GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 0,
character: 6,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_goto_definition(&documents, params);
let _ = result;
}
#[test]
fn test_completion_keywords() {
let uri = test_uri();
let text = "mod";
let _documents = create_documents(&uri, text);
let mut workspace = WorkspaceState::new();
workspace.update_document(uri.clone(), text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 0,
character: 3,
},
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: Some(lsp_types::CompletionContext {
trigger_kind: CompletionTriggerKind::INVOKED,
trigger_character: None,
}),
};
let result = handle_completion_workspace(&mut workspace, params);
assert!(result.is_some(), "Expected completion items");
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
assert!(
items.iter().any(|i| i.label == "model"),
"Expected 'model' keyword in completions"
);
}
}
#[test]
fn test_completion_builtin_functions() {
let uri = test_uri();
let text = r#"model Test
Real x;
equation
x = sin
end Test;"#;
let _documents = create_documents(&uri, text);
let mut workspace = WorkspaceState::new();
workspace.update_document(uri.clone(), text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 9,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: None,
};
let result = handle_completion_workspace(&mut workspace, params);
assert!(result.is_some(), "Expected completion items");
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
assert!(
items.iter().any(|i| i.label == "sin"),
"Expected 'sin' function in completions"
);
}
}
#[test]
fn test_completion_modifiers() {
let uri = test_uri();
let text = r#"model Test
Real h(
end Test;"#;
let _documents = create_documents(&uri, text);
let mut workspace = WorkspaceState::new();
workspace.update_document(uri.clone(), text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 1,
character: 9,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: None,
};
let result = handle_completion_workspace(&mut workspace, params);
assert!(result.is_some(), "Expected completion items for modifiers");
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"start"),
"Expected 'start' modifier in completions, got: {:?}",
labels
);
assert!(
labels.contains(&"fixed"),
"Expected 'fixed' modifier in completions, got: {:?}",
labels
);
assert!(
labels.contains(&"min"),
"Expected 'min' modifier in completions, got: {:?}",
labels
);
assert!(
labels.contains(&"max"),
"Expected 'max' modifier in completions, got: {:?}",
labels
);
assert!(
labels.contains(&"unit"),
"Expected 'unit' modifier in completions, got: {:?}",
labels
);
assert!(
labels.contains(&"stateSelect"),
"Expected 'stateSelect' modifier in completions, got: {:?}",
labels
);
assert!(
!labels.contains(&"model"),
"Should NOT have 'model' keyword in modifier completions"
);
}
}
#[test]
fn test_completion_modifiers_after_comma() {
let uri = test_uri();
let text = r#"model Test
Real h(start=10.0,
end Test;"#;
let _documents = create_documents(&uri, text);
let mut workspace = WorkspaceState::new();
workspace.update_document(uri.clone(), text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 1,
character: 21,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: None,
};
let result = handle_completion_workspace(&mut workspace, params);
assert!(
result.is_some(),
"Expected completion items for modifiers after comma"
);
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"fixed"),
"Expected 'fixed' modifier in completions after comma, got: {:?}",
labels
);
}
}
#[test]
fn test_completion_member_access() {
let uri = test_uri();
let text = r#"model BouncingBall
parameter Real g = 9.81;
Real h(start = 10.0);
Real v(start = 0.0);
equation
der(h) = v;
der(v) = -g;
end BouncingBall;
model B
BouncingBall ball;
equation
ball.h = 1;
end B;"#;
let _documents = create_documents(&uri, text);
let mut workspace = WorkspaceState::new();
workspace.update_document(uri.clone(), text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 12,
character: 7,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: Some(lsp_types::CompletionContext {
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(".".to_string()),
}),
};
let result = handle_completion_workspace(&mut workspace, params);
assert!(result.is_some(), "Expected completion items for ball.");
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"g"),
"Expected 'g' parameter in completions, got: {:?}",
labels
);
assert!(
labels.contains(&"h"),
"Expected 'h' variable in completions, got: {:?}",
labels
);
assert!(
labels.contains(&"v"),
"Expected 'v' variable in completions, got: {:?}",
labels
);
}
}
#[test]
fn test_signature_help_builtin_function() {
let uri = test_uri();
let text = r#"model Test
Real x;
equation
x = sin(
end Test;"#;
let documents = create_documents(&uri, text);
let params = SignatureHelpParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 3,
character: 10,
}, },
work_done_progress_params: Default::default(),
context: None,
};
let result = handle_signature_help(&documents, params);
assert!(result.is_some(), "Expected signature help for sin()");
if let Some(sig_help) = result {
assert!(
!sig_help.signatures.is_empty(),
"Expected at least one signature"
);
}
}
#[test]
fn test_references_variable() {
let uri = test_uri();
let text = r#"model Test
Real x;
Real y;
equation
der(x) = y;
y = x + 1;
end Test;"#;
let documents = create_documents(&uri, text);
let params = ReferenceParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 1,
character: 7,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: ReferenceContext {
include_declaration: true,
},
};
let result = handle_references(&documents, params);
assert!(result.is_some(), "Expected references");
if let Some(refs) = result {
assert!(refs.len() >= 2, "Expected multiple references to x");
}
}
#[test]
fn test_folding_range_model() {
let uri = test_uri();
let text = r#"model Test
Real x;
Real y;
equation
der(x) = 1;
y = x;
end Test;"#;
let documents = create_documents(&uri, text);
let params = FoldingRangeParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_folding_range(&documents, params);
assert!(result.is_some(), "Expected folding ranges");
if let Some(ranges) = result {
assert!(!ranges.is_empty(), "Expected at least one folding range");
}
}
#[test]
fn test_folding_range_comments() {
let uri = test_uri();
let text = r#"// This is a comment
// spanning multiple
// lines
model Test
end Test;"#;
let documents = create_documents(&uri, text);
let params = FoldingRangeParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_folding_range(&documents, params);
assert!(result.is_some());
}
#[test]
fn test_code_action_on_diagnostic() {
let uri = test_uri();
let text = r#"model Test
Real x;
end Test;"#;
let documents = create_documents(&uri, text);
let params = CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 2,
character: 9,
},
},
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_code_action(&documents, params);
assert!(result.is_some());
}
#[test]
fn test_inlay_hints_function_params() {
let uri = test_uri();
let text = r#"model Test
Real x;
equation
x = sin(3.14);
end Test;"#;
let documents = create_documents(&uri, text);
let params = InlayHintParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 4,
character: 9,
},
},
work_done_progress_params: Default::default(),
};
let result = handle_inlay_hints(&documents, params);
assert!(result.is_some());
}
#[test]
fn test_semantic_tokens_legend() {
let legend = get_semantic_token_legend();
assert!(
!legend.token_types.is_empty(),
"Expected token types in legend"
);
}
#[test]
fn test_semantic_tokens_model() {
let uri = test_uri();
let text = r#"model Test
parameter Real k = 1.0;
Real x;
equation
der(x) = k * x;
end Test;"#;
let documents = create_documents(&uri, text);
let params = SemanticTokensParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_semantic_tokens(&documents, params);
assert!(result.is_some(), "Expected semantic tokens");
if let Some(lsp_types::SemanticTokensResult::Tokens(tokens)) = result {
assert!(!tokens.data.is_empty(), "Expected token data");
}
}
#[test]
fn test_workspace_symbols_search() {
let uri = test_uri();
let text = r#"model TestModel
Real x;
end TestModel;
function TestFunction
input Real x;
output Real y;
algorithm
y := x * 2;
end TestFunction;"#;
let documents = create_documents(&uri, text);
let params = WorkspaceSymbolParams {
query: "Test".to_string(),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_workspace_symbol(&documents, params);
assert!(result.is_some());
if let Some(symbols) = result {
assert!(
symbols.iter().any(|s| s.name.contains("Test")),
"Expected symbols matching 'Test'"
);
}
}
#[test]
fn test_workspace_symbols_empty_query() {
let uri = test_uri();
let text = r#"model MyModel
end MyModel;"#;
let documents = create_documents(&uri, text);
let params = WorkspaceSymbolParams {
query: "".to_string(),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_workspace_symbol(&documents, params);
assert!(result.is_some());
}
#[test]
fn test_formatting_indentation() {
let uri = test_uri();
let text = "model Test\nReal x;\nend Test;";
let documents = create_documents(&uri, text);
let params = DocumentFormattingParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
options: FormattingOptions {
tab_size: 2,
insert_spaces: true,
..Default::default()
},
work_done_progress_params: Default::default(),
};
let result = handle_formatting(&documents, params);
assert!(result.is_some(), "Expected formatting result");
if let Some(edits) = result
&& !edits.is_empty()
{
let new_text = &edits[0].new_text;
assert!(
new_text.contains(" Real x;"),
"Expected proper indentation"
);
}
}
#[test]
fn test_formatting_operators() {
let uri = test_uri();
let text = r#"model Test
Real x;
equation
x=1+2*3;
end Test;"#;
let documents = create_documents(&uri, text);
let params = DocumentFormattingParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
options: FormattingOptions {
tab_size: 2,
insert_spaces: true,
..Default::default()
},
work_done_progress_params: Default::default(),
};
let result = handle_formatting(&documents, params);
assert!(result.is_some());
if let Some(edits) = result
&& !edits.is_empty()
{
let new_text = &edits[0].new_text;
assert!(
new_text.contains("x = 1 + 2 * 3"),
"Expected spaces around operators"
);
}
}
#[test]
fn test_code_lens_model() {
let uri = test_uri();
let text = r#"model Test
Real x;
Real y;
equation
der(x) = 1;
y = x;
end Test;"#;
let mut workspace = WorkspaceState::new();
workspace.open_document(uri.clone(), text.to_string());
compute_diagnostics(&uri, text, &mut workspace);
let params = CodeLensParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_code_lens(&workspace, params);
assert!(result.is_some());
if let Some(lenses) = result {
assert!(!lenses.is_empty(), "Expected code lenses");
}
}
#[test]
fn test_code_lens_extends() {
let uri = test_uri();
let text = r#"model Base
Real x;
end Base;
model Derived
extends Base;
Real y;
end Derived;"#;
let mut workspace = WorkspaceState::new();
workspace.open_document(uri.clone(), text.to_string());
compute_diagnostics(&uri, text, &mut workspace);
let params = CodeLensParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_code_lens(&workspace, params);
assert!(result.is_some());
}
#[test]
fn test_call_hierarchy_prepare() {
let uri = test_uri();
let text = r#"function myFunc
input Real x;
output Real y;
algorithm
y := x * 2;
end myFunc;
model Test
Real z;
equation
z = myFunc(1.0);
end Test;"#;
let documents = create_documents(&uri, text);
let params = CallHierarchyPrepareParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 0,
character: 10,
}, },
work_done_progress_params: Default::default(),
};
let result = handle_prepare_call_hierarchy(&documents, params);
assert!(result.is_some());
}
#[test]
fn test_document_links_imports() {
let uri = test_uri();
let text = r#"model Test
annotation(Icon(graphics={Bitmap(fileName="resources/icon.svg")}));
end Test;"#;
let documents = create_documents(&uri, text);
let params = DocumentLinkParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_document_links(&documents, params);
assert!(result.is_some(), "Expected document links result");
if let Some(links) = result {
assert!(
!links.is_empty(),
"Expected document links for fileName annotation"
);
}
}
#[test]
fn test_document_links_within() {
let uri = test_uri();
let text = r#"within MyPackage;
model Test
Real x;
end Test;"#;
let documents = create_documents(&uri, text);
let params = DocumentLinkParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_document_links(&documents, params);
assert!(result.is_some(), "Expected document links result");
}
#[test]
fn test_document_links_urls() {
let uri = test_uri();
let text = r#"model Test "Test model"
annotation(Documentation(info="<html>
<p>See <a href=\"https://example.com\">docs</a></p>
</html>"));
end Test;"#;
let documents = create_documents(&uri, text);
let params = DocumentLinkParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_document_links(&documents, params);
assert!(result.is_some());
}
#[test]
fn test_workspace_state_document_management() {
let mut ws = WorkspaceState::new();
let uri: Uri = "file:///tmp/test.mo".parse().unwrap();
let text = "model Test end Test;";
ws.open_document(uri.clone(), text.to_string());
assert!(ws.get_document(&uri).is_some());
assert_eq!(ws.get_document(&uri).unwrap(), text);
let new_text = "model Test Real x; end Test;";
ws.update_document(uri.clone(), new_text.to_string());
assert_eq!(ws.get_document(&uri).unwrap(), new_text);
ws.close_document(&uri);
assert!(ws.get_document(&uri).is_none());
}
#[test]
fn test_workspace_state_symbol_indexing() {
let mut ws = WorkspaceState::new();
let uri: Uri = "file:///tmp/test.mo".parse().unwrap();
let text = r#"model TestModel
Real x;
end TestModel;
function TestFunction
input Real x;
output Real y;
algorithm
y := x * 2;
end TestFunction;"#;
ws.open_document(uri.clone(), text.to_string());
let symbols = ws.find_symbols("Test");
assert!(
!symbols.is_empty(),
"Expected symbols matching 'Test' query"
);
}
#[test]
fn test_empty_document() {
let uri = test_uri();
let text = "";
let documents = create_documents(&uri, text);
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_document_symbols(&documents, params);
assert!(
result.is_none()
|| matches!(result, Some(lsp_types::DocumentSymbolResponse::Nested(v)) if v.is_empty())
);
}
#[test]
fn test_nonexistent_document() {
let uri: Uri = "file:///nonexistent.mo".parse().unwrap();
let other_uri: Uri = "file:///other.mo".parse().unwrap();
let documents = create_documents(&other_uri, "model Test end Test;");
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_document_symbols(&documents, params);
assert!(result.is_none());
}
#[test]
fn test_position_out_of_bounds() {
let uri = test_uri();
let text = "model Test end Test;";
let documents = create_documents(&uri, text);
let params = HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 100,
character: 100,
},
},
work_done_progress_params: Default::default(),
};
let result = handle_hover(&documents, params);
assert!(result.is_none());
}
#[test]
fn test_completion_member_access_with_syntax_error() {
let uri = test_uri();
let valid_text = r#"model BouncingBall
parameter Real g = 9.81;
Real h(start = 10.0);
Real v(start = 0.0);
equation
der(h) = v;
der(v) = -g;
end BouncingBall;
model B
BouncingBall ball;
equation
ball.h = 1;
end B;"#;
let mut ws = WorkspaceState::new();
ws.open_document(uri.clone(), valid_text.to_string());
let invalid_text = r#"model BouncingBall
parameter Real g = 9.81;
Real h(start = 10.0);
Real v(start = 0.0);
equation
der(h) = v;
der(v) = -g;
end BouncingBall;
model B
BouncingBall ball;
equation
ball.
end B;"#;
ws.update_document(uri.clone(), invalid_text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 12,
character: 7,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: Some(lsp_types::CompletionContext {
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(".".to_string()),
}),
};
let result = rumoca::lsp::handle_completion_workspace(&mut ws, params);
assert!(
result.is_some(),
"Expected completion items for ball. even with syntax error"
);
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
!labels.contains(&"model"),
"Should NOT have keywords in dot completion: {:?}",
labels
);
assert!(
!labels.contains(&"parameter"),
"Should NOT have keywords in dot completion: {:?}",
labels
);
assert!(
labels.contains(&"g"),
"Expected 'g' parameter in completions, got: {:?}",
labels
);
assert!(
labels.contains(&"h"),
"Expected 'h' variable in completions, got: {:?}",
labels
);
assert!(
labels.contains(&"v"),
"Expected 'v' variable in completions, got: {:?}",
labels
);
}
}
#[test]
fn test_completion_class_instance_modifiers() {
let uri = test_uri();
let text = r#"model BouncingBall
parameter Real g = 9.81;
Real h(start = 10.0);
Real v(start = 0.0);
equation
der(h) = v;
der(v) = -g;
end BouncingBall;
model B
BouncingBall ball(g = 1);
end B;"#;
let _documents = create_documents(&uri, text);
let mut workspace = WorkspaceState::new();
workspace.update_document(uri.clone(), text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 10,
character: 20,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: Some(lsp_types::CompletionContext {
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some("(".to_string()),
}),
};
let result = handle_completion_workspace(&mut workspace, params);
assert!(
result.is_some(),
"Expected completion items for class instance modifiers"
);
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"g"),
"Expected 'g' parameter in instance modifier completions, got: {:?}",
labels
);
assert!(
labels.contains(&"h"),
"Expected 'h' variable in instance modifier completions, got: {:?}",
labels
);
assert!(
labels.contains(&"v"),
"Expected 'v' variable in instance modifier completions, got: {:?}",
labels
);
assert!(
!labels.contains(&"start"),
"Should NOT have primitive modifiers like 'start' for class instance, got: {:?}",
labels
);
assert!(
labels.contains(&"each"),
"Expected 'each' modifier for class instance, got: {:?}",
labels
);
}
}
#[test]
fn test_malformed_modelica() {
let uri = test_uri();
let text = "this is not valid modelica {{{{";
let documents = create_documents(&uri, text);
let params = DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
};
let result = handle_document_symbols(&documents, params);
let _ = result;
let mut workspace = WorkspaceState::new();
let diags = compute_diagnostics(&uri, text, &mut workspace);
assert!(!diags.is_empty(), "Expected diagnostics for invalid code");
}
#[test]
fn test_completion_imported_type_members() {
let lib_uri: Uri = "file:///test/MyLib.mo".parse().unwrap();
let lib_text = r#"package MyLib
model Controller
parameter Real gain = 1.0 "Controller gain";
Real u "Input signal";
Real y "Output signal";
equation
y = gain * u;
end Controller;
end MyLib;"#;
let main_uri: Uri = "file:///test/Main.mo".parse().unwrap();
let main_text = r#"model Main
import MyLib.Controller;
Controller ctrl(gain = 2.0);
equation
ctrl.u = time;
end Main;"#;
let mut ws = WorkspaceState::new();
ws.open_document(lib_uri.clone(), lib_text.to_string());
ws.open_document(main_uri.clone(), main_text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: main_uri.clone(),
},
position: Position {
line: 4,
character: 7,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: Some(lsp_types::CompletionContext {
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(".".to_string()),
}),
};
let result = rumoca::lsp::handle_completion_workspace(&mut ws, params);
assert!(
result.is_some(),
"Expected completion items for ctrl. (imported type)"
);
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"gain"),
"Expected 'gain' parameter from imported Controller, got: {:?}",
labels
);
assert!(
labels.contains(&"u"),
"Expected 'u' variable from imported Controller, got: {:?}",
labels
);
assert!(
labels.contains(&"y"),
"Expected 'y' variable from imported Controller, got: {:?}",
labels
);
}
}
#[test]
fn test_completion_inherited_members() {
let base_uri: Uri = "file:///test/Interfaces.mo".parse().unwrap();
let base_text = r#"within MyLib;
package Interfaces
partial block SISO "Single Input Single Output block"
Real u "Input signal";
Real y "Output signal";
end SISO;
end Interfaces;"#;
let derived_uri: Uri = "file:///test/Controllers.mo".parse().unwrap();
let derived_text = r#"within MyLib;
package Controllers
block PID "PID Controller"
extends Interfaces.SISO;
parameter Real K = 1.0 "Gain";
parameter Real Ti = 0.5 "Integral time";
parameter Real Td = 0.1 "Derivative time";
end PID;
end Controllers;"#;
let main_uri: Uri = "file:///test/Main.mo".parse().unwrap();
let main_text = r#"model Main
import MyLib.Controllers.PID;
PID pid1(K = 2.0);
equation
pid1.u = time;
end Main;"#;
let mut ws = WorkspaceState::new();
ws.open_document(base_uri.clone(), base_text.to_string());
ws.open_document(derived_uri.clone(), derived_text.to_string());
ws.open_document(main_uri.clone(), main_text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: main_uri.clone(),
},
position: Position {
line: 4,
character: 7,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: Some(lsp_types::CompletionContext {
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(".".to_string()),
}),
};
let result = rumoca::lsp::handle_completion_workspace(&mut ws, params);
assert!(
result.is_some(),
"Expected completion items for pid1. (with inheritance)"
);
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"K"),
"Expected 'K' parameter from PID, got: {:?}",
labels
);
assert!(
labels.contains(&"Ti"),
"Expected 'Ti' parameter from PID, got: {:?}",
labels
);
assert!(
labels.contains(&"Td"),
"Expected 'Td' parameter from PID, got: {:?}",
labels
);
assert!(
labels.contains(&"u"),
"Expected inherited 'u' from SISO base class, got: {:?}",
labels
);
assert!(
labels.contains(&"y"),
"Expected inherited 'y' from SISO base class, got: {:?}",
labels
);
}
}
#[test]
fn test_completion_inherited_members_relative_path() {
let interfaces_uri: Uri = "file:///test/Blocks/Interfaces.mo".parse().unwrap();
let interfaces_text = r#"within Modelica.Blocks;
package Interfaces
partial block SISO "Single Input Single Output block"
Real u "Input signal";
Real y "Output signal";
end SISO;
end Interfaces;"#;
let continuous_uri: Uri = "file:///test/Blocks/Continuous.mo".parse().unwrap();
let continuous_text = r#"within Modelica.Blocks;
package Continuous
block PID "PID Controller"
extends Interfaces.SISO;
parameter Real k = 1.0 "Gain";
end PID;
end Continuous;"#;
let main_uri: Uri = "file:///test/Main.mo".parse().unwrap();
let main_text = r#"model Main
import Modelica.Blocks.Continuous.PID;
PID controller(k = 2.0);
equation
controller.u = time;
end Main;"#;
let mut ws = WorkspaceState::new();
ws.open_document(interfaces_uri.clone(), interfaces_text.to_string());
ws.open_document(continuous_uri.clone(), continuous_text.to_string());
ws.open_document(main_uri.clone(), main_text.to_string());
let params = CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: main_uri.clone(),
},
position: Position {
line: 4,
character: 13,
}, },
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
context: Some(lsp_types::CompletionContext {
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some(".".to_string()),
}),
};
let result = rumoca::lsp::handle_completion_workspace(&mut ws, params);
assert!(
result.is_some(),
"Expected completion items for controller. (with relative inheritance)"
);
if let Some(lsp_types::CompletionResponse::Array(items)) = result {
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"k"),
"Expected 'k' parameter from PID, got: {:?}",
labels
);
assert!(
labels.contains(&"u"),
"Expected inherited 'u' from SISO (via relative extends path), got: {:?}",
labels
);
assert!(
labels.contains(&"y"),
"Expected inherited 'y' from SISO (via relative extends path), got: {:?}",
labels
);
}
}