#![allow(clippy::disallowed_methods)]
use std::collections::HashMap;
use std::path::PathBuf;
use mdt_core::TransformerType;
use mdt_core::apply_transformers;
use mdt_core::parse;
use mdt_core::parse_with_diagnostics;
use mdt_core::project::ConsumerEntry;
use mdt_core::project::ProviderEntry;
use mdt_core::project::extract_content_between_tags;
use serde_json::Value as JsonValue;
use tempfile::tempdir;
#[allow(unused_imports)]
use tower_lsp_server::LanguageServer;
#[allow(unused_imports)]
use tower_lsp_server::LspService;
#[allow(unused_imports)]
use tower_lsp_server::ls_types::*;
use super::*;
fn make_test_state(provider_content: &str, consumer_content: &str) -> (WorkspaceState, Uri) {
let provider_template =
format!("<!-- {{@greeting}} -->\n\n{provider_content}\n\n<!-- {{/greeting}} -->\n");
let consumer_doc = format!(
"# Readme\n\n<!-- {{=greeting}} -->\n\n{consumer_content}\n\n<!-- {{/greeting}} -->\n"
);
let provider_blocks = parse(&provider_template).unwrap_or_default();
let (consumer_blocks, consumer_parse_diagnostics) =
parse_with_diagnostics(&consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block in template"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(&provider_template, &provider_blocks[0]),
};
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry.clone());
let mut consumers = Vec::new();
for block in &consumer_blocks {
if block.r#type == BlockType::Consumer {
consumers.push(ConsumerEntry {
block: block.clone(),
file: PathBuf::from("/tmp/test/readme.md"),
content: extract_content_between_tags(&consumer_doc, block),
});
}
}
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc,
blocks: consumer_blocks,
parse_diagnostics: consumer_parse_diagnostics,
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers,
data: HashMap::new(),
};
(state, consumer_uri)
}
fn test_uri(path: &std::path::Path) -> Uri {
Uri::from_file_path(path).unwrap_or_else(|| panic!("expected file path URI"))
}
fn make_inline_test_state(
template_argument: Option<&str>,
consumer_content: &str,
data: HashMap<String, JsonValue>,
) -> (WorkspaceState, Uri) {
let opening_tag = template_argument.map_or_else(
|| "<!-- {~version} -->".to_string(),
|arg| format!("<!-- {{~version:\"{arg}\"}} -->"),
);
let inline_doc = format!("{opening_tag}{consumer_content}<!-- {{/version}} -->\n");
let (blocks, parse_diagnostics) = parse_with_diagnostics(&inline_doc).unwrap_or_default();
let consumer_uri = test_uri(std::path::Path::new("/tmp/test/readme.md"));
let mut consumers = Vec::new();
for block in &blocks {
if block.r#type == BlockType::Inline {
consumers.push(ConsumerEntry {
block: block.clone(),
file: PathBuf::from("/tmp/test/readme.md"),
content: extract_content_between_tags(&inline_doc, block),
});
}
}
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: inline_doc,
blocks,
parse_diagnostics,
},
);
(
WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers,
data,
},
consumer_uri,
)
}
#[test]
fn diagnostics_stale_consumer() {
let (state, uri) = make_test_state("Hello world!", "Old content");
let diagnostics = compute_diagnostics(&state, &uri);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
assert!(
diagnostics[0].message.contains("out of date"),
"expected 'out of date' in message: {}",
diagnostics[0].message
);
assert!(diagnostics[0].message.contains("greeting"));
}
#[test]
fn diagnostics_up_to_date_consumer() {
let (state, uri) = make_test_state("Hello world!", "Hello world!");
let diagnostics = compute_diagnostics(&state, &uri);
let doc = state.documents.get(&uri).unwrap();
let block = doc
.blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let content = extract_content_between_tags(&doc.content, block);
let provider = state.providers.get("greeting").unwrap();
let expected = apply_transformers(&provider.content, &block.transformers);
if content == expected {
assert!(diagnostics.is_empty(), "expected no diagnostics");
}
}
#[test]
fn diagnostics_missing_provider() {
let consumer_doc = "<!-- {=orphan} -->\n\nstuff\n\n<!-- {/orphan} -->\n";
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &consumer_uri);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].message.contains("No provider found"));
assert!(diagnostics[0].message.contains("orphan"));
}
#[test]
fn diagnostics_provider_in_non_template_file() {
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
assert_eq!(diagnostics.len(), 1);
assert!(diagnostics[0].message.contains("only recognized in *.t.md"));
}
#[test]
fn diagnostics_duplicate_provider_across_template_files() {
let provider_a = "<!-- {@shared} -->\n\nA\n\n<!-- {/shared} -->\n";
let provider_b = "<!-- {@shared} -->\n\nB\n\n<!-- {/shared} -->\n";
let blocks_a = parse(provider_a).unwrap_or_default();
let blocks_b = parse(provider_b).unwrap_or_default();
let uri_a = "file:///tmp/test/a.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let uri_b = "file:///tmp/test/b.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri_a.clone(),
DocumentState {
content: provider_a.to_string(),
blocks: blocks_a.clone(),
parse_diagnostics: Vec::new(),
},
);
documents.insert(
uri_b.clone(),
DocumentState {
content: provider_b.to_string(),
blocks: blocks_b,
parse_diagnostics: Vec::new(),
},
);
let provider_block = blocks_a
.iter()
.find(|block| block.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected provider block"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/a.t.md"),
content: extract_content_between_tags(provider_a, &blocks_a[0]),
};
let mut providers = HashMap::new();
providers.insert("shared".to_string(), provider_entry);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics_a = compute_diagnostics(&state, &uri_a);
assert!(diagnostics_a.iter().any(|diagnostic| {
diagnostic.severity == Some(DiagnosticSeverity::ERROR)
&& diagnostic
.message
.contains("Duplicate provider block `shared`")
&& diagnostic.message.contains("b.t.md")
}));
let diagnostics_b = compute_diagnostics(&state, &uri_b);
assert!(diagnostics_b.iter().any(|diagnostic| {
diagnostic.severity == Some(DiagnosticSeverity::ERROR)
&& diagnostic
.message
.contains("Duplicate provider block `shared`")
&& diagnostic.message.contains("a.t.md")
}));
}
#[test]
fn diagnostics_duplicate_provider_in_same_file() {
let content = "<!-- {@shared} -->\n\nA\n\n<!-- {/shared} -->\n\n<!-- {@shared} \
-->\n\nB\n\n<!-- {/shared} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
let duplicate_diagnostics = diagnostics
.iter()
.filter(|diagnostic| {
diagnostic
.message
.contains("Duplicate provider block `shared`")
})
.collect::<Vec<_>>();
assert_eq!(duplicate_diagnostics.len(), 2);
assert!(duplicate_diagnostics.iter().all(|diagnostic| {
diagnostic.severity == Some(DiagnosticSeverity::ERROR)
&& diagnostic
.message
.contains("multiple definitions in this file")
}));
}
#[test]
fn hover_on_consumer_shows_provider_content() {
let (state, uri) = make_test_state("Hello world!", "Old content");
let doc = state.documents.get(&uri).unwrap();
let block = doc
.blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let position = to_lsp_position(&block.opening.start);
let hover = compute_hover(&state, &uri, position);
assert!(hover.is_some());
let hover = hover.unwrap();
if let HoverContents::Markup(markup) = &hover.contents {
assert!(markup.value.contains("Consumer block"));
assert!(markup.value.contains("greeting"));
assert!(markup.value.contains("Hello world!"));
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn hover_on_provider_shows_consumer_count() {
let provider_template = "<!-- {@greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block.clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let consumers = vec![ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/readme.md"),
content: "\n\nold\n\n".to_string(),
}];
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers,
data: HashMap::new(),
};
let position = to_lsp_position(&provider_block.opening.start);
let hover = compute_hover(&state, &provider_uri, position);
assert!(hover.is_some());
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(markup.value.contains("Provider block"));
assert!(markup.value.contains("1 consumer(s)"));
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn hover_outside_block_returns_none() {
let (state, uri) = make_test_state("Hello!", "Hello!");
let position = Position {
line: 0,
character: 0,
};
let hover = compute_hover(&state, &uri, position);
assert!(hover.is_none());
}
#[test]
fn completion_inside_consumer_tag() {
let consumer_doc = "<!-- {=gre";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let provider_entry = ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello!\n\n".to_string(),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 0,
character: 9,
};
let completions = compute_completions(&state, &uri, position);
assert!(!completions.is_empty());
assert!(
completions.iter().any(|c| c.label == "greeting"),
"expected 'greeting' completion item"
);
}
#[test]
fn completion_after_pipe_suggests_transformers() {
let consumer_doc = "<!-- {=greeting|";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 0,
character: 16,
};
let completions = compute_completions(&state, &uri, position);
assert!(!completions.is_empty());
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(names.contains(&"trim"));
assert!(names.contains(&"indent"));
assert!(names.contains(&"codeBlock"));
assert!(names.contains(&"replace"));
}
#[test]
fn completion_outside_tag_returns_empty() {
let consumer_doc = "# Normal markdown";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 0,
character: 5,
};
let completions = compute_completions(&state, &uri, position);
assert!(completions.is_empty());
}
#[test]
fn goto_definition_consumer_to_provider() {
let (state, uri) = make_test_state("Hello!", "Old");
let doc = state.documents.get(&uri).unwrap();
let block = doc
.blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let position = to_lsp_position(&block.opening.start);
let result = compute_goto_definition(&state, &uri, position);
assert!(result.is_some());
match result.unwrap() {
GotoDefinitionResponse::Scalar(loc) => {
assert!(
loc.uri.path().as_str().contains("template.t.md"),
"expected target to be the template file"
);
}
GotoDefinitionResponse::Array(locs) => {
assert!(locs[0].uri.path().as_str().contains("template.t.md"));
}
GotoDefinitionResponse::Link(_) => panic!("unexpected Link goto definition response"),
}
}
#[test]
fn goto_definition_without_matching_provider_returns_none() {
let consumer_doc = "<!-- {=missing} -->\n\nstuff\n\n<!-- {/missing} -->\n";
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let position = to_lsp_position(&block.opening.start);
let result = compute_goto_definition(&state, &uri, position);
assert!(result.is_none());
}
#[test]
fn document_symbols_lists_blocks() {
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n\n<!-- {=other} \
-->\n\nstuff\n\n<!-- {/other} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let symbols = compute_document_symbols(&state, &uri);
assert_eq!(symbols.len(), 2);
let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"@greeting"));
assert!(names.contains(&"=other"));
}
#[test]
fn document_symbols_empty_for_no_blocks() {
let content = "# Just a heading\n\nNo blocks here.\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let symbols = compute_document_symbols(&state, &uri);
assert!(symbols.is_empty());
}
#[test]
fn code_action_for_stale_consumer() {
let (state, uri) = make_test_state("Hello world!", "Old content");
let doc = state.documents.get(&uri).unwrap();
let block = doc
.blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let range = Range {
start: to_lsp_position(&block.opening.start),
end: to_lsp_position(&block.closing.end),
};
let actions = compute_code_actions(&state, &uri, range);
assert!(!actions.is_empty(), "expected at least one code action");
let CodeActionOrCommand::CodeAction(action) = &actions[0] else {
panic!("expected CodeAction")
};
assert!(action.title.contains("Update block"));
assert!(action.title.contains("greeting"));
assert!(action.edit.is_some());
}
#[test]
fn code_action_not_offered_when_up_to_date() {
let provider_template = "<!-- {@greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n";
let consumer_doc = "<!-- {=greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected provider block"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let range = Range {
start: to_lsp_position(&block.opening.start),
end: to_lsp_position(&block.closing.end),
};
let actions = compute_code_actions(&state, &consumer_uri, range);
assert!(
actions.is_empty(),
"expected no code actions for up-to-date block"
);
}
#[test]
fn position_in_range_basic() {
let range = Range {
start: Position {
line: 2,
character: 0,
},
end: Position {
line: 2,
character: 20,
},
};
assert!(position_in_range(
Position {
line: 2,
character: 10
},
range
));
assert!(!position_in_range(
Position {
line: 1,
character: 10
},
range
));
assert!(!position_in_range(
Position {
line: 3,
character: 0
},
range
));
}
#[test]
fn ranges_overlap_basic() {
let a = Range {
start: Position {
line: 2,
character: 0,
},
end: Position {
line: 5,
character: 10,
},
};
let b = Range {
start: Position {
line: 4,
character: 0,
},
end: Position {
line: 8,
character: 5,
},
};
assert!(ranges_overlap(a, b));
let c = Range {
start: Position {
line: 10,
character: 0,
},
end: Position {
line: 12,
character: 5,
},
};
assert!(!ranges_overlap(a, c));
}
#[test]
fn to_lsp_position_converts_correctly() {
let point = mdt_core::Point::new(1, 1, 0);
let lsp_pos = to_lsp_position(&point);
assert_eq!(lsp_pos.line, 0);
assert_eq!(lsp_pos.character, 0);
let point2 = mdt_core::Point::new(5, 10, 42);
let lsp_pos2 = to_lsp_position(&point2);
assert_eq!(lsp_pos2.line, 4);
assert_eq!(lsp_pos2.character, 9);
}
#[test]
fn parse_document_content_markdown() {
let uri = "file:///test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let (blocks, diagnostics) = parse_document_content(&uri, content);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].name, "greeting");
assert!(diagnostics.is_empty());
}
#[test]
fn parse_document_content_source_file() {
let uri = "file:///test/main.rs"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "// <!-- {=block} -->\n// content\n// <!-- {/block} -->\n";
let (blocks, diagnostics) = parse_document_content(&uri, content);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].name, "block");
assert!(diagnostics.is_empty());
}
#[test]
fn transformer_type_display_all() {
assert_eq!(TransformerType::Trim.to_string(), "trim");
assert_eq!(TransformerType::TrimStart.to_string(), "trimStart");
assert_eq!(TransformerType::TrimEnd.to_string(), "trimEnd");
assert_eq!(TransformerType::Indent.to_string(), "indent");
assert_eq!(TransformerType::Prefix.to_string(), "prefix");
assert_eq!(TransformerType::Wrap.to_string(), "wrap");
assert_eq!(TransformerType::CodeBlock.to_string(), "codeBlock");
assert_eq!(TransformerType::Code.to_string(), "code");
assert_eq!(TransformerType::Replace.to_string(), "replace");
assert_eq!(TransformerType::If.to_string(), "if");
}
#[test]
fn diagnostics_unclosed_block() {
let content = "<!-- {=greeting} -->\n\nHello\n";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let (blocks, parse_diagnostics) = parse_with_diagnostics(content).unwrap_or_default();
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics,
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("Missing closing tag") && d.message.contains("greeting")),
"expected unclosed block diagnostic, got: {diagnostics:?}"
);
assert!(
diagnostics
.iter()
.any(|d| d.severity == Some(DiagnosticSeverity::ERROR)),
"unclosed block should be an error"
);
}
#[test]
fn diagnostics_unknown_transformer() {
let content = "<!-- {=greeting|foobar} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let (blocks, parse_diagnostics) = parse_with_diagnostics(content).unwrap_or_default();
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics,
},
);
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello!\n\n".to_string(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("Unknown transformer") && d.message.contains("foobar")),
"expected unknown transformer diagnostic, got: {diagnostics:?}"
);
}
#[test]
fn diagnostics_unused_provider() {
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let blocks = parse(content).unwrap_or_default();
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(5, 1, 28, 5, 22, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello\n\n".to_string(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("has no consumers") && d.message.contains("greeting")),
"expected unused provider diagnostic, got: {diagnostics:?}"
);
}
#[test]
fn diagnostics_missing_provider_with_suggestion() {
let content = "<!-- {=greetng} -->\n\nstuff\n\n<!-- {/greetng} -->\n";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let (blocks, parse_diagnostics) = parse_with_diagnostics(content).unwrap_or_default();
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics,
},
);
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello!\n\n".to_string(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("Did you mean") && d.message.contains("`greeting`")),
"expected suggestion for similar name, got: {diagnostics:?}"
);
}
#[test]
fn diagnostics_missing_provider_no_suggestion_when_too_different() {
let content = "<!-- {=xyz} -->\n\nstuff\n\n<!-- {/xyz} -->\n";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let (blocks, parse_diagnostics) = parse_with_diagnostics(content).unwrap_or_default();
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics,
},
);
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello!\n\n".to_string(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
let missing_diag = diagnostics
.iter()
.find(|d| d.message.contains("No provider found"))
.unwrap_or_else(|| panic!("expected missing provider diagnostic, got: {diagnostics:?}"));
assert!(
!missing_diag.message.contains("Did you mean"),
"should not suggest when names are too different"
);
}
#[test]
fn parse_document_content_with_unclosed_block() {
let uri = "file:///test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "<!-- {=greeting} -->\n\nHello\n";
let (blocks, diagnostics) = parse_document_content(&uri, content);
assert!(
blocks.is_empty(),
"unclosed block should not produce a block"
);
assert_eq!(diagnostics.len(), 1);
assert!(matches!(
diagnostics[0],
ParseDiagnostic::UnclosedBlock { .. }
));
}
#[test]
fn parse_document_content_with_unknown_transformer() {
let uri = "file:///test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "<!-- {=greeting|unknownFilter} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let (blocks, diagnostics) = parse_document_content(&uri, content);
assert_eq!(blocks.len(), 1);
assert_eq!(diagnostics.len(), 1);
assert!(matches!(
diagnostics[0],
ParseDiagnostic::UnknownTransformer { .. }
));
}
#[test]
fn goto_definition_provider_to_single_consumer() {
let provider_template = "<!-- {@greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block.clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let consumers = vec![ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/readme.md"),
content: "\n\nold\n\n".to_string(),
}];
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers,
data: HashMap::new(),
};
let position = to_lsp_position(&provider_block.opening.start);
let result = compute_goto_definition(&state, &provider_uri, position);
assert!(result.is_some(), "expected goto definition result");
match result.unwrap() {
GotoDefinitionResponse::Scalar(loc) => {
assert!(
loc.uri.path().as_str().contains("readme.md"),
"expected target to be the consumer file, got: {}",
loc.uri.path().as_str()
);
}
GotoDefinitionResponse::Array(locs) => {
assert_eq!(locs.len(), 1);
assert!(locs[0].uri.path().as_str().contains("readme.md"));
}
GotoDefinitionResponse::Link(_) => panic!("unexpected Link goto definition response"),
}
}
#[test]
fn goto_definition_provider_to_multiple_consumers() {
let provider_template = "<!-- {@greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block.clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let consumers = vec![
ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/readme.md"),
content: "\n\nold\n\n".to_string(),
},
ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/docs.md"),
content: "\n\nold\n\n".to_string(),
},
ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/other.md"),
content: "\n\nold\n\n".to_string(),
},
];
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers,
data: HashMap::new(),
};
let position = to_lsp_position(&provider_block.opening.start);
let result = compute_goto_definition(&state, &provider_uri, position);
assert!(result.is_some(), "expected goto definition result");
match result.unwrap() {
GotoDefinitionResponse::Array(locs) => {
assert_eq!(locs.len(), 3, "expected 3 consumer locations");
let paths: Vec<String> = locs
.iter()
.map(|l| l.uri.path().as_str().to_string())
.collect();
assert!(
paths.iter().any(|p| p.contains("readme.md")),
"expected readme.md in locations"
);
assert!(
paths.iter().any(|p| p.contains("docs.md")),
"expected docs.md in locations"
);
assert!(
paths.iter().any(|p| p.contains("other.md")),
"expected other.md in locations"
);
}
other => panic!("expected Array response for multiple consumers, got: {other:?}"),
}
}
#[test]
fn goto_definition_provider_with_no_consumers_returns_none() {
let provider_template = "<!-- {@greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block.clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let position = to_lsp_position(&provider_block.opening.start);
let result = compute_goto_definition(&state, &provider_uri, position);
assert!(
result.is_none(),
"expected None for provider with no consumers"
);
}
#[test]
fn code_action_no_overlap_with_block_returns_empty() {
let (state, uri) = make_test_state("Hello world!", "Old content");
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
};
let actions = compute_code_actions(&state, &uri, range);
assert!(
actions.is_empty(),
"expected no code actions when cursor doesn't overlap any block"
);
}
#[test]
fn code_action_consumer_without_matching_provider() {
let consumer_doc = "<!-- {=orphan} -->\n\nstuff\n\n<!-- {/orphan} -->\n";
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("expected consumer block"));
let range = Range {
start: to_lsp_position(&block.opening.start),
end: to_lsp_position(&block.closing.end),
};
let actions = compute_code_actions(&state, &uri, range);
assert!(
actions.is_empty(),
"expected no code actions when provider is missing"
);
}
#[test]
fn completion_cursor_past_line_length_returns_empty() {
let consumer_doc = "short";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 0,
character: 100,
};
let completions = compute_completions(&state, &uri, position);
assert!(
completions.is_empty(),
"expected no completions when cursor is past line length"
);
}
#[test]
fn completion_document_with_no_blocks() {
let consumer_doc = "# Empty document\n\nNo blocks here.\n";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 1,
character: 0,
};
let completions = compute_completions(&state, &uri, position);
assert!(completions.is_empty(), "expected no completions");
}
#[test]
fn completion_cursor_on_nonexistent_line_returns_empty() {
let consumer_doc = "one line";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 5,
character: 0,
};
let completions = compute_completions(&state, &uri, position);
assert!(
completions.is_empty(),
"expected no completions for nonexistent line"
);
}
#[test]
fn completion_for_unknown_document_returns_empty() {
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let uri = "file:///tmp/test/unknown.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let position = Position {
line: 0,
character: 0,
};
let completions = compute_completions(&state, &uri, position);
assert!(
completions.is_empty(),
"expected no completions for unknown document"
);
}
#[test]
fn diagnostics_invalid_transformer_args() {
let content = "<!-- {=greeting|trim} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let blocks = parse(content).unwrap_or_default();
let parse_diagnostics = vec![ParseDiagnostic::InvalidTransformerArgs {
name: "trim".to_string(),
expected: "0".to_string(),
got: 1,
line: 1,
column: 1,
}];
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics,
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
assert!(
diagnostics.iter().any(|d| {
d.message
.contains("Transformer `trim` expects 0 argument(s), got 1")
}),
"expected InvalidTransformerArgs diagnostic, got: {diagnostics:?}"
);
assert!(
diagnostics
.iter()
.any(|d| d.severity == Some(DiagnosticSeverity::ERROR)),
"InvalidTransformerArgs should be an error"
);
}
#[test]
fn update_document_in_project_template_updates_provider() {
let provider_template = "<!-- {@greeting} -->\n\nHello updated!\n\n<!-- {/greeting} -->\n";
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_blocks = parse(provider_template).unwrap_or_default();
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let mut state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
assert!(state.providers.is_empty());
state.update_document_in_project(&provider_uri);
assert!(
state.providers.contains_key("greeting"),
"expected 'greeting' provider to be registered"
);
let provider = state.providers.get("greeting").unwrap();
assert!(
provider.content.contains("Hello updated!"),
"expected provider content to contain updated text"
);
assert_eq!(provider.file, PathBuf::from("/tmp/test/template.t.md"));
}
#[test]
fn update_document_in_project_consumer_file_updates_consumers() {
let consumer_doc = "<!-- {=greeting} -->\n\nOld content\n\n<!-- {/greeting} -->\n";
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks,
parse_diagnostics: Vec::new(),
},
);
let mut state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
assert!(state.consumers.is_empty());
state.update_document_in_project(&consumer_uri);
assert_eq!(state.consumers.len(), 1);
assert_eq!(state.consumers[0].block.name, "greeting");
assert_eq!(
state.consumers[0].file,
PathBuf::from("/tmp/test/readme.md")
);
}
#[test]
fn update_document_in_project_replaces_existing_consumers() {
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let old_consumer = ConsumerEntry {
block: Block {
name: "old_block".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/readme.md"),
content: "\n\nold\n\n".to_string(),
};
let other_consumer = ConsumerEntry {
block: Block {
name: "other_block".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/other.md"),
content: "\n\nother\n\n".to_string(),
};
let consumer_doc = "<!-- {=greeting} -->\n\nNew content\n\n<!-- {/greeting} -->\n";
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks,
parse_diagnostics: Vec::new(),
},
);
let mut state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: vec![old_consumer, other_consumer],
data: HashMap::new(),
};
assert_eq!(state.consumers.len(), 2);
state.update_document_in_project(&consumer_uri);
assert_eq!(state.consumers.len(), 2);
assert!(
state
.consumers
.iter()
.any(|c| c.block.name == "other_block"),
"consumer from other file should be preserved"
);
assert!(
state.consumers.iter().any(|c| c.block.name == "greeting"),
"new consumer should be added"
);
assert!(
!state.consumers.iter().any(|c| c.block.name == "old_block"),
"old consumer from same file should be removed"
);
}
#[test]
fn update_document_in_project_unknown_document_is_noop() {
let uri = "file:///tmp/test/unknown.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
state.update_document_in_project(&uri);
assert!(state.providers.is_empty());
assert!(state.consumers.is_empty());
}
#[test]
fn workspace_parse_document_stores_state() {
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "<!-- {=greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let mut state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let blocks = state.parse_document(&uri, content.to_string());
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].name, "greeting");
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("document should be stored"));
assert_eq!(doc.content, content);
assert_eq!(doc.blocks.len(), 1);
assert!(doc.parse_diagnostics.is_empty());
}
#[test]
fn workspace_parse_document_with_diagnostics() {
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "<!-- {=greeting} -->\n\nHello\n";
let mut state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let blocks = state.parse_document(&uri, content.to_string());
assert!(
blocks.is_empty(),
"unclosed block should not produce blocks"
);
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("document should be stored"));
assert_eq!(doc.parse_diagnostics.len(), 1);
assert!(matches!(
doc.parse_diagnostics[0],
ParseDiagnostic::UnclosedBlock { .. }
));
}
#[test]
fn workspace_parse_document_replaces_previous() {
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let mut state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let content_v1 = "# Version 1\n";
state.parse_document(&uri, content_v1.to_string());
assert_eq!(state.documents.get(&uri).unwrap().content, content_v1);
let content_v2 = "<!-- {=block} -->\n\nv2\n\n<!-- {/block} -->\n";
let blocks = state.parse_document(&uri, content_v2.to_string());
assert_eq!(blocks.len(), 1);
assert_eq!(state.documents.get(&uri).unwrap().content, content_v2);
}
#[test]
fn hover_consumer_without_provider_shows_no_matching() {
let consumer_doc = "<!-- {=orphan} -->\n\nstuff\n\n<!-- {/orphan} -->\n";
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("expected consumer block"));
let position = to_lsp_position(&block.opening.start);
let hover = compute_hover(&state, &uri, position);
assert!(hover.is_some(), "expected hover result");
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(
markup.value.contains("No matching provider found"),
"expected 'No matching provider found' in hover, got: {}",
markup.value
);
assert!(markup.value.contains("Consumer block"));
assert!(markup.value.contains("orphan"));
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn hover_consumer_with_transformers_shows_transformer_list() {
let consumer_doc = "<!-- {=greeting|trim|indent:\" \"} -->\n\nstuff\n\n<!-- {/greeting} -->\n";
let (consumer_blocks, consumer_parse_diags) =
parse_with_diagnostics(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_entry = ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\n Hello world! \n\n".to_string(),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: consumer_parse_diags,
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("expected consumer block"));
let position = to_lsp_position(&block.opening.start);
let hover = compute_hover(&state, &uri, position);
assert!(hover.is_some(), "expected hover result");
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(
markup.value.contains("Transformers"),
"expected 'Transformers' section in hover, got: {}",
markup.value
);
assert!(
markup.value.contains("trim"),
"expected 'trim' in transformers list, got: {}",
markup.value
);
assert!(
markup.value.contains("indent"),
"expected 'indent' in transformers list, got: {}",
markup.value
);
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn transformer_type_display_suffix() {
assert_eq!(TransformerType::Suffix.to_string(), "suffix");
}
#[test]
fn transformer_type_display_line_suffix() {
assert_eq!(TransformerType::LineSuffix.to_string(), "lineSuffix");
}
#[test]
fn transformer_type_display_line_prefix() {
assert_eq!(TransformerType::LinePrefix.to_string(), "linePrefix");
}
#[test]
fn position_in_range_exact_start_boundary() {
let range = Range {
start: Position {
line: 5,
character: 10,
},
end: Position {
line: 5,
character: 20,
},
};
assert!(position_in_range(
Position {
line: 5,
character: 10,
},
range
));
}
#[test]
fn position_in_range_exact_end_boundary() {
let range = Range {
start: Position {
line: 5,
character: 10,
},
end: Position {
line: 5,
character: 20,
},
};
assert!(position_in_range(
Position {
line: 5,
character: 20,
},
range
));
}
#[test]
fn position_in_range_just_before_start_on_same_line() {
let range = Range {
start: Position {
line: 5,
character: 10,
},
end: Position {
line: 5,
character: 20,
},
};
assert!(!position_in_range(
Position {
line: 5,
character: 9,
},
range
));
}
#[test]
fn position_in_range_just_after_end_on_same_line() {
let range = Range {
start: Position {
line: 5,
character: 10,
},
end: Position {
line: 5,
character: 20,
},
};
assert!(!position_in_range(
Position {
line: 5,
character: 21,
},
range
));
}
#[test]
fn position_in_range_multi_line_middle() {
let range = Range {
start: Position {
line: 2,
character: 5,
},
end: Position {
line: 8,
character: 15,
},
};
assert!(position_in_range(
Position {
line: 5,
character: 0,
},
range
));
assert!(position_in_range(
Position {
line: 5,
character: 99,
},
range
));
}
#[test]
fn position_in_range_start_line_before_start_char() {
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 5,
character: 15,
},
};
assert!(!position_in_range(
Position {
line: 2,
character: 5,
},
range
));
}
#[test]
fn position_in_range_end_line_after_end_char() {
let range = Range {
start: Position {
line: 2,
character: 10,
},
end: Position {
line: 5,
character: 15,
},
};
assert!(!position_in_range(
Position {
line: 5,
character: 16,
},
range
));
}
#[test]
fn ranges_overlap_same_line_touching_at_boundary() {
let a = Range {
start: Position {
line: 5,
character: 0,
},
end: Position {
line: 5,
character: 10,
},
};
let b = Range {
start: Position {
line: 5,
character: 10,
},
end: Position {
line: 5,
character: 20,
},
};
assert!(
ranges_overlap(a, b),
"ranges that touch at boundary should overlap"
);
}
#[test]
fn ranges_overlap_same_line_gap_between() {
let a = Range {
start: Position {
line: 5,
character: 0,
},
end: Position {
line: 5,
character: 9,
},
};
let b = Range {
start: Position {
line: 5,
character: 10,
},
end: Position {
line: 5,
character: 20,
},
};
assert!(
!ranges_overlap(a, b),
"ranges with a gap on the same line should not overlap"
);
}
#[test]
fn ranges_overlap_identical_ranges() {
let a = Range {
start: Position {
line: 3,
character: 5,
},
end: Position {
line: 7,
character: 10,
},
};
assert!(ranges_overlap(a, a), "identical ranges should overlap");
}
#[test]
fn ranges_overlap_one_contains_other() {
let outer = Range {
start: Position {
line: 1,
character: 0,
},
end: Position {
line: 10,
character: 50,
},
};
let inner = Range {
start: Position {
line: 3,
character: 5,
},
end: Position {
line: 7,
character: 10,
},
};
assert!(ranges_overlap(outer, inner));
assert!(ranges_overlap(inner, outer));
}
#[test]
fn ranges_overlap_adjacent_lines_no_overlap() {
let a = Range {
start: Position {
line: 1,
character: 0,
},
end: Position {
line: 2,
character: 0,
},
};
let b = Range {
start: Position {
line: 2,
character: 1,
},
end: Position {
line: 3,
character: 0,
},
};
assert!(
!ranges_overlap(a, b),
"ranges on adjacent lines with character gap should not overlap"
);
}
#[test]
fn diagnostics_unknown_document_returns_empty() {
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let uri = "file:///tmp/test/unknown.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let diagnostics = compute_diagnostics(&state, &uri);
assert!(
diagnostics.is_empty(),
"expected no diagnostics for unknown document"
);
}
#[test]
fn hover_provider_lists_consumer_files() {
let provider_template = "<!-- {@greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block.clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let consumers = vec![
ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/readme.md"),
content: "\n\nold\n\n".to_string(),
},
ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/docs.md"),
content: "\n\nold\n\n".to_string(),
},
];
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers,
data: HashMap::new(),
};
let position = to_lsp_position(&provider_block.opening.start);
let hover = compute_hover(&state, &provider_uri, position);
assert!(hover.is_some());
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(
markup.value.contains("2 consumer(s)"),
"expected '2 consumer(s)' in hover, got: {}",
markup.value
);
assert!(
markup.value.contains("Consumers in:"),
"expected 'Consumers in:' section"
);
assert!(
markup.value.contains("readme.md"),
"expected readme.md in consumer listing"
);
assert!(
markup.value.contains("docs.md"),
"expected docs.md in consumer listing"
);
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn code_actions_unknown_document_returns_empty() {
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let uri = "file:///tmp/test/unknown.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 10,
character: 0,
},
};
let actions = compute_code_actions(&state, &uri, range);
assert!(
actions.is_empty(),
"expected no code actions for unknown document"
);
}
#[test]
fn document_symbols_unknown_document_returns_empty() {
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let uri = "file:///tmp/test/unknown.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let symbols = compute_document_symbols(&state, &uri);
assert!(
symbols.is_empty(),
"expected no symbols for unknown document"
);
}
#[test]
fn hover_unknown_document_returns_none() {
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let uri = "file:///tmp/test/unknown.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let hover = compute_hover(
&state,
&uri,
Position {
line: 0,
character: 0,
},
);
assert!(hover.is_none());
}
#[test]
fn goto_definition_unknown_document_returns_none() {
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let uri = "file:///tmp/test/unknown.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let result = compute_goto_definition(
&state,
&uri,
Position {
line: 0,
character: 0,
},
);
assert!(result.is_none());
}
#[test]
fn to_lsp_range_converts_correctly() {
let pos = mdt_core::Position::new(2, 5, 10, 4, 20, 50);
let range = to_lsp_range(&pos);
assert_eq!(range.start.line, 1);
assert_eq!(range.start.character, 4);
assert_eq!(range.end.line, 3);
assert_eq!(range.end.character, 19);
}
#[test]
fn find_block_at_position_returns_none_for_empty_blocks() {
let position = Position {
line: 0,
character: 0,
};
let result = find_block_at_position(&[], position);
assert!(result.is_none());
}
#[test]
fn find_block_at_position_finds_correct_block() {
let content = "<!-- {=greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let blocks = parse(content).unwrap_or_default();
let block = &blocks[0];
let position = to_lsp_position(&block.opening.start);
let found = find_block_at_position(&blocks, position);
assert!(found.is_some());
assert_eq!(found.unwrap().name, "greeting");
}
#[test]
fn find_block_at_position_returns_none_outside_all_blocks() {
let content =
"# Heading\n\n<!-- {=greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n\nTrailing text\n";
let blocks = parse(content).unwrap_or_default();
let position = Position {
line: 0,
character: 0,
};
let found = find_block_at_position(&blocks, position);
assert!(found.is_none());
}
#[test]
fn parse_document_content_mdx_is_markdown() {
let uri = "file:///test/page.mdx"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let (blocks, diagnostics) = parse_document_content(&uri, content);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].name, "greeting");
assert!(diagnostics.is_empty());
}
#[test]
fn parse_document_content_markdown_extension() {
let uri = "file:///test/readme.markdown"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "<!-- {=block} -->\n\ncontent\n\n<!-- {/block} -->\n";
let (blocks, diagnostics) = parse_document_content(&uri, content);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].name, "block");
assert!(diagnostics.is_empty());
}
#[test]
fn parse_document_content_typescript_file() {
let uri = "file:///test/main.ts"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "// <!-- {=block} -->\n// content\n// <!-- {/block} -->\n";
let (blocks, diagnostics) = parse_document_content(&uri, content);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].name, "block");
assert!(diagnostics.is_empty());
}
#[test]
fn rescan_project_without_root_is_noop() {
let mut state = WorkspaceState {
root: None,
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
state.rescan_project();
assert!(state.providers.is_empty());
assert!(state.consumers.is_empty());
}
#[test]
fn diagnostics_stale_consumer_with_template_data() {
let provider_template = "<!-- {@ver} -->\n\nv{{ pkg.version }}\n\n<!-- {/ver} -->\n";
let consumer_doc = "<!-- {=ver} -->\n\nv1.0.0\n\n<!-- {/ver} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let (consumer_blocks, consumer_parse_diagnostics) =
parse_with_diagnostics(consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert("ver".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks,
parse_diagnostics: consumer_parse_diagnostics,
},
);
let mut data = HashMap::new();
data.insert("pkg".to_string(), serde_json::json!({"version": "2.0.0"}));
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data,
};
let diagnostics = compute_diagnostics(&state, &consumer_uri);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("out of date")),
"expected stale diagnostic when rendered content differs, got: {diagnostics:?}"
);
}
#[test]
fn diagnostics_stale_consumer_includes_data_payload() {
let (state, uri) = make_test_state("Hello world!", "Old content");
let diagnostics = compute_diagnostics(&state, &uri);
assert_eq!(diagnostics.len(), 1);
let data = diagnostics[0]
.data
.as_ref()
.unwrap_or_else(|| panic!("expected diagnostic data"));
assert_eq!(data["kind"], "stale");
assert_eq!(data["block_name"], "greeting");
assert!(
data["expected_content"].is_string(),
"expected_content should be a string"
);
}
#[test]
fn diagnostics_multiple_consumers_in_single_document() {
let consumer_doc = "\
<!-- {=greeting} -->
Old greeting
<!-- {/greeting} -->
<!-- {=farewell} -->
Old farewell
<!-- {/farewell} -->
";
let (consumer_blocks, consumer_parse_diagnostics) =
parse_with_diagnostics(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks,
parse_diagnostics: consumer_parse_diagnostics,
},
);
let provider_template_1 = "<!-- {@greeting} -->\n\nNew greeting\n\n<!-- {/greeting} -->\n";
let provider_blocks_1 = parse(provider_template_1).unwrap_or_default();
let provider_template_2 = "<!-- {@farewell} -->\n\nNew farewell\n\n<!-- {/farewell} -->\n";
let provider_blocks_2 = parse(provider_template_2).unwrap_or_default();
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: provider_blocks_1[0].clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template_1, &provider_blocks_1[0]),
},
);
providers.insert(
"farewell".to_string(),
ProviderEntry {
block: provider_blocks_2[0].clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template_2, &provider_blocks_2[0]),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
let stale_names: Vec<&str> = diagnostics
.iter()
.filter(|d| d.message.contains("out of date"))
.filter_map(|d| {
if d.message.contains("greeting") {
Some("greeting")
} else if d.message.contains("farewell") {
Some("farewell")
} else {
None
}
})
.collect();
assert!(
stale_names.contains(&"greeting"),
"expected greeting to be stale"
);
assert!(
stale_names.contains(&"farewell"),
"expected farewell to be stale"
);
}
#[test]
fn completion_returns_all_provider_names() {
let doc = "<!-- {=";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let make_provider = |name: &str| {
ProviderEntry {
block: Block {
name: name.to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\ncontent\n\n".to_string(),
}
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), make_provider("greeting"));
providers.insert("farewell".to_string(), make_provider("farewell"));
providers.insert("install".to_string(), make_provider("install"));
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 0,
character: 7,
};
let completions = compute_completions(&state, &uri, position);
assert_eq!(completions.len(), 3, "expected 3 completion items");
let labels: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(labels.contains(&"greeting"));
assert!(labels.contains(&"farewell"));
assert!(labels.contains(&"install"));
}
#[test]
fn transformer_completions_have_function_kind() {
let completions = transformer_completions();
assert!(!completions.is_empty());
for item in &completions {
assert_eq!(
item.kind,
Some(CompletionItemKind::FUNCTION),
"transformer completions should have FUNCTION kind"
);
}
}
#[test]
fn transformer_completions_have_sort_text() {
let completions = transformer_completions();
for (i, item) in completions.iter().enumerate() {
assert_eq!(
item.sort_text,
Some(format!("{i:02}")),
"transformer at index {i} should have sort_text '{i:02}'"
);
}
}
#[test]
fn transformer_completions_include_all_known_transformers() {
let completions = transformer_completions();
let names: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
let expected = [
"trim",
"trimStart",
"trimEnd",
"indent",
"prefix",
"suffix",
"linePrefix",
"lineSuffix",
"wrap",
"codeBlock",
"code",
"replace",
"if",
];
for name in expected {
assert!(
names.contains(&name),
"expected transformer '{name}' in completions, got: {names:?}"
);
}
}
#[test]
fn block_name_completions_have_reference_kind() {
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello!\n\n".to_string(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::new(),
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let completions = block_name_completions(&state);
assert_eq!(completions.len(), 1);
assert_eq!(completions[0].kind, Some(CompletionItemKind::REFERENCE));
assert!(
completions[0]
.detail
.as_ref()
.unwrap_or_else(|| panic!("expected detail"))
.contains("template.t.md")
);
}
#[test]
fn document_symbols_full_range_spans_opening_to_closing() {
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let symbols = compute_document_symbols(&state, &uri);
assert_eq!(symbols.len(), 1);
let symbol = &symbols[0];
assert!(
symbol.range.start.line <= symbol.selection_range.start.line,
"full range should start at or before selection range"
);
assert!(
symbol.range.end.line >= symbol.selection_range.end.line,
"full range should end at or after selection range"
);
}
#[test]
fn code_action_edit_targets_content_between_tags() {
let (state, uri) = make_test_state("Hello world!", "Old content");
let doc = state.documents.get(&uri).unwrap_or_else(|| panic!("doc"));
let block = doc
.blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("consumer block"));
let range = Range {
start: to_lsp_position(&block.opening.start),
end: to_lsp_position(&block.closing.end),
};
let actions = compute_code_actions(&state, &uri, range);
assert!(!actions.is_empty());
let CodeActionOrCommand::CodeAction(action) = &actions[0] else {
panic!("expected CodeAction")
};
assert_eq!(action.kind, Some(CodeActionKind::QUICKFIX));
let edit = action
.edit
.as_ref()
.unwrap_or_else(|| panic!("expected workspace edit"));
let changes = edit
.changes
.as_ref()
.unwrap_or_else(|| panic!("expected changes map"));
assert!(
changes.contains_key(&uri),
"changes should target the consumer file"
);
let text_edits = changes
.get(&uri)
.unwrap_or_else(|| panic!("expected text edits for URI"));
assert_eq!(text_edits.len(), 1, "expected exactly one text edit");
let text_edit = &text_edits[0];
let opening_end = to_lsp_position(&block.opening.end);
assert_eq!(text_edit.range.start, opening_end);
}
#[test]
fn to_lsp_position_saturates_at_zero() {
let point = mdt_core::Point::new(0, 0, 0);
let lsp_pos = to_lsp_position(&point);
assert_eq!(lsp_pos.line, 0);
assert_eq!(lsp_pos.character, 0);
}
#[test]
fn parse_document_content_python_file() {
let uri = "file:///test/main.py"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let content = "# <!-- {=block} -->\n# content\n# <!-- {/block} -->\n";
let (blocks, diagnostics) = parse_document_content(&uri, content);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].name, "block");
assert!(diagnostics.is_empty());
}
#[test]
fn parse_document_content_empty_string() {
let uri = "file:///test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid URI"));
let (blocks, diagnostics) = parse_document_content(&uri, "");
assert!(blocks.is_empty());
assert!(diagnostics.is_empty());
}
#[test]
fn hover_provider_shows_content_in_code_block() {
let provider_template =
"<!-- {@greeting} -->\n\nHello from provider!\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block.clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let position = to_lsp_position(&provider_block.opening.start);
let hover = compute_hover(&state, &provider_uri, position);
assert!(hover.is_some());
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(
markup.value.contains("Hello from provider!"),
"hover should show provider content"
);
assert!(
markup.value.contains("```"),
"hover should render content in a code block"
);
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn hover_consumer_shows_provider_source_path() {
let (state, uri) = make_test_state("Hello world!", "Old content");
let doc = state.documents.get(&uri).unwrap_or_else(|| panic!("doc"));
let block = doc
.blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("consumer block"));
let position = to_lsp_position(&block.opening.start);
let hover = compute_hover(&state, &uri, position);
assert!(hover.is_some());
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(
markup.value.contains("Provider source:"),
"should show provider source label"
);
assert!(
markup.value.contains("template.t.md"),
"should show provider file path"
);
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn goto_definition_cursor_between_blocks_returns_none() {
let content = "\
# Heading
<!-- {=greeting} -->
Hello
<!-- {/greeting} -->
Some text between blocks.
<!-- {=farewell} -->
Bye
<!-- {/farewell} -->
";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 8,
character: 5,
};
let result = compute_goto_definition(&state, &uri, position);
assert!(
result.is_none(),
"should return None when cursor is between blocks"
);
}
#[test]
fn code_actions_for_multiple_stale_blocks() {
let consumer_doc = "\
<!-- {=greeting} -->
Old greeting
<!-- {/greeting} -->
<!-- {=farewell} -->
Old farewell
<!-- {/farewell} -->
";
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks,
parse_diagnostics: Vec::new(),
},
);
let provider_template_1 = "<!-- {@greeting} -->\n\nNew greeting\n\n<!-- {/greeting} -->\n";
let provider_blocks_1 = parse(provider_template_1).unwrap_or_default();
let provider_template_2 = "<!-- {@farewell} -->\n\nNew farewell\n\n<!-- {/farewell} -->\n";
let provider_blocks_2 = parse(provider_template_2).unwrap_or_default();
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: provider_blocks_1[0].clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template_1, &provider_blocks_1[0]),
},
);
providers.insert(
"farewell".to_string(),
ProviderEntry {
block: provider_blocks_2[0].clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template_2, &provider_blocks_2[0]),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 20,
character: 0,
},
};
let actions = compute_code_actions(&state, &uri, range);
assert_eq!(
actions.len(),
2,
"expected 2 code actions (one per stale block)"
);
let titles: Vec<String> = actions
.iter()
.map(|a| {
match a {
CodeActionOrCommand::CodeAction(ca) => ca.title.clone(),
CodeActionOrCommand::Command(cmd) => cmd.title.clone(),
}
})
.collect();
assert!(
titles.iter().any(|t| t.contains("greeting")),
"expected greeting code action"
);
assert!(
titles.iter().any(|t| t.contains("farewell")),
"expected farewell code action"
);
}
#[test]
fn workspace_state_default() {
let state = WorkspaceState::default();
assert!(state.root.is_none());
assert!(state.documents.is_empty());
assert!(state.providers.is_empty());
assert!(state.consumers.is_empty());
assert!(state.data.is_empty());
}
#[test]
fn completion_inside_provider_tag_context() {
let doc = "<!-- {@gre";
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let provider_entry = ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello!\n\n".to_string(),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 0,
character: 9,
};
let completions = compute_completions(&state, &uri, position);
assert!(
!completions.is_empty(),
"expected completions in provider tag context"
);
assert!(
completions.iter().any(|c| c.label == "greeting"),
"expected 'greeting' completion item"
);
}
#[test]
fn completion_inside_close_tag_context() {
let doc = "<!-- {/gre";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let provider_entry = ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello!\n\n".to_string(),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 0,
character: 9,
};
let completions = compute_completions(&state, &uri, position);
assert!(
!completions.is_empty(),
"expected completions in close tag context"
);
assert!(
completions.iter().any(|c| c.label == "greeting"),
"expected 'greeting' completion item"
);
}
#[test]
fn code_action_skips_provider_blocks() {
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks: blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let block = &blocks[0];
let range = Range {
start: to_lsp_position(&block.opening.start),
end: to_lsp_position(&block.closing.end),
};
let actions = compute_code_actions(&state, &uri, range);
assert!(
actions.is_empty(),
"expected no code actions for provider block"
);
}
#[test]
fn rescan_project_with_valid_project_populates_state() {
let dir = tempdir().unwrap_or_else(|e| panic!("failed to create tempdir: {e}"));
let root = dir.path();
std::fs::write(
root.join("template.t.md"),
"<!-- {@greeting} -->\n\nHello from template!\n\n<!-- {/greeting} -->\n",
)
.unwrap_or_else(|e| panic!("failed to write template: {e}"));
std::fs::write(
root.join("readme.md"),
"<!-- {=greeting} -->\n\nOld content\n\n<!-- {/greeting} -->\n",
)
.unwrap_or_else(|e| panic!("failed to write readme: {e}"));
let mut state = WorkspaceState {
root: Some(root.to_path_buf()),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
state.rescan_project();
assert!(
state.providers.contains_key("greeting"),
"expected 'greeting' provider after rescan, got: {:?}",
state.providers.keys().collect::<Vec<_>>()
);
assert!(
!state.consumers.is_empty(),
"expected at least one consumer after rescan"
);
assert!(
state.consumers.iter().any(|c| c.block.name == "greeting"),
"expected a 'greeting' consumer"
);
}
#[test]
fn rescan_project_with_invalid_config_prints_error_but_does_not_panic() {
let dir = tempdir().unwrap_or_else(|e| panic!("failed to create tempdir: {e}"));
let root = dir.path();
std::fs::write(root.join("mdt.toml"), "this is not valid toml {{{{")
.unwrap_or_else(|e| panic!("failed to write mdt.toml: {e}"));
let mut state = WorkspaceState {
root: Some(root.to_path_buf()),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
state.rescan_project();
assert!(state.providers.is_empty());
assert!(state.consumers.is_empty());
}
#[test]
fn rescan_project_with_data_from_config() {
let dir = tempdir().unwrap_or_else(|e| panic!("failed to create tempdir: {e}"));
let root = dir.path();
std::fs::write(root.join("mdt.toml"), "[data]\npkg = \"package.json\"\n")
.unwrap_or_else(|e| panic!("failed to write mdt.toml: {e}"));
std::fs::write(
root.join("package.json"),
r#"{"name": "test-pkg", "version": "1.0.0"}"#,
)
.unwrap_or_else(|e| panic!("failed to write package.json: {e}"));
std::fs::write(
root.join("template.t.md"),
"<!-- {@version} -->\n\n{{ pkg.version }}\n\n<!-- {/version} -->\n",
)
.unwrap_or_else(|e| panic!("failed to write template: {e}"));
let mut state = WorkspaceState {
root: Some(root.to_path_buf()),
documents: HashMap::new(),
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
state.rescan_project();
assert!(
!state.data.is_empty(),
"expected data to be populated from mdt.toml config"
);
assert!(
state.data.contains_key("pkg"),
"expected 'pkg' namespace in data"
);
}
#[test]
fn update_document_in_project_non_file_uri_is_noop() {
let uri = "untitled:Untitled-1"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let blocks = parse(content).unwrap_or_default();
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let mut state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
state.update_document_in_project(&uri);
assert!(
state.providers.is_empty(),
"expected no providers for non-file URI"
);
assert!(
state.consumers.is_empty(),
"expected no consumers for non-file URI"
);
}
#[test]
fn diagnostics_stale_consumer_with_render_template_failure() {
let provider_content = "{{ broken";
let consumer_content = "something else";
let provider_template =
format!("<!-- {{@greeting}} -->\n\n{provider_content}\n\n<!-- {{/greeting}} -->\n");
let consumer_doc = format!(
"# Readme\n\n<!-- {{=greeting}} -->\n\n{consumer_content}\n\n<!-- {{/greeting}} -->\n"
);
let provider_blocks = parse(&provider_template).unwrap_or_default();
let (consumer_blocks, consumer_parse_diagnostics) =
parse_with_diagnostics(&consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block in template"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(&provider_template, &provider_blocks[0]),
};
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc,
blocks: consumer_blocks,
parse_diagnostics: consumer_parse_diagnostics,
},
);
let mut data = HashMap::new();
data.insert("pkg".to_string(), serde_json::json!({"version": "1.0.0"}));
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data,
};
let diagnostics = compute_diagnostics(&state, &consumer_uri);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("out of date")),
"expected stale consumer diagnostic even with template render failure, got: \
{diagnostics:?}"
);
}
#[test]
fn hover_consumer_with_render_template_failure_shows_fallback() {
let provider_content = "{{ broken";
let provider_template =
format!("<!-- {{@greeting}} -->\n\n{provider_content}\n\n<!-- {{/greeting}} -->\n");
let consumer_doc = "# Readme\n\n<!-- {=greeting} -->\n\nold\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(&provider_template).unwrap_or_default();
let (consumer_blocks, consumer_parse_diagnostics) =
parse_with_diagnostics(consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block in template"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(&provider_template, &provider_blocks[0]),
};
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: consumer_parse_diagnostics,
},
);
let mut data = HashMap::new();
data.insert("pkg".to_string(), serde_json::json!({"version": "1.0.0"}));
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data,
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("expected consumer block"));
let position = to_lsp_position(&block.opening.start);
let hover = compute_hover(&state, &consumer_uri, position);
assert!(hover.is_some(), "expected hover result with fallback");
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(
markup.value.contains("Consumer block"),
"expected 'Consumer block' in hover, got: {}",
markup.value
);
assert!(
markup.value.contains("greeting"),
"expected 'greeting' in hover, got: {}",
markup.value
);
assert!(
markup.value.contains("broken"),
"expected raw fallback content in hover, got: {}",
markup.value
);
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn code_action_with_render_template_failure_uses_fallback() {
let provider_content = "{{ broken";
let provider_template =
format!("<!-- {{@greeting}} -->\n\n{provider_content}\n\n<!-- {{/greeting}} -->\n");
let consumer_doc = "<!-- {=greeting} -->\n\nold content\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(&provider_template).unwrap_or_default();
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(&provider_template, &provider_blocks[0]),
};
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let mut data = HashMap::new();
data.insert("pkg".to_string(), serde_json::json!({"version": "1.0.0"}));
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data,
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("expected consumer block"));
let range = Range {
start: to_lsp_position(&block.opening.start),
end: to_lsp_position(&block.closing.end),
};
let actions = compute_code_actions(&state, &consumer_uri, range);
assert!(
!actions.is_empty(),
"expected code action even with render template failure"
);
let CodeActionOrCommand::CodeAction(action) = &actions[0] else {
panic!("expected CodeAction")
};
assert!(action.title.contains("Update block"));
assert!(action.title.contains("greeting"));
assert!(action.edit.is_some());
}
#[test]
fn diagnostics_stale_consumer_with_transformers() {
let provider_template = "<!-- {@greeting} -->\n\n Hello world! \n\n<!-- {/greeting} -->\n";
let consumer_doc = "<!-- {=greeting|trim} -->\n\nnot trimmed content\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let (consumer_blocks, consumer_parse_diagnostics) =
parse_with_diagnostics(consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks,
parse_diagnostics: consumer_parse_diagnostics,
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &consumer_uri);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("out of date")),
"expected stale consumer diagnostic with transformers applied, got: {diagnostics:?}"
);
}
#[test]
fn hover_provider_with_zero_consumers() {
let provider_template = "<!-- {@greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block.clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let position = to_lsp_position(&provider_block.opening.start);
let hover = compute_hover(&state, &provider_uri, position);
assert!(hover.is_some());
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(
markup.value.contains("Provider block"),
"expected 'Provider block' in hover"
);
assert!(
markup.value.contains("0 consumer(s)"),
"expected '0 consumer(s)' in hover, got: {}",
markup.value
);
assert!(
!markup.value.contains("Consumers in:"),
"should not list consumers when there are none"
);
assert!(
markup.value.contains("Hello!"),
"expected provider content in hover"
);
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn document_symbols_provider_block_has_class_kind() {
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let symbols = compute_document_symbols(&state, &uri);
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name, "@greeting");
assert_eq!(symbols[0].kind, SymbolKind::CLASS);
}
#[test]
fn document_symbols_consumer_block_has_variable_kind() {
let content = "<!-- {=greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let symbols = compute_document_symbols(&state, &uri);
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name, "=greeting");
assert_eq!(symbols[0].kind, SymbolKind::VARIABLE);
}
#[test]
fn diagnostics_provider_with_consumers_no_unused_warning() {
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(5, 1, 28, 5, 22, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello\n\n".to_string(),
},
);
let consumers = vec![ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/readme.md"),
content: "\n\nHello\n\n".to_string(),
}];
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers,
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
assert!(
!diagnostics
.iter()
.any(|d| d.message.contains("has no consumers")),
"provider with consumers should not produce unused warning, got: {diagnostics:?}"
);
}
#[test]
fn diagnostics_stale_consumer_with_successful_template_rendering() {
let provider_template =
"<!-- {@version} -->\n\nVersion: {{ pkg.version }}\n\n<!-- {/version} -->\n";
let consumer_doc = "<!-- {=version} -->\n\nVersion: 0.9.0\n\n<!-- {/version} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let (consumer_blocks, consumer_parse_diagnostics) =
parse_with_diagnostics(consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert("version".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks,
parse_diagnostics: consumer_parse_diagnostics,
},
);
let mut data = HashMap::new();
data.insert("pkg".to_string(), serde_json::json!({"version": "1.0.0"}));
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data,
};
let diagnostics = compute_diagnostics(&state, &consumer_uri);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("out of date")),
"expected stale diagnostic when rendered content differs, got: {diagnostics:?}"
);
let stale_diag = diagnostics
.iter()
.find(|d| d.message.contains("out of date"))
.unwrap_or_else(|| panic!("expected stale diagnostic"));
let data = stale_diag
.data
.as_ref()
.unwrap_or_else(|| panic!("expected diagnostic data"));
let expected_content = data["expected_content"]
.as_str()
.unwrap_or_else(|| panic!("expected expected_content string"));
assert!(
expected_content.contains("Version: 1.0.0"),
"expected rendered content to contain 'Version: 1.0.0', got: {expected_content}"
);
}
#[test]
fn hover_consumer_with_transformers_shows_transformed_content() {
let consumer_doc = "<!-- {=greeting|trim} -->\n\nstuff\n\n<!-- {/greeting} -->\n";
let (consumer_blocks, consumer_parse_diags) =
parse_with_diagnostics(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_entry = ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\n Hello world! \n\n".to_string(),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: consumer_parse_diags,
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("expected consumer block"));
let position = to_lsp_position(&block.opening.start);
let hover = compute_hover(&state, &uri, position);
assert!(hover.is_some(), "expected hover result");
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(
markup.value.contains("Transformers"),
"expected 'Transformers' section, got: {}",
markup.value
);
assert!(
markup.value.contains("trim"),
"expected 'trim' in transformers, got: {}",
markup.value
);
assert!(
markup.value.contains("Hello world!"),
"expected trimmed content in hover preview, got: {}",
markup.value
);
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn completion_lists_all_providers() {
let doc = "<!-- {=";
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: doc.to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
);
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello!\n\n".to_string(),
},
);
providers.insert(
"installation".to_string(),
ProviderEntry {
block: Block {
name: "installation".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(5, 1, 50, 5, 25, 74),
closing: mdt_core::Position::new(7, 1, 80, 7, 25, 104),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nInstall it.\n\n".to_string(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let position = Position {
line: 0,
character: 7,
};
let completions = compute_completions(&state, &uri, position);
assert_eq!(
completions.len(),
2,
"expected 2 completion items for 2 providers"
);
let labels: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
assert!(labels.contains(&"greeting"));
assert!(labels.contains(&"installation"));
}
#[test]
fn code_action_with_successful_template_rendering() {
let provider_template =
"<!-- {@version} -->\n\nVersion: {{ pkg.version }}\n\n<!-- {/version} -->\n";
let consumer_doc = "<!-- {=version} -->\n\nVersion: 0.9.0\n\n<!-- {/version} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert("version".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let mut data = HashMap::new();
data.insert("pkg".to_string(), serde_json::json!({"version": "1.0.0"}));
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data,
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("expected consumer block"));
let range = Range {
start: to_lsp_position(&block.opening.start),
end: to_lsp_position(&block.closing.end),
};
let actions = compute_code_actions(&state, &consumer_uri, range);
assert!(
!actions.is_empty(),
"expected code action for stale consumer with rendered template"
);
let CodeActionOrCommand::CodeAction(action) = &actions[0] else {
panic!("expected CodeAction")
};
assert!(action.title.contains("Update block"));
assert!(action.title.contains("version"));
let edit = action
.edit
.as_ref()
.unwrap_or_else(|| panic!("expected workspace edit"));
let changes = edit
.changes
.as_ref()
.unwrap_or_else(|| panic!("expected changes"));
let edits = changes
.get(&consumer_uri)
.unwrap_or_else(|| panic!("expected edits for consumer URI"));
assert!(
edits[0].new_text.contains("Version: 1.0.0"),
"expected rendered content in edit, got: {}",
edits[0].new_text
);
}
#[test]
fn hover_provider_shows_raw_content_with_template_syntax() {
let provider_template =
"<!-- {@version} -->\n\nVersion: {{ pkg.version }}\n\n<!-- {/version} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block.clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("version".to_string(), provider_entry);
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let position = to_lsp_position(&provider_block.opening.start);
let hover = compute_hover(&state, &provider_uri, position);
assert!(hover.is_some());
if let HoverContents::Markup(markup) = &hover.unwrap().contents {
assert!(
markup.value.contains("Provider block"),
"expected 'Provider block' header"
);
assert!(
markup.value.contains("pkg.version"),
"expected raw template syntax in provider hover, got: {}",
markup.value
);
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn diagnostics_multiple_blocks_mixed_states() {
let consumer_doc = "<!-- {=greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n\n<!-- {=missing} \
-->\n\nstuff\n\n<!-- {/missing} -->\n";
let (consumer_blocks, consumer_parse_diagnostics) =
parse_with_diagnostics(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert(
"greeting".to_string(),
ProviderEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Provider,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/template.t.md"),
content: "\n\nHello!\n\n".to_string(),
},
);
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks,
parse_diagnostics: consumer_parse_diagnostics,
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers: Vec::new(),
data: HashMap::new(),
};
let diagnostics = compute_diagnostics(&state, &uri);
assert!(
diagnostics
.iter()
.any(|d| d.message.contains("No provider found") && d.message.contains("missing")),
"expected missing provider diagnostic for 'missing' block, got: {diagnostics:?}"
);
assert!(
!diagnostics
.iter()
.any(|d| d.message.contains("greeting") && d.message.contains("out of date")),
"'greeting' should be up to date, got: {diagnostics:?}"
);
}
#[test]
fn update_document_in_project_provider_in_non_template_file_not_registered() {
let content = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let mut state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
state.update_document_in_project(&uri);
assert!(
state.providers.is_empty(),
"provider in non-template file should not be registered"
);
}
#[test]
fn document_symbols_multiple_blocks_correct_ranges() {
let content = "<!-- {@first} -->\n\nContent1\n\n<!-- {/first} -->\n\n<!-- {=second} \
-->\n\nContent2\n\n<!-- {/second} -->\n";
let blocks = parse(content).unwrap_or_default();
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: content.to_string(),
blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let symbols = compute_document_symbols(&state, &uri);
assert_eq!(symbols.len(), 2);
assert_eq!(symbols[0].name, "@first");
assert_eq!(symbols[0].kind, SymbolKind::CLASS);
assert_eq!(symbols[1].name, "=second");
assert_eq!(symbols[1].kind, SymbolKind::VARIABLE);
assert!(
symbols[0].range.start.line < symbols[0].range.end.line,
"expected multi-line range for block"
);
}
fn make_args_test_state(consumer_arg: &str, consumer_body: &str) -> (WorkspaceState, Uri) {
let provider_template =
"<!-- {@badges:\"crate_name\"} -->\n\n[]\n\n<!-- {/badges} -->\n";
let consumer_doc = format!(
"# Readme\n\n<!-- {{=badges:\"{consumer_arg}\"}} -->\n\n{consumer_body}\n\n<!-- \
{{/badges}} -->\n"
);
let provider_blocks = parse(provider_template).unwrap_or_default();
let (consumer_blocks, consumer_parse_diagnostics) =
parse_with_diagnostics(&consumer_doc).unwrap_or_default();
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block in template"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut providers = HashMap::new();
providers.insert("badges".to_string(), provider_entry.clone());
let mut consumers = Vec::new();
for block in &consumer_blocks {
if block.r#type == BlockType::Consumer {
consumers.push(ConsumerEntry {
block: block.clone(),
file: PathBuf::from("/tmp/test/readme.md"),
content: extract_content_between_tags(&consumer_doc, block),
});
}
}
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc,
blocks: consumer_blocks,
parse_diagnostics: consumer_parse_diagnostics,
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers,
data: HashMap::new(),
};
(state, consumer_uri)
}
#[test]
fn diagnostics_stale_consumer_with_block_arguments() {
let (state, uri) = make_args_test_state("mdt_core", "Old stale content");
let diagnostics = compute_diagnostics(&state, &uri);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
assert!(
diagnostics[0].message.contains("out of date"),
"expected 'out of date' in message: {}",
diagnostics[0].message
);
assert!(diagnostics[0].message.contains("badges"));
let data = diagnostics[0]
.data
.as_ref()
.unwrap_or_else(|| panic!("expected diagnostic data"));
let expected_content = data["expected_content"]
.as_str()
.unwrap_or_else(|| panic!("expected expected_content string"));
assert!(
expected_content.contains("mdt_core"),
"expected rendered argument 'mdt_core' in expected content, got: {expected_content}"
);
assert!(
!expected_content.contains("{{ crate_name }}"),
"template variable should be interpolated, got: {expected_content}"
);
}
#[test]
fn hover_on_consumer_with_block_arguments_shows_rendered_content() {
let (state, uri) = make_args_test_state("mdt_core", "Old content");
let doc = state.documents.get(&uri).unwrap();
let block = doc
.blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let position = to_lsp_position(&block.opening.start);
let hover = compute_hover(&state, &uri, position);
assert!(hover.is_some());
let hover = hover.unwrap();
if let HoverContents::Markup(markup) = &hover.contents {
assert!(
markup.value.contains("Consumer block"),
"expected 'Consumer block' in hover: {}",
markup.value
);
assert!(
markup.value.contains("badges"),
"expected block name 'badges' in hover: {}",
markup.value
);
assert!(
markup.value.contains("mdt_core"),
"expected rendered argument 'mdt_core' in hover content: {}",
markup.value
);
assert!(
!markup.value.contains("{{ crate_name }}"),
"template variable should be interpolated in hover content: {}",
markup.value
);
} else {
panic!("expected Markup hover contents");
}
}
#[test]
fn code_action_for_stale_consumer_with_block_arguments() {
let (state, uri) = make_args_test_state("mdt_core", "Old stale content");
let doc = state.documents.get(&uri).unwrap();
let block = doc
.blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let range = Range {
start: to_lsp_position(&block.opening.start),
end: to_lsp_position(&block.closing.end),
};
let actions = compute_code_actions(&state, &uri, range);
assert!(!actions.is_empty(), "expected at least one code action");
let CodeActionOrCommand::CodeAction(action) = &actions[0] else {
panic!("expected CodeAction")
};
assert!(action.title.contains("Update block"));
assert!(action.title.contains("badges"));
assert!(action.edit.is_some());
let edit = action.edit.as_ref().unwrap();
let changes = edit.changes.as_ref().unwrap();
let edits = changes.get(&uri).unwrap();
let new_text = &edits[0].new_text;
assert!(
new_text.contains("mdt_core"),
"expected rendered argument 'mdt_core' in code action edit, got: {new_text}"
);
assert!(
!new_text.contains("{{ crate_name }}"),
"template variable should be interpolated in code action edit, got: {new_text}"
);
}
#[test]
fn references_from_consumer_returns_provider_and_consumers() {
let (state, consumer_uri) = make_test_state("Hello!", "Old");
let doc = state.documents.get(&consumer_uri).unwrap();
let block = doc
.blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let position = to_lsp_position(&block.opening.start);
let result = compute_references(&state, &consumer_uri, position);
assert!(result.is_some(), "expected references result");
let locations = result.unwrap();
assert!(
locations.len() >= 2,
"expected at least 2 locations (provider + consumer), got {}",
locations.len()
);
assert!(
locations
.iter()
.any(|l| l.uri.path().as_str().contains("template.t.md")),
"expected provider location in references"
);
assert!(
locations
.iter()
.any(|l| l.uri.path().as_str().contains("readme.md")),
"expected consumer location in references"
);
}
#[test]
fn references_from_provider_returns_provider_and_consumers() {
let provider_template = "<!-- {@greeting} -->\n\nHello!\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected a provider block"));
let provider_entry = ProviderEntry {
block: provider_block.clone(),
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let consumers = vec![
ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/readme.md"),
content: "\n\nold\n\n".to_string(),
},
ConsumerEntry {
block: Block {
name: "greeting".to_string(),
r#type: BlockType::Consumer,
opening: mdt_core::Position::new(1, 1, 0, 1, 20, 19),
closing: mdt_core::Position::new(3, 1, 30, 3, 20, 49),
transformers: Vec::new(),
arguments: vec![],
},
file: PathBuf::from("/tmp/test/docs.md"),
content: "\n\nold\n\n".to_string(),
},
];
let mut documents = HashMap::new();
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers,
data: HashMap::new(),
};
let position = to_lsp_position(&provider_block.opening.start);
let result = compute_references(&state, &provider_uri, position);
assert!(result.is_some(), "expected references result");
let locations = result.unwrap();
assert_eq!(locations.len(), 3, "expected 3 locations");
let paths: Vec<String> = locations
.iter()
.map(|l| l.uri.path().as_str().to_string())
.collect();
assert!(
paths.iter().any(|p| p.contains("template.t.md")),
"expected provider in references"
);
assert!(
paths.iter().any(|p| p.contains("readme.md")),
"expected readme consumer in references"
);
assert!(
paths.iter().any(|p| p.contains("docs.md")),
"expected docs consumer in references"
);
}
#[test]
fn references_outside_block_returns_none() {
let (state, uri) = make_test_state("Hello!", "Hello!");
let position = Position {
line: 0,
character: 0,
};
let result = compute_references(&state, &uri, position);
assert!(result.is_none(), "expected None for position outside block");
}
#[test]
fn references_consumer_without_provider_returns_only_consumer() {
let consumer_doc = "<!-- {=orphan} -->\n\nstuff\n\n<!-- {/orphan} -->\n";
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let consumers = vec![ConsumerEntry {
block: consumer_blocks[0].clone(),
file: PathBuf::from("/tmp/test/readme.md"),
content: "\n\nstuff\n\n".to_string(),
}];
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers,
data: HashMap::new(),
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let position = to_lsp_position(&block.opening.start);
let result = compute_references(&state, &uri, position);
assert!(result.is_some(), "expected references result");
let locations = result.unwrap();
assert_eq!(locations.len(), 1, "expected only the consumer location");
assert!(locations[0].uri.path().as_str().contains("readme.md"));
}
#[test]
fn prepare_rename_on_consumer_returns_name_range() {
let consumer_doc = "<!-- {=greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let position = to_lsp_position(&block.opening.start);
let result = compute_prepare_rename(&state, &uri, position);
assert!(result.is_some(), "expected prepare rename result");
match result.unwrap() {
PrepareRenameResponse::Range(range) => {
assert_eq!(range.start.line, 0);
assert_eq!(range.start.character, 7);
assert_eq!(range.end.line, 0);
assert_eq!(range.end.character, 15);
}
other => panic!("expected Range response, got: {other:?}"),
}
}
#[test]
fn prepare_rename_on_provider_returns_name_range() {
let provider_doc = "<!-- {@myBlock} -->\n\nContent\n\n<!-- {/myBlock} -->\n";
let provider_blocks = parse(provider_doc).unwrap_or_default();
let uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let mut documents = HashMap::new();
documents.insert(
uri.clone(),
DocumentState {
content: provider_doc.to_string(),
blocks: provider_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers: HashMap::new(),
consumers: Vec::new(),
data: HashMap::new(),
};
let block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.unwrap();
let position = to_lsp_position(&block.opening.start);
let result = compute_prepare_rename(&state, &uri, position);
assert!(result.is_some(), "expected prepare rename result");
match result.unwrap() {
PrepareRenameResponse::Range(range) => {
assert_eq!(range.start.line, 0);
assert_eq!(range.start.character, 7);
assert_eq!(range.end.line, 0);
assert_eq!(range.end.character, 14);
}
other => panic!("expected Range response, got: {other:?}"),
}
}
#[test]
fn prepare_rename_outside_block_returns_none() {
let (state, uri) = make_test_state("Hello!", "Hello!");
let position = Position {
line: 0,
character: 0,
};
let result = compute_prepare_rename(&state, &uri, position);
assert!(result.is_none(), "expected None for position outside block");
}
#[test]
fn rename_consumer_renames_both_tags_in_open_document() {
let consumer_doc = "<!-- {=greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let consumer_uri = "file:///tmp/test/readme.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_template = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let provider_blocks = parse(provider_template).unwrap_or_default();
let provider_uri = "file:///tmp/test/template.t.md"
.parse::<Uri>()
.unwrap_or_else(|_| panic!("invalid test URI"));
let provider_block = provider_blocks
.iter()
.find(|b| b.r#type == BlockType::Provider)
.cloned()
.unwrap_or_else(|| panic!("expected provider block"));
let provider_entry = ProviderEntry {
block: provider_block,
file: PathBuf::from("/tmp/test/template.t.md"),
content: extract_content_between_tags(provider_template, &provider_blocks[0]),
};
let mut providers = HashMap::new();
providers.insert("greeting".to_string(), provider_entry);
let mut consumers = Vec::new();
for block in &consumer_blocks {
if block.r#type == BlockType::Consumer {
consumers.push(ConsumerEntry {
block: block.clone(),
file: PathBuf::from("/tmp/test/readme.md"),
content: extract_content_between_tags(consumer_doc, block),
});
}
}
let mut documents = HashMap::new();
documents.insert(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
);
documents.insert(
provider_uri.clone(),
DocumentState {
content: provider_template.to_string(),
blocks: provider_blocks,
parse_diagnostics: Vec::new(),
},
);
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents,
providers,
consumers,
data: HashMap::new(),
};
let block = consumer_blocks
.iter()
.find(|b| b.r#type == BlockType::Consumer)
.unwrap();
let position = to_lsp_position(&block.opening.start);
let result = compute_rename(&state, &consumer_uri, position, "salutation");
assert!(result.is_some(), "expected rename result");
let edit = result.unwrap();
let changes = edit.changes.unwrap();
assert!(
changes.contains_key(&consumer_uri),
"expected edits for consumer file"
);
assert!(
changes.contains_key(&provider_uri),
"expected edits for provider file"
);
let consumer_edits = &changes[&consumer_uri];
assert_eq!(
consumer_edits.len(),
2,
"expected 2 edits for consumer (open + close tag)"
);
for edit in consumer_edits {
assert_eq!(edit.new_text, "salutation");
}
let provider_edits = &changes[&provider_uri];
assert_eq!(
provider_edits.len(),
2,
"expected 2 edits for provider (open + close tag)"
);
for edit in provider_edits {
assert_eq!(edit.new_text, "salutation");
}
}
#[test]
fn rename_outside_block_returns_none() {
let (state, uri) = make_test_state("Hello!", "Hello!");
let position = Position {
line: 0,
character: 0,
};
let result = compute_rename(&state, &uri, position, "newName");
assert!(result.is_none(), "expected None for position outside block");
}
#[test]
fn find_name_range_in_consumer_tag() {
let tag = "<!-- {=greeting} -->";
let start = Position {
line: 0,
character: 0,
};
let range = find_name_range_in_tag(tag, start, "greeting");
assert!(range.is_some(), "expected name range");
let range = range.unwrap();
assert_eq!(range.start.line, 0);
assert_eq!(range.start.character, 7); assert_eq!(range.end.line, 0);
assert_eq!(range.end.character, 15); }
#[test]
fn find_name_range_in_provider_tag() {
let tag = "<!-- {@myBlock} -->";
let start = Position {
line: 0,
character: 0,
};
let range = find_name_range_in_tag(tag, start, "myBlock");
assert!(range.is_some(), "expected name range");
let range = range.unwrap();
assert_eq!(range.start.line, 0);
assert_eq!(range.start.character, 7); assert_eq!(range.end.line, 0);
assert_eq!(range.end.character, 14); }
#[test]
fn find_name_range_in_close_tag() {
let tag = "<!-- {/greeting} -->";
let start = Position {
line: 0,
character: 0,
};
let range = find_name_range_in_tag(tag, start, "greeting");
assert!(range.is_some(), "expected name range");
let range = range.unwrap();
assert_eq!(range.start.line, 0);
assert_eq!(range.start.character, 7); assert_eq!(range.end.line, 0);
assert_eq!(range.end.character, 15); }
#[test]
fn find_name_range_with_nonzero_start() {
let tag = "<!-- {=greeting} -->";
let start = Position {
line: 5,
character: 10,
};
let range = find_name_range_in_tag(tag, start, "greeting");
assert!(range.is_some(), "expected name range");
let range = range.unwrap();
assert_eq!(range.start.line, 5);
assert_eq!(range.start.character, 17); assert_eq!(range.end.line, 5);
assert_eq!(range.end.character, 25); }
#[test]
fn find_name_range_in_consumer_with_transformers() {
let tag = "<!-- {=greeting|trim|indent:\" \"} -->";
let start = Position {
line: 0,
character: 0,
};
let range = find_name_range_in_tag(tag, start, "greeting");
assert!(range.is_some(), "expected name range");
let range = range.unwrap();
assert_eq!(range.start.line, 0);
assert_eq!(range.start.character, 7);
assert_eq!(range.end.line, 0);
assert_eq!(range.end.character, 15);
}
#[test]
fn lsp_position_to_offset_start_of_file() {
let content = "hello\nworld\n";
let pos = Position {
line: 0,
character: 0,
};
assert_eq!(lsp_position_to_offset(content, pos), Some(0));
}
#[test]
fn lsp_position_to_offset_middle_of_first_line() {
let content = "hello\nworld\n";
let pos = Position {
line: 0,
character: 3,
};
assert_eq!(lsp_position_to_offset(content, pos), Some(3));
}
#[test]
fn lsp_position_to_offset_end_of_first_line() {
let content = "hello\nworld\n";
let pos = Position {
line: 0,
character: 5,
};
assert_eq!(lsp_position_to_offset(content, pos), Some(5));
}
#[test]
fn lsp_position_to_offset_start_of_second_line() {
let content = "hello\nworld\n";
let pos = Position {
line: 1,
character: 0,
};
assert_eq!(lsp_position_to_offset(content, pos), Some(6));
}
#[test]
fn lsp_position_to_offset_middle_of_second_line() {
let content = "hello\nworld\n";
let pos = Position {
line: 1,
character: 3,
};
assert_eq!(lsp_position_to_offset(content, pos), Some(9));
}
#[test]
fn lsp_position_to_offset_out_of_bounds_line() {
let content = "hello\nworld\n";
let pos = Position {
line: 5,
character: 0,
};
assert_eq!(lsp_position_to_offset(content, pos), None);
}
#[test]
fn lsp_position_to_offset_out_of_bounds_character() {
let content = "hello\nworld\n";
let pos = Position {
line: 0,
character: 99,
};
assert_eq!(lsp_position_to_offset(content, pos), None);
}
#[test]
fn lsp_position_to_offset_empty_content() {
let content = "";
let pos = Position {
line: 0,
character: 0,
};
assert_eq!(lsp_position_to_offset(content, pos), Some(0));
}
#[test]
fn lsp_position_to_offset_empty_line() {
let content = "hello\n\nworld";
let pos = Position {
line: 1,
character: 0,
};
assert_eq!(lsp_position_to_offset(content, pos), Some(6));
}
#[test]
fn lsp_position_to_offset_multibyte_utf8_ascii_like() {
let content = "a\u{20AC}b";
let pos_a = Position {
line: 0,
character: 0,
};
assert_eq!(lsp_position_to_offset(content, pos_a), Some(0));
let pos_euro = Position {
line: 0,
character: 1,
};
assert_eq!(lsp_position_to_offset(content, pos_euro), Some(1));
let pos_b = Position {
line: 0,
character: 2,
};
assert_eq!(lsp_position_to_offset(content, pos_b), Some(4));
}
#[test]
fn lsp_position_to_offset_surrogate_pair() {
let content = "a\u{1F600}b";
let pos_a = Position {
line: 0,
character: 0,
};
assert_eq!(lsp_position_to_offset(content, pos_a), Some(0));
let pos_emoji = Position {
line: 0,
character: 1,
};
assert_eq!(lsp_position_to_offset(content, pos_emoji), Some(1));
let pos_b = Position {
line: 0,
character: 3,
};
assert_eq!(lsp_position_to_offset(content, pos_b), Some(5));
let pos_mid_surrogate = Position {
line: 0,
character: 2,
};
assert_eq!(lsp_position_to_offset(content, pos_mid_surrogate), None);
}
fn apply_incremental_change(content: &str, range: Range, new_text: &str) -> String {
let mut result = content.to_string();
let start = lsp_position_to_offset(&result, range.start)
.unwrap_or_else(|| panic!("invalid start position"));
let end = lsp_position_to_offset(&result, range.end)
.unwrap_or_else(|| panic!("invalid end position"));
result.replace_range(start..end, new_text);
result
}
#[test]
fn incremental_insert_at_beginning() {
let content = "hello world";
let range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
};
let result = apply_incremental_change(content, range, "prefix ");
assert_eq!(result, "prefix hello world");
}
#[test]
fn incremental_insert_at_end() {
let content = "hello";
let range = Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 5,
},
};
let result = apply_incremental_change(content, range, " world");
assert_eq!(result, "hello world");
}
#[test]
fn incremental_delete_range() {
let content = "hello world";
let range = Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 11,
},
};
let result = apply_incremental_change(content, range, "");
assert_eq!(result, "hello");
}
#[test]
fn incremental_replace_range() {
let content = "hello world";
let range = Range {
start: Position {
line: 0,
character: 6,
},
end: Position {
line: 0,
character: 11,
},
};
let result = apply_incremental_change(content, range, "rust");
assert_eq!(result, "hello rust");
}
#[test]
fn incremental_multiline_insert() {
let content = "line1\nline2\nline3";
let range = Range {
start: Position {
line: 1,
character: 5,
},
end: Position {
line: 1,
character: 5,
},
};
let result = apply_incremental_change(content, range, " extra");
assert_eq!(result, "line1\nline2 extra\nline3");
}
#[test]
fn incremental_multiline_delete() {
let content = "line1\nline2\nline3";
let range = Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 2,
character: 0,
},
};
let result = apply_incremental_change(content, range, "\n");
assert_eq!(result, "line1\nline3");
}
#[test]
fn incremental_replace_across_lines() {
let content = "aaa\nbbb\nccc";
let range = Range {
start: Position {
line: 0,
character: 1,
},
end: Position {
line: 2,
character: 2,
},
};
let result = apply_incremental_change(content, range, "XYZ");
assert_eq!(result, "aXYZc");
}
#[test]
fn incremental_full_replacement_no_range() {
let original = "old content";
let new_text = "brand new content";
let result = new_text.to_string();
assert_eq!(result, "brand new content");
assert_ne!(result, original);
}
#[test]
fn incremental_multiple_sequential_changes() {
let mut content = "abcdef".to_string();
let range1 = Range {
start: Position {
line: 0,
character: 3,
},
end: Position {
line: 0,
character: 3,
},
};
let start1 = lsp_position_to_offset(&content, range1.start)
.unwrap_or_else(|| panic!("invalid position"));
let end1 =
lsp_position_to_offset(&content, range1.end).unwrap_or_else(|| panic!("invalid position"));
content.replace_range(start1..end1, "X");
assert_eq!(content, "abcXdef");
let range2 = Range {
start: Position {
line: 0,
character: 1,
},
end: Position {
line: 0,
character: 2,
},
};
let start2 = lsp_position_to_offset(&content, range2.start)
.unwrap_or_else(|| panic!("invalid position"));
let end2 =
lsp_position_to_offset(&content, range2.end).unwrap_or_else(|| panic!("invalid position"));
content.replace_range(start2..end2, "");
assert_eq!(content, "acXdef");
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_initialize_sets_workspace_root_and_capabilities() {
let tmp = tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
let root_uri = test_uri(tmp.path());
let (service, _) = LspService::new(MdtLanguageServer::new);
let result = service
.inner()
.initialize(InitializeParams {
workspace_folders: Some(vec![WorkspaceFolder {
uri: root_uri.clone(),
name: "tmp".to_string(),
}]),
..Default::default()
})
.await
.unwrap_or_else(|e| panic!("initialize: {e:?}"));
assert_eq!(
result.capabilities.text_document_sync,
Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::INCREMENTAL,
))
);
assert_eq!(
service.inner().state.read().await.root,
Some(tmp.path().to_path_buf())
);
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_did_open_tracks_document_state() {
let doc_path = PathBuf::from("/tmp/lsp-open-test/readme.md");
let uri = test_uri(&doc_path);
let (service, _) = LspService::new(MdtLanguageServer::new);
let content = "<!-- {=greeting} -->\n\nhello\n\n<!-- {/greeting} -->\n".to_string();
service
.inner()
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: content.clone(),
},
})
.await;
let state = service.inner().state.read().await;
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("document should be tracked after open"));
assert_eq!(doc.content, content);
assert_eq!(doc.blocks.len(), 1);
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_did_change_uses_last_change_for_untracked_documents() {
let doc_path = PathBuf::from("/tmp/lsp-change-test/readme.md");
let uri = test_uri(&doc_path);
let (service, _) = LspService::new(MdtLanguageServer::new);
service
.inner()
.did_change(DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier {
uri: uri.clone(),
version: 2,
},
content_changes: vec![
TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "ignored".to_string(),
},
TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "<!-- {=greeting} -->\n\nupdated\n\n<!-- {/greeting} -->\n".to_string(),
},
],
})
.await;
let state = service.inner().state.read().await;
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("document should be created from did_change"));
assert!(doc.content.contains("updated"));
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_did_close_removes_document_state() {
let doc_path = PathBuf::from("/tmp/lsp-close-test/readme.md");
let uri = test_uri(&doc_path);
let (service, _) = LspService::new(MdtLanguageServer::new);
service
.inner()
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "<!-- {=greeting} -->\n\nhello\n\n<!-- {/greeting} -->\n".to_string(),
},
})
.await;
service
.inner()
.did_close(DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
})
.await;
assert!(
!service
.inner()
.state
.read()
.await
.documents
.contains_key(&uri)
);
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_shutdown_returns_ok() {
let (service, _) = LspService::new(MdtLanguageServer::new);
service
.inner()
.shutdown()
.await
.unwrap_or_else(|e| panic!("shutdown: {e:?}"));
}
#[test]
fn diagnostics_inline_block_requires_template_argument() {
let (state, uri) = make_inline_test_state(None, "0.0.0", HashMap::new());
let diagnostics = compute_diagnostics(&state, &uri);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
assert!(
diagnostics[0]
.message
.contains("requires a template argument")
);
}
#[test]
fn diagnostics_inline_block_reports_stale_rendered_content() {
let (state, uri) = make_inline_test_state(
Some("{{ pkg.version }}"),
"0.0.0",
HashMap::from([("pkg".to_string(), serde_json::json!({"version": "1.2.3"}))]),
);
let diagnostics = compute_diagnostics(&state, &uri);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
assert!(
diagnostics[0]
.message
.contains("Inline block `version` is out of date")
);
assert_eq!(
diagnostics[0].data.as_ref().unwrap()["expected_content"],
"1.2.3"
);
}
#[test]
fn diagnostics_inline_block_reports_render_errors() {
let (state, uri) = make_inline_test_state(
Some("{% if true %}"),
"0.0.0",
HashMap::from([("pkg".to_string(), serde_json::json!({"version": "1.2.3"}))]),
);
let diagnostics = compute_diagnostics(&state, &uri);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
assert!(diagnostics[0].message.contains("failed to render"));
}
#[test]
fn hover_inline_block_shows_rendered_content() {
let (state, uri) = make_inline_test_state(
Some("{{ pkg.version }}"),
"0.0.0",
HashMap::from([("pkg".to_string(), serde_json::json!({"version": "1.2.3"}))]),
);
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("missing document"));
let block = doc
.blocks
.iter()
.find(|block| block.r#type == BlockType::Inline)
.unwrap_or_else(|| panic!("expected inline block"));
let hover = compute_hover(&state, &uri, to_lsp_position(&block.opening.start))
.unwrap_or_else(|| panic!("expected hover result"));
let HoverContents::Markup(markup) = hover.contents else {
panic!("expected markdown hover");
};
assert!(markup.value.contains("**Inline block:** `version`"));
assert!(markup.value.contains("**Template:** `{{ pkg.version }}`"));
assert!(markup.value.contains("1.2.3"));
}
#[test]
fn hover_inline_block_reports_missing_template_argument() {
let (state, uri) = make_inline_test_state(None, "0.0.0", HashMap::new());
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("missing document"));
let block = doc
.blocks
.iter()
.find(|block| block.r#type == BlockType::Inline)
.unwrap_or_else(|| panic!("expected inline block"));
let hover = compute_hover(&state, &uri, to_lsp_position(&block.opening.start))
.unwrap_or_else(|| panic!("expected hover result"));
let HoverContents::Markup(markup) = hover.contents else {
panic!("expected markdown hover");
};
assert!(markup.value.contains("Missing inline template argument"));
}
#[test]
fn hover_inline_block_lists_transformers() {
let inline_doc = "<!-- {~version:\"{{ pkg.version }}\"|trim} --> 1.2.3 <!-- {/version} -->\n";
let uri = test_uri(std::path::Path::new("/tmp/test/inline.md"));
let (blocks, parse_diagnostics) = parse_with_diagnostics(inline_doc).unwrap_or_default();
let block = blocks[0].clone();
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::from([(
uri.clone(),
DocumentState {
content: inline_doc.to_string(),
blocks,
parse_diagnostics,
},
)]),
providers: HashMap::new(),
consumers: vec![ConsumerEntry {
block: block.clone(),
file: PathBuf::from("/tmp/test/inline.md"),
content: extract_content_between_tags(inline_doc, &block),
}],
data: HashMap::from([("pkg".to_string(), serde_json::json!({"version": " 1.2.3 "}))]),
};
let hover = compute_hover(&state, &uri, to_lsp_position(&block.opening.start))
.unwrap_or_else(|| panic!("expected hover result"));
let HoverContents::Markup(markup) = hover.contents else {
panic!("expected markdown hover");
};
assert!(markup.value.contains("**Transformers:** trim"));
}
#[test]
fn hover_inline_block_reports_render_errors() {
let (state, uri) = make_inline_test_state(
Some("{% if true %}"),
"0.0.0",
HashMap::from([("pkg".to_string(), serde_json::json!({"version": "1.2.3"}))]),
);
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("missing document"));
let block = doc
.blocks
.iter()
.find(|block| block.r#type == BlockType::Inline)
.unwrap_or_else(|| panic!("expected inline block"));
let hover = compute_hover(&state, &uri, to_lsp_position(&block.opening.start))
.unwrap_or_else(|| panic!("expected hover result"));
let HoverContents::Markup(markup) = hover.contents else {
panic!("expected markdown hover");
};
assert!(markup.value.contains("Failed to render inline template"));
}
#[test]
fn code_action_for_stale_inline_block_updates_rendered_content() {
let (state, uri) = make_inline_test_state(
Some("{{ pkg.version }}"),
"0.0.0",
HashMap::from([("pkg".to_string(), serde_json::json!({"version": "1.2.3"}))]),
);
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("missing document"));
let block = doc
.blocks
.iter()
.find(|block| block.r#type == BlockType::Inline)
.unwrap_or_else(|| panic!("expected inline block"));
let range = Range {
start: to_lsp_position(&block.opening.start),
end: to_lsp_position(&block.closing.end),
};
let actions = compute_code_actions(&state, &uri, range);
assert_eq!(actions.len(), 1);
let CodeActionOrCommand::CodeAction(action) = &actions[0] else {
panic!("expected inline code action");
};
let edit = action
.edit
.as_ref()
.unwrap_or_else(|| panic!("missing edit"));
let changes = edit
.changes
.as_ref()
.unwrap_or_else(|| panic!("missing workspace changes"));
let new_text = &changes[&uri][0].new_text;
assert_eq!(new_text, "1.2.3");
}
#[test]
fn references_from_inline_returns_other_inline_blocks() {
let uri_a = test_uri(std::path::Path::new("/tmp/test/readme.md"));
let uri_b = test_uri(std::path::Path::new("/tmp/test/docs.md"));
let doc_a = "<!-- {~version:\"{{ pkg.version }}\"} -->0.0.0<!-- {/version} -->\n";
let doc_b = "<!-- {~version:\"{{ pkg.version }}\"} -->1.0.0<!-- {/version} -->\n";
let (blocks_a, diagnostics_a) = parse_with_diagnostics(doc_a).unwrap_or_default();
let (blocks_b, diagnostics_b) = parse_with_diagnostics(doc_b).unwrap_or_default();
let inline_a = blocks_a[0].clone();
let inline_b = blocks_b[0].clone();
let state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::from([
(
uri_a.clone(),
DocumentState {
content: doc_a.to_string(),
blocks: blocks_a,
parse_diagnostics: diagnostics_a,
},
),
(
uri_b.clone(),
DocumentState {
content: doc_b.to_string(),
blocks: blocks_b,
parse_diagnostics: diagnostics_b,
},
),
]),
providers: HashMap::new(),
consumers: vec![
ConsumerEntry {
block: inline_a.clone(),
file: PathBuf::from("/tmp/test/readme.md"),
content: extract_content_between_tags(doc_a, &inline_a),
},
ConsumerEntry {
block: inline_b.clone(),
file: PathBuf::from("/tmp/test/docs.md"),
content: extract_content_between_tags(doc_b, &inline_b),
},
],
data: HashMap::from([("pkg".to_string(), serde_json::json!({"version": "1.2.3"}))]),
};
let locations = compute_references(&state, &uri_a, to_lsp_position(&inline_a.opening.start))
.unwrap_or_else(|| panic!("expected references"));
assert_eq!(locations.len(), 2);
assert!(locations.iter().any(|location| location.uri == uri_a));
assert!(locations.iter().any(|location| location.uri == uri_b));
}
#[test]
fn rename_reads_unopened_provider_and_consumer_files_from_disk() {
let tmp = tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
let provider_path = tmp.path().join("template.t.md");
let consumer_path = tmp.path().join("readme.md");
let extra_consumer_path = tmp.path().join("docs.md");
let provider_doc = "<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
let consumer_doc = "<!-- {=greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n";
std::fs::write(&provider_path, provider_doc).unwrap_or_else(|e| panic!("write provider: {e}"));
std::fs::write(&consumer_path, consumer_doc).unwrap_or_else(|e| panic!("write consumer: {e}"));
std::fs::write(&extra_consumer_path, consumer_doc)
.unwrap_or_else(|e| panic!("write extra consumer: {e}"));
let provider_blocks = parse(provider_doc).unwrap_or_default();
let consumer_blocks = parse(consumer_doc).unwrap_or_default();
let provider_block = provider_blocks[0].clone();
let consumer_block = consumer_blocks[0].clone();
let provider_uri = test_uri(&provider_path);
let consumer_uri = test_uri(&consumer_path);
let extra_consumer_uri = test_uri(&extra_consumer_path);
let state = WorkspaceState {
root: Some(tmp.path().to_path_buf()),
documents: HashMap::from([(
consumer_uri.clone(),
DocumentState {
content: consumer_doc.to_string(),
blocks: consumer_blocks.clone(),
parse_diagnostics: Vec::new(),
},
)]),
providers: HashMap::from([(
"greeting".to_string(),
ProviderEntry {
block: provider_block.clone(),
file: provider_path.clone(),
content: extract_content_between_tags(provider_doc, &provider_block),
},
)]),
consumers: vec![
ConsumerEntry {
block: consumer_block.clone(),
file: consumer_path.clone(),
content: extract_content_between_tags(consumer_doc, &consumer_block),
},
ConsumerEntry {
block: consumer_block.clone(),
file: extra_consumer_path.clone(),
content: extract_content_between_tags(consumer_doc, &consumer_block),
},
],
data: HashMap::new(),
};
let edit = compute_rename(
&state,
&consumer_uri,
to_lsp_position(&consumer_block.opening.start),
"salutation",
)
.unwrap_or_else(|| panic!("expected rename edit"));
let changes = edit.changes.unwrap_or_else(|| panic!("missing changes"));
assert!(changes.contains_key(&consumer_uri));
assert!(changes.contains_key(&provider_uri));
assert!(changes.contains_key(&extra_consumer_uri));
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_initialize_falls_back_to_root_uri() {
let tmp = tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
let root_uri = test_uri(tmp.path());
let (service, _) = LspService::new(MdtLanguageServer::new);
service
.inner()
.initialize(InitializeParams {
#[allow(deprecated)]
root_uri: Some(root_uri),
..Default::default()
})
.await
.unwrap_or_else(|e| panic!("initialize with root_uri: {e:?}"));
assert_eq!(
service.inner().state.read().await.root,
Some(tmp.path().to_path_buf())
);
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_initialized_completes_without_error() {
let (service, _) = LspService::new(MdtLanguageServer::new);
service.inner().initialized(InitializedParams {}).await;
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_did_change_applies_incremental_edits() {
let doc_path = PathBuf::from("/tmp/lsp-change-tracked/readme.md");
let uri = test_uri(&doc_path);
let (service, _) = LspService::new(MdtLanguageServer::new);
service
.inner()
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "<!-- {=greeting} -->\n\nhello\n\n<!-- {/greeting} -->\n".to_string(),
},
})
.await;
service
.inner()
.did_change(DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier {
uri: uri.clone(),
version: 2,
},
content_changes: vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 2,
character: 0,
},
end: Position {
line: 2,
character: 5,
},
}),
range_length: None,
text: "updated".to_string(),
}],
})
.await;
let state = service.inner().state.read().await;
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("missing document"));
assert!(doc.content.contains("updated"));
assert!(!doc.content.contains("\n\nhello\n\n"));
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_did_change_full_replaces_tracked_document() {
let doc_path = PathBuf::from("/tmp/lsp-change-full/readme.md");
let uri = test_uri(&doc_path);
let (service, _) = LspService::new(MdtLanguageServer::new);
service
.inner()
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "<!-- {=greeting} -->\n\nhello\n\n<!-- {/greeting} -->\n".to_string(),
},
})
.await;
service
.inner()
.did_change(DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier {
uri: uri.clone(),
version: 2,
},
content_changes: vec![TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "<!-- {=greeting} -->\n\nreplacement\n\n<!-- {/greeting} -->\n".to_string(),
}],
})
.await;
let state = service.inner().state.read().await;
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("missing document"));
assert!(doc.content.contains("replacement"));
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_did_save_updates_template_provider_state() {
let doc_path = PathBuf::from("/tmp/lsp-save-template/template.t.md");
let uri = test_uri(&doc_path);
let (service, _) = LspService::new(MdtLanguageServer::new);
let content = "<!-- {@greeting} -->\n\nhello\n\n<!-- {/greeting} -->\n".to_string();
service
.inner()
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: content,
},
})
.await;
service
.inner()
.did_save(DidSaveTextDocumentParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
text: None,
})
.await;
let state = service.inner().state.read().await;
assert!(state.providers.contains_key("greeting"));
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_did_save_rescans_when_config_changes() {
let tmp = tempdir().unwrap_or_else(|e| panic!("tempdir: {e}"));
std::fs::write(
tmp.path().join("template.t.md"),
"<!-- {@greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n",
)
.unwrap_or_else(|e| panic!("write template: {e}"));
std::fs::write(
tmp.path().join("readme.md"),
"<!-- {=greeting} -->\n\nHello\n\n<!-- {/greeting} -->\n",
)
.unwrap_or_else(|e| panic!("write readme: {e}"));
std::fs::write(tmp.path().join("mdt.toml"), "").unwrap_or_else(|e| panic!("write config: {e}"));
let config_uri = test_uri(&tmp.path().join("mdt.toml"));
let (service, _) = LspService::new(MdtLanguageServer::new);
service.inner().state.write().await.root = Some(tmp.path().to_path_buf());
service
.inner()
.did_save(DidSaveTextDocumentParams {
text_document: TextDocumentIdentifier { uri: config_uri },
text: None,
})
.await;
let state = service.inner().state.read().await;
assert!(state.providers.contains_key("greeting"));
assert_eq!(state.consumers.len(), 1);
}
#[tokio::test(flavor = "current_thread")]
async fn language_server_request_wrappers_delegate_to_core_handlers() {
let (state, uri) = make_test_state("Hello world!", "Old content");
let doc = state
.documents
.get(&uri)
.unwrap_or_else(|| panic!("missing document"));
let block = doc
.blocks
.iter()
.find(|block| block.r#type == BlockType::Consumer)
.unwrap_or_else(|| panic!("expected consumer block"))
.clone();
let position = to_lsp_position(&block.opening.start);
let range = Range {
start: position,
end: to_lsp_position(&block.closing.end),
};
let (service, _) = LspService::new(MdtLanguageServer::new);
*service.inner().state.write().await = state;
assert!(
service
.inner()
.hover(HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position,
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap_or_else(|e| panic!("hover: {e:?}"))
.is_some()
);
let completion_uri = test_uri(std::path::Path::new("/tmp/test/completion.md"));
let completion_state = WorkspaceState {
root: Some(PathBuf::from("/tmp/test")),
documents: HashMap::from([(
completion_uri.clone(),
DocumentState {
content: "<!-- {=gre".to_string(),
blocks: Vec::new(),
parse_diagnostics: Vec::new(),
},
)]),
providers: service.inner().state.read().await.providers.clone(),
consumers: Vec::new(),
data: HashMap::new(),
};
*service.inner().state.write().await = completion_state;
assert!(matches!(
service
.inner()
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier {
uri: completion_uri.clone(),
},
position: Position {
line: 0,
character: 9,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap_or_else(|e| panic!("completion: {e:?}")),
Some(CompletionResponse::Array(_))
));
*service.inner().state.write().await = make_test_state("Hello world!", "Old content").0;
assert!(
service
.inner()
.goto_definition(GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position,
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap_or_else(|e| panic!("goto_definition: {e:?}"))
.is_some()
);
assert!(matches!(
service
.inner()
.document_symbol(DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap_or_else(|e| panic!("document_symbol: {e:?}")),
Some(DocumentSymbolResponse::Nested(_))
));
assert!(
service
.inner()
.code_action(CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range,
context: CodeActionContext {
diagnostics: Vec::new(),
only: None,
trigger_kind: None,
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap_or_else(|e| panic!("code_action: {e:?}"))
.is_some()
);
assert!(
service
.inner()
.references(ReferenceParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position,
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: ReferenceContext {
include_declaration: true,
},
})
.await
.unwrap_or_else(|e| panic!("references: {e:?}"))
.is_some()
);
assert!(
service
.inner()
.prepare_rename(TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position,
})
.await
.unwrap_or_else(|e| panic!("prepare_rename: {e:?}"))
.is_some()
);
assert!(
service
.inner()
.rename(RenameParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position,
},
new_name: "salutation".to_string(),
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap_or_else(|e| panic!("rename: {e:?}"))
.is_some()
);
}