#![cfg(feature = "lsp")]
use mkdlint::lsp::MkdlintLanguageServer;
use tower_lsp::lsp_types::*;
use tower_lsp::{LanguageServer, LspService};
async fn create_test_server() -> MkdlintLanguageServer {
let (service, _socket) = LspService::new(MkdlintLanguageServer::new);
service.inner().clone()
}
#[tokio::test]
async fn test_initialize_and_shutdown() {
let server = create_test_server().await;
let init_params = InitializeParams {
root_uri: Some(Url::parse("file:///test/workspace").unwrap()),
capabilities: ClientCapabilities::default(),
..Default::default()
};
let result = server.initialize(init_params).await.unwrap();
assert!(result.capabilities.text_document_sync.is_some());
assert!(result.capabilities.code_action_provider.is_some());
assert!(result.server_info.is_some());
assert_eq!(result.server_info.unwrap().name, "mkdlint");
server.shutdown().await.unwrap();
}
#[tokio::test]
async fn test_did_open_and_close() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Test\n\nTrailing spaces: \n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
server
.did_close(DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier { uri },
})
.await;
}
#[tokio::test]
async fn test_did_change_with_debouncing() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Test\n".to_string(),
},
})
.await;
for i in 2..10 {
server
.did_change(DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier {
uri: uri.clone(),
version: i,
},
content_changes: vec![TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: format!("# Test {}\n", i),
}],
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
}
tokio::time::sleep(tokio::time::Duration::from_millis(400)).await;
}
#[tokio::test]
async fn test_did_save_bypasses_debounce() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "#Bad\n".to_string(),
},
})
.await;
server
.did_save(DidSaveTextDocumentParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
text: None,
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
#[tokio::test]
async fn test_code_action_returns_actions() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "#No space\nTrailing: \n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.code_action(CodeActionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 2,
character: 0,
},
},
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
assert!(result.is_some());
}
#[tokio::test]
async fn test_execute_fix_all_command() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "#Bad\n#AlsoBad\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.execute_command(ExecuteCommandParams {
command: "mkdlint.fixAll".to_string(),
arguments: vec![serde_json::to_value(&uri).unwrap()],
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_workspace_roots_from_initialize() {
let server = create_test_server().await;
let workspace_folders = vec![
WorkspaceFolder {
uri: Url::parse("file:///workspace1").unwrap(),
name: "Workspace 1".to_string(),
},
WorkspaceFolder {
uri: Url::parse("file:///workspace2").unwrap(),
name: "Workspace 2".to_string(),
},
];
let init_params = InitializeParams {
workspace_folders: Some(workspace_folders),
capabilities: ClientCapabilities::default(),
..Default::default()
};
let result = server.initialize(init_params).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_unknown_execute_command() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let result = server
.execute_command(ExecuteCommandParams {
command: "mkdlint.unknownCommand".to_string(),
arguments: vec![],
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_hover_on_diagnostic() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "#No space\n".to_string(), },
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.hover(HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 0,
character: 0,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(result.is_some());
let hover = result.unwrap();
match hover.contents {
HoverContents::Markup(markup) => {
assert_eq!(markup.kind, MarkupKind::Markdown);
assert!(markup.value.contains("MD018"));
assert!(markup.value.contains("no-missing-space-atx"));
}
_ => panic!("Expected MarkupContent"),
}
}
#[tokio::test]
async fn test_hover_on_clean_line() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Good Heading\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.hover(HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 0,
character: 0,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn test_hover_shows_fix_availability() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "Trailing spaces \n".to_string(), },
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.hover(HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 0,
character: 0,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(result.is_some());
let hover = result.unwrap();
match hover.contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("Auto-fixable") || markup.value.contains("🔧"));
}
_ => panic!("Expected MarkupContent"),
}
}
#[tokio::test]
async fn test_capabilities_include_hover() {
let server = create_test_server().await;
let result = server
.initialize(InitializeParams::default())
.await
.unwrap();
assert!(result.capabilities.hover_provider.is_some());
}
#[tokio::test]
async fn test_did_change_watched_files_invalidates_cache() {
use tower_lsp::lsp_types::{DidChangeWatchedFilesParams, FileChangeType, FileEvent, Url};
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Test\nTrailing: \n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let config_uri = Url::parse("file:///.markdownlint.json").unwrap();
server
.did_change_watched_files(DidChangeWatchedFilesParams {
changes: vec![FileEvent {
uri: config_uri,
typ: FileChangeType::CHANGED,
}],
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
}
#[tokio::test]
async fn test_multiple_config_file_changes() {
use tower_lsp::lsp_types::{DidChangeWatchedFilesParams, FileChangeType, FileEvent, Url};
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
for i in 1..=3 {
let uri = Url::parse(&format!("file:///test{}.md", i)).unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri,
language_id: "markdown".to_string(),
version: 1,
text: format!("# Test {}\n", i),
},
})
.await;
}
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let changes = vec![
FileEvent {
uri: Url::parse("file:///.markdownlint.json").unwrap(),
typ: FileChangeType::CHANGED,
},
FileEvent {
uri: Url::parse("file:///.markdownlint.yaml").unwrap(),
typ: FileChangeType::CREATED,
},
];
server
.did_change_watched_files(DidChangeWatchedFilesParams { changes })
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
}
#[tokio::test]
async fn test_config_deletion_triggers_relint() {
use tower_lsp::lsp_types::{DidChangeWatchedFilesParams, FileChangeType, FileEvent, Url};
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Test\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let config_uri = Url::parse("file:///.markdownlint.json").unwrap();
server
.did_change_watched_files(DidChangeWatchedFilesParams {
changes: vec![FileEvent {
uri: config_uri,
typ: FileChangeType::DELETED,
}],
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
}
#[tokio::test]
async fn test_capabilities_include_document_symbol() {
let server = create_test_server().await;
let result = server
.initialize(InitializeParams::default())
.await
.unwrap();
assert!(
result.capabilities.document_symbol_provider.is_some(),
"Server should advertise documentSymbol capability"
);
}
#[tokio::test]
async fn test_document_symbol_flat_headings() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Title\n\n## Section A\n\n## Section B\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let result = server
.document_symbol(DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
assert!(result.is_some());
match result.unwrap() {
DocumentSymbolResponse::Nested(symbols) => {
assert_eq!(symbols.len(), 1, "Should have one top-level h1");
assert_eq!(symbols[0].name, "Title");
let children = symbols[0].children.as_ref().unwrap();
assert_eq!(children.len(), 2);
assert_eq!(children[0].name, "Section A");
assert_eq!(children[1].name, "Section B");
}
_ => panic!("Expected nested document symbols"),
}
}
#[tokio::test]
async fn test_document_symbol_nested_headings() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Root\n\n## Child\n\n### Grandchild\n\n## Child 2\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let result = server
.document_symbol(DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
match result.unwrap() {
DocumentSymbolResponse::Nested(symbols) => {
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name, "Root");
let children = symbols[0].children.as_ref().unwrap();
assert_eq!(children.len(), 2);
assert_eq!(children[0].name, "Child");
let grandchildren = children[0].children.as_ref().unwrap();
assert_eq!(grandchildren.len(), 1);
assert_eq!(grandchildren[0].name, "Grandchild");
assert_eq!(children[1].name, "Child 2");
}
_ => panic!("Expected nested document symbols"),
}
}
#[tokio::test]
async fn test_document_symbol_empty_document() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "Just some text, no headings.\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let result = server
.document_symbol(DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
match result.unwrap() {
DocumentSymbolResponse::Nested(symbols) => {
assert!(symbols.is_empty(), "No headings = no symbols");
}
_ => panic!("Expected nested document symbols"),
}
}
#[tokio::test]
async fn test_document_symbol_skips_code_blocks() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Real Heading\n\n```\n# Not a heading\n```\n\n## Another\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let result = server
.document_symbol(DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
match result.unwrap() {
DocumentSymbolResponse::Nested(symbols) => {
assert_eq!(symbols.len(), 1, "Should have one top-level heading");
assert_eq!(symbols[0].name, "Real Heading");
let children = symbols[0].children.as_ref().unwrap();
assert_eq!(children.len(), 1);
assert_eq!(children[0].name, "Another");
}
_ => panic!("Expected nested document symbols"),
}
}
#[tokio::test]
async fn test_document_symbol_detail_shows_level() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "## Level 2\n\n### Level 3\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let result = server
.document_symbol(DocumentSymbolParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
match result.unwrap() {
DocumentSymbolResponse::Nested(symbols) => {
assert_eq!(symbols[0].detail, Some("h2".to_string()));
let children = symbols[0].children.as_ref().unwrap();
assert_eq!(children[0].detail, Some("h3".to_string()));
}
_ => panic!("Expected nested document symbols"),
}
}
#[tokio::test]
async fn test_hover_multiple_errors_same_line() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "#No space and trailing \n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.hover(HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 0,
character: 0,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(result.is_some());
match result.unwrap().contents {
HoverContents::Markup(markup) => {
assert!(markup.value.contains("MD018"), "Should contain MD018");
assert!(markup.value.contains("MD009"), "Should contain MD009");
}
_ => panic!("Expected MarkupContent"),
}
}
#[tokio::test]
async fn test_hover_on_unknown_document() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///nonexistent.md").unwrap();
let result = server
.hover(HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 0,
character: 0,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(result.is_none(), "Hover on unknown doc should return None");
}
#[tokio::test]
async fn test_code_action_on_clean_document() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Clean Document\n\nNo issues here.\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.code_action(CodeActionParams {
text_document: TextDocumentIdentifier { uri },
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 2,
character: 0,
},
},
context: CodeActionContext {
diagnostics: vec![],
only: None,
trigger_kind: None,
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
assert!(
result.is_none(),
"Clean document should not produce code actions"
);
}
async fn open_doc(server: &MkdlintLanguageServer, uri: &Url, content: &str) {
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: content.to_string(),
},
})
.await;
}
#[tokio::test]
async fn test_completion_capability_declared() {
let server = create_test_server().await;
let result = server
.initialize(InitializeParams::default())
.await
.unwrap();
assert!(
result.capabilities.completion_provider.is_some(),
"Server should declare completion_provider capability"
);
}
#[tokio::test]
async fn test_completion_inside_ial_returns_items() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
let uri = Url::parse("file:///test/doc.md").unwrap();
let content = "# Heading\n\n{: \n";
open_doc(&server, &uri, content).await;
let result = server
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 3,
}, },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
let items = match result {
Some(CompletionResponse::Array(items)) => items,
_ => panic!("Expected CompletionResponse::Array"),
};
assert!(
!items.is_empty(),
"Should return completion items inside IAL"
);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(labels.contains(&"#"), "Should include # (ID selector)");
assert!(labels.contains(&"."), "Should include . (class selector)");
assert!(labels.contains(&"id"), "Should include id attribute");
assert!(labels.contains(&"class"), "Should include class attribute");
}
#[tokio::test]
async fn test_completion_outside_ial_returns_none() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
let uri = Url::parse("file:///test/doc2.md").unwrap();
let content = "# Heading\n\nSome paragraph text here.\n";
open_doc(&server, &uri, content).await;
let result = server
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 10,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(
result.is_none(),
"Should not return completions outside an IAL"
);
}
#[tokio::test]
async fn test_completion_after_closed_ial_returns_none() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
let uri = Url::parse("file:///test/doc3.md").unwrap();
let content = "# Heading\n\n{: #my-id}\n";
open_doc(&server, &uri, content).await;
let result = server
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 11,
}, },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(
result.is_none(),
"Should not return completions after a closed IAL"
);
}
#[tokio::test]
async fn test_completion_filters_by_typed_prefix() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
let uri = Url::parse("file:///test/doc4.md").unwrap();
let content = "# Heading\n\n{: ar\n";
open_doc(&server, &uri, content).await;
let result = server
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 5,
}, },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
let items = match result {
Some(CompletionResponse::Array(items)) => items,
_ => panic!("Expected CompletionResponse::Array"),
};
for item in &items {
assert!(
item.label.starts_with("ar"),
"Filtered item '{}' should start with 'ar'",
item.label
);
}
assert!(!items.is_empty(), "Should have at least aria-* matches");
}
fn lint_with_preset(markdown: &str, preset: &str) -> Vec<mkdlint::LintError> {
use mkdlint::{Config, LintOptions};
use std::collections::HashMap;
let mut cfg = Config {
preset: Some(preset.to_string()),
..Default::default()
};
cfg.apply_preset();
let mut strings = HashMap::new();
strings.insert("test.md".to_string(), markdown.to_string());
let options = LintOptions {
strings,
config: Some(cfg),
..Default::default()
};
mkdlint::lint_sync(&options)
.unwrap()
.get("test.md")
.unwrap_or(&[])
.to_vec()
}
fn lint_default(markdown: &str) -> Vec<mkdlint::LintError> {
use mkdlint::LintOptions;
use std::collections::HashMap;
let mut strings = HashMap::new();
strings.insert("test.md".to_string(), markdown.to_string());
let options = LintOptions {
strings,
..Default::default()
};
mkdlint::lint_sync(&options)
.unwrap()
.get("test.md")
.unwrap_or(&[])
.to_vec()
}
async fn initialize_with_preset(server: &MkdlintLanguageServer, preset: &str) {
server
.initialize(InitializeParams {
initialization_options: Some(serde_json::json!({ "preset": preset })),
..Default::default()
})
.await
.unwrap();
server.initialized(InitializedParams {}).await;
}
#[tokio::test]
async fn test_preset_kramdown_via_initialization_options() {
let server = create_test_server().await;
initialize_with_preset(&server, "kramdown").await;
let uri = Url::parse("file:///test.md").unwrap();
let content = "# Title\n\nSee footnote[^1].\n\n<b>bold</b>\n";
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: content.to_string(),
},
})
.await;
let errors = lint_with_preset(content, "kramdown");
let rule_names: Vec<&str> = errors.iter().map(|e| e.rule_names[0]).collect();
assert!(
rule_names.contains(&"KMD002"),
"kramdown preset should enable KMD002 (undefined footnote ref); got: {rule_names:?}"
);
assert!(
!rule_names.contains(&"MD033"),
"kramdown preset should disable MD033 (inline HTML); got: {rule_names:?}"
);
}
#[tokio::test]
async fn test_preset_github_via_initialization_options() {
let server = create_test_server().await;
initialize_with_preset(&server, "github").await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Title\n\nText https://example.com\n".to_string(),
},
})
.await;
let errors = lint_with_preset("# Title\n\nText https://example.com\n", "github");
let rule_names: Vec<&str> = errors.iter().map(|e| e.rule_names[0]).collect();
assert!(
!rule_names.contains(&"MD034"),
"github preset should disable MD034 (bare URLs); got: {rule_names:?}"
);
}
#[tokio::test]
async fn test_no_preset_in_initialization_options() {
let server = create_test_server().await;
server
.initialize(InitializeParams {
initialization_options: Some(serde_json::json!({})),
..Default::default()
})
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let errors = lint_default("# Title\n\nText https://example.com\n");
let rule_names: Vec<&str> = errors.iter().map(|e| e.rule_names[0]).collect();
assert!(
rule_names.contains(&"MD034"),
"no preset: MD034 should fire on bare URL; got: {rule_names:?}"
);
}
#[tokio::test]
async fn test_hover_on_rule_alias_shows_docs() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///alias_test.md").unwrap();
let doc_text = "no-missing-space-atx is the alias for MD018.\n".to_string();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: doc_text,
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.hover(HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 0,
character: 0,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(
result.is_some(),
"Hovering over a rule alias should return documentation"
);
let hover = result.unwrap();
match hover.contents {
HoverContents::Markup(markup) => {
assert_eq!(markup.kind, MarkupKind::Markdown);
assert!(
markup.value.contains("MD018") || markup.value.contains("no-missing-space-atx"),
"Hover content should reference the rule. Got: {}",
markup.value
);
}
_ => panic!("Expected MarkupContent"),
}
}
#[tokio::test]
async fn test_capabilities_include_formatting() {
let server = create_test_server().await;
let result = server
.initialize(InitializeParams::default())
.await
.unwrap();
assert!(
result.capabilities.document_formatting_provider.is_some(),
"Server should advertise documentFormatting capability"
);
}
#[tokio::test]
async fn test_formatting_applies_fixes() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Title\n\nTrailing spaces \n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.formatting(DocumentFormattingParams {
text_document: TextDocumentIdentifier { uri },
options: FormattingOptions {
tab_size: 4,
insert_spaces: true,
..Default::default()
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(result.is_some(), "Should return formatting edits");
let edits = result.unwrap();
assert_eq!(edits.len(), 1, "Should have one text edit");
assert!(
!edits[0].new_text.contains("spaces \n"),
"Trailing spaces should be removed"
);
}
#[tokio::test]
async fn test_formatting_returns_none_for_clean_document() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Clean Document\n\nNo issues here.\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.formatting(DocumentFormattingParams {
text_document: TextDocumentIdentifier { uri },
options: FormattingOptions {
tab_size: 4,
insert_spaces: true,
..Default::default()
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(
result.is_none(),
"Clean document should not return formatting edits"
);
}
#[tokio::test]
async fn test_capabilities_include_folding_range() {
let server = create_test_server().await;
let result = server
.initialize(InitializeParams::default())
.await
.unwrap();
assert!(
result.capabilities.folding_range_provider.is_some(),
"Server should advertise foldingRange capability"
);
}
#[tokio::test]
async fn test_folding_range_headings() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Title\n\nSome text.\n\n## Section A\n\nContent A.\n\n## Section B\n\nContent B.\n"
.to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let result = server
.folding_range(FoldingRangeParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
assert!(result.is_some(), "Should return folding ranges");
let ranges = result.unwrap();
assert!(
ranges.len() >= 3,
"Expected at least 3 folding ranges, got {}",
ranges.len()
);
assert!(
ranges.iter().any(|r| r.start_line == 0),
"Should have a folding range starting at line 0 (h1)"
);
}
#[tokio::test]
async fn test_folding_range_code_blocks() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Title\n\n```rust\nfn main() {}\n```\n\nMore text.\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let result = server
.folding_range(FoldingRangeParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
assert!(result.is_some());
let ranges = result.unwrap();
let code_range = ranges
.iter()
.find(|r| r.start_line == 2)
.expect("Should have a folding range for the code block");
assert_eq!(code_range.end_line, 4);
}
#[tokio::test]
async fn test_folding_range_empty_document() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "Just a line.\n".to_string(),
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let result = server
.folding_range(FoldingRangeParams {
text_document: TextDocumentIdentifier { uri },
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
assert!(
result.is_none(),
"Document without headings or code blocks should have no folding ranges"
);
}
#[tokio::test]
async fn test_hover_on_rule_name_without_diagnostic_shows_docs() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///name_test.md").unwrap();
let doc_text = "MD009 is the trailing-spaces rule.\n".to_string();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: doc_text,
},
})
.await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let result = server
.hover(HoverParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 0,
character: 0,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(
result.is_some(),
"Hovering over a rule ID in text should return documentation"
);
let hover = result.unwrap();
match hover.contents {
HoverContents::Markup(markup) => {
assert_eq!(markup.kind, MarkupKind::Markdown);
assert!(
markup.value.contains("MD009"),
"Hover content should reference MD009. Got: {}",
markup.value
);
}
_ => panic!("Expected MarkupContent"),
}
}
#[tokio::test]
async fn test_capabilities_include_rename() {
let server = create_test_server().await;
let result = server
.initialize(InitializeParams::default())
.await
.unwrap();
assert!(
result.capabilities.rename_provider.is_some(),
"rename_provider capability should be declared"
);
}
#[tokio::test]
async fn test_prepare_rename_on_heading() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# My Heading\n\nSome text.\n".to_string(),
},
})
.await;
let result = server
.prepare_rename(TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 0,
character: 5,
},
})
.await
.unwrap();
assert!(
result.is_some(),
"prepare_rename on a heading should return a range"
);
}
#[tokio::test]
async fn test_prepare_rename_on_non_heading_returns_error() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Heading\n\nBody text here.\n".to_string(),
},
})
.await;
let result = server
.prepare_rename(TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 0,
},
})
.await;
assert!(
result.is_err(),
"prepare_rename on body text should return an error"
);
}
#[tokio::test]
async fn test_rename_heading_updates_anchor_links() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
let content =
"## My Heading\n\nSee [link](#my-heading) and [other](#my-heading).\n".to_string();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: content,
},
})
.await;
let result = server
.rename(RenameParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 0,
character: 5,
},
},
new_name: "New Name".to_string(),
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(result.is_some(), "rename should return a WorkspaceEdit");
let edit = result.unwrap();
let changes = edit.changes.expect("changes should be present");
let edits = changes.get(&uri).expect("edits for the file");
assert!(
edits.len() >= 3,
"Expected heading + 2 link edits, got {}",
edits.len()
);
assert_eq!(edits[0].new_text, "## New Name");
for edit in &edits[1..] {
assert_eq!(edit.new_text, "new-name", "Link anchors should be updated");
}
}
#[tokio::test]
async fn test_rename_heading_no_links() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "## Old Title\n\nNo links here.\n".to_string(),
},
})
.await;
let result = server
.rename(RenameParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 0,
character: 5,
},
},
new_name: "New Title".to_string(),
work_done_progress_params: WorkDoneProgressParams::default(),
})
.await
.unwrap();
assert!(result.is_some(), "rename should return a WorkspaceEdit");
let edit = result.unwrap();
let changes = edit.changes.expect("changes should be present");
let edits = changes.get(&uri).expect("edits for the file");
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].new_text, "## New Title");
}
#[tokio::test]
async fn test_completion_heading_anchors() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Introduction\n\n## Getting Started\n\n## API Reference\n\nSee [link](#\n"
.to_string(),
},
})
.await;
let result = server
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: 12, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some(), "completion should return items");
let items = match result.unwrap() {
CompletionResponse::Array(items) => items,
CompletionResponse::List(list) => list.items,
};
assert!(
!items.is_empty(),
"Should return heading anchor completions"
);
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.contains(&"introduction"),
"Should include 'introduction' anchor. Got: {:?}",
labels
);
assert!(
labels.contains(&"getting-started"),
"Should include 'getting-started' anchor. Got: {:?}",
labels
);
assert!(
labels.contains(&"api-reference"),
"Should include 'api-reference' anchor. Got: {:?}",
labels
);
}
#[tokio::test]
async fn test_completion_heading_anchor_prefix_filter() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text:
"# Alpha Section\n\n## Beta Section\n\n## Alpha Details\n\nSee [link](#alpha\n"
.to_string(),
},
})
.await;
let result = server
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 6,
character: 17, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();
assert!(result.is_some());
let items = match result.unwrap() {
CompletionResponse::Array(items) => items,
CompletionResponse::List(list) => list.items,
};
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
assert!(
labels.iter().all(|l| l.starts_with("alpha")),
"All completions should start with 'alpha', got: {:?}",
labels
);
assert!(
!labels.contains(&"beta-section"),
"beta-section should be filtered out"
);
}
#[tokio::test]
async fn test_capabilities_include_references() {
let server = create_test_server().await;
let result = server
.initialize(InitializeParams::default())
.await
.unwrap();
assert!(
result.capabilities.references_provider.is_some(),
"references_provider capability should be declared"
);
}
#[tokio::test]
async fn test_references_from_heading() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "## My Heading\n\nSee [link](#my-heading) and [other](#my-heading).\n"
.to_string(),
},
})
.await;
let result = server
.references(ReferenceParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 0,
character: 5,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: ReferenceContext {
include_declaration: false,
},
})
.await
.unwrap();
assert!(
result.is_some(),
"references from heading should return locations"
);
let locations = result.unwrap();
assert_eq!(
locations.len(),
2,
"Should find 2 references to #my-heading"
);
for loc in &locations {
assert_eq!(loc.uri, uri, "All refs should be in the same document");
}
}
#[tokio::test]
async fn test_references_from_anchor_link() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "## My Heading\n\nSee [link](#my-heading) and [x](#my-heading).\n"
.to_string(),
},
})
.await;
let result = server
.references(ReferenceParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 13, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: ReferenceContext {
include_declaration: false,
},
})
.await
.unwrap();
assert!(
result.is_some(),
"references from anchor link should return locations"
);
let locations = result.unwrap();
assert_eq!(locations.len(), 2, "Should find 2 references");
}
#[tokio::test]
async fn test_references_returns_none_on_body_text() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Heading\n\nPlain body text here.\n".to_string(),
},
})
.await;
let result = server
.references(ReferenceParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 5,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: ReferenceContext {
include_declaration: false,
},
})
.await
.unwrap();
assert!(
result.is_none(),
"references on plain body text should return None"
);
}
#[tokio::test]
async fn test_capabilities_include_definition() {
let server = create_test_server().await;
let result = server
.initialize(InitializeParams::default())
.await
.unwrap();
assert!(
result.capabilities.definition_provider.is_some(),
"definition_provider capability should be declared"
);
}
#[tokio::test]
async fn test_goto_definition_from_anchor_link() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "## My Heading\n\nSee [link](#my-heading).\n".to_string(),
},
})
.await;
let result = server
.goto_definition(GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 13, },
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
assert!(
result.is_some(),
"goto_definition from anchor link should return a location"
);
let response = result.unwrap();
let location = match response {
GotoDefinitionResponse::Scalar(loc) => loc,
GotoDefinitionResponse::Array(locs) => locs.into_iter().next().expect("at least one"),
GotoDefinitionResponse::Link(links) => {
panic!("Expected Scalar, got Link: {:?}", links)
}
};
assert_eq!(location.uri, uri, "Definition should be in the same file");
assert_eq!(
location.range.start.line, 0,
"Heading is on line 0 (## My Heading)"
);
}
#[tokio::test]
async fn test_goto_definition_returns_none_on_body_text() {
let server = create_test_server().await;
server
.initialize(InitializeParams::default())
.await
.unwrap();
server.initialized(InitializedParams {}).await;
let uri = Url::parse("file:///test.md").unwrap();
server
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "markdown".to_string(),
version: 1,
text: "# Heading\n\nPlain body text.\n".to_string(),
},
})
.await;
let result = server
.goto_definition(GotoDefinitionParams {
text_document_position_params: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri: uri.clone() },
position: Position {
line: 2,
character: 3,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
})
.await
.unwrap();
assert!(
result.is_none(),
"goto_definition on plain text should return None"
);
}