1use std::collections::HashMap;
7use std::sync::Arc;
8
9use anyhow::Result;
10use tokio::sync::RwLock;
11use tower_lsp::jsonrpc::Result as JsonRpcResult;
12use tower_lsp::lsp_types::*;
13use tower_lsp::{Client, LanguageServer};
14
15use crate::config::Config;
16use crate::lsp::types::{RumdlLspConfig, warning_to_code_action, warning_to_diagnostic};
17use crate::rules;
18
19#[derive(Clone)]
27pub struct RumdlLanguageServer {
28 client: Client,
29 config: Arc<RwLock<RumdlLspConfig>>,
31 rumdl_config: Arc<RwLock<Config>>,
33 documents: Arc<RwLock<HashMap<Url, String>>>,
35}
36
37impl RumdlLanguageServer {
38 pub fn new(client: Client) -> Self {
39 Self {
40 client,
41 config: Arc::new(RwLock::new(RumdlLspConfig::default())),
42 rumdl_config: Arc::new(RwLock::new(Config::default())),
43 documents: Arc::new(RwLock::new(HashMap::new())),
44 }
45 }
46
47 async fn lint_document(&self, uri: &Url, text: &str) -> Result<Vec<Diagnostic>> {
49 let config_guard = self.config.read().await;
50
51 if !config_guard.enable_linting {
53 return Ok(Vec::new());
54 }
55
56 drop(config_guard); let rumdl_config = self.rumdl_config.read().await;
60 let all_rules = rules::all_rules(&rumdl_config);
61 drop(rumdl_config); match crate::lint(text, &all_rules, false, crate::config::MarkdownFlavor::Standard) {
65 Ok(warnings) => {
66 let diagnostics = warnings.iter().map(warning_to_diagnostic).collect();
67 Ok(diagnostics)
68 }
69 Err(e) => {
70 log::error!("Failed to lint document {uri}: {e}");
71 Ok(Vec::new())
72 }
73 }
74 }
75
76 async fn update_diagnostics(&self, uri: Url, text: String) {
78 match self.lint_document(&uri, &text).await {
79 Ok(diagnostics) => {
80 self.client.publish_diagnostics(uri, diagnostics, None).await;
81 }
82 Err(e) => {
83 log::error!("Failed to update diagnostics: {e}");
84 }
85 }
86 }
87
88 async fn apply_all_fixes(&self, _uri: &Url, text: &str) -> Result<Option<String>> {
90 let rumdl_config = self.rumdl_config.read().await;
91 let all_rules = rules::all_rules(&rumdl_config);
92 drop(rumdl_config);
93
94 let mut fixed_text = text.to_string();
96 let mut any_changes = false;
97
98 for rule in &all_rules {
99 let ctx = crate::lint_context::LintContext::new(&fixed_text, crate::config::MarkdownFlavor::Standard);
100 match rule.fix(&ctx) {
101 Ok(new_text) => {
102 if new_text != fixed_text {
103 fixed_text = new_text;
104 any_changes = true;
105 }
106 }
107 Err(e) => {
108 log::warn!("Failed to apply fix for rule {}: {}", rule.name(), e);
109 }
110 }
111 }
112
113 if any_changes { Ok(Some(fixed_text)) } else { Ok(None) }
114 }
115
116 fn get_end_position(&self, text: &str) -> Position {
118 let lines: Vec<&str> = text.lines().collect();
119 let line = lines.len().saturating_sub(1) as u32;
120 let character = lines.last().map_or(0, |l| l.len() as u32);
121 Position { line, character }
122 }
123
124 async fn get_code_actions(&self, uri: &Url, text: &str, range: Range) -> Result<Vec<CodeAction>> {
126 let rumdl_config = self.rumdl_config.read().await;
127 let all_rules = rules::all_rules(&rumdl_config);
128 drop(rumdl_config);
129
130 match crate::lint(text, &all_rules, false, crate::config::MarkdownFlavor::Standard) {
131 Ok(warnings) => {
132 let mut actions = Vec::new();
133
134 for warning in warnings {
135 let warning_line = (warning.line.saturating_sub(1)) as u32;
137 if warning_line >= range.start.line
138 && warning_line <= range.end.line
139 && let Some(action) = warning_to_code_action(&warning, uri, text)
140 {
141 actions.push(action);
142 }
143 }
144
145 Ok(actions)
146 }
147 Err(e) => {
148 log::error!("Failed to get code actions: {e}");
149 Ok(Vec::new())
150 }
151 }
152 }
153
154 async fn load_configuration(&self, notify_client: bool) {
156 let config_guard = self.config.read().await;
157 let explicit_config_path = config_guard.config_path.clone();
158 drop(config_guard);
159
160 match Self::load_config_for_lsp(explicit_config_path.as_deref()) {
162 Ok(sourced_config) => {
163 let loaded_files = sourced_config.loaded_files.clone();
164 *self.rumdl_config.write().await = sourced_config.into();
165
166 if !loaded_files.is_empty() {
167 let message = format!("Loaded rumdl config from: {}", loaded_files.join(", "));
168 log::info!("{message}");
169 if notify_client {
170 self.client.log_message(MessageType::INFO, &message).await;
171 }
172 } else {
173 log::info!("Using default rumdl configuration (no config files found)");
174 }
175 }
176 Err(e) => {
177 let message = format!("Failed to load rumdl config: {e}");
178 log::warn!("{message}");
179 if notify_client {
180 self.client.log_message(MessageType::WARNING, &message).await;
181 }
182 *self.rumdl_config.write().await = crate::config::Config::default();
184 }
185 }
186 }
187
188 async fn reload_configuration(&self) {
190 self.load_configuration(true).await;
191 }
192
193 fn load_config_for_lsp(
195 config_path: Option<&str>,
196 ) -> Result<crate::config::SourcedConfig, crate::config::ConfigError> {
197 crate::config::SourcedConfig::load_with_discovery(config_path, None, false)
199 }
200}
201
202#[tower_lsp::async_trait]
203impl LanguageServer for RumdlLanguageServer {
204 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
205 log::info!("Initializing rumdl Language Server");
206
207 if let Some(options) = params.initialization_options
209 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
210 {
211 *self.config.write().await = config;
212 }
213
214 self.load_configuration(false).await;
216
217 Ok(InitializeResult {
218 capabilities: ServerCapabilities {
219 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
220 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
221 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
222 identifier: Some("rumdl".to_string()),
223 inter_file_dependencies: false,
224 workspace_diagnostics: false,
225 work_done_progress_options: WorkDoneProgressOptions::default(),
226 })),
227 workspace: Some(WorkspaceServerCapabilities {
228 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
229 supported: Some(true),
230 change_notifications: Some(OneOf::Left(true)),
231 }),
232 file_operations: None,
233 }),
234 ..Default::default()
235 },
236 server_info: Some(ServerInfo {
237 name: "rumdl".to_string(),
238 version: Some(env!("CARGO_PKG_VERSION").to_string()),
239 }),
240 })
241 }
242
243 async fn initialized(&self, _: InitializedParams) {
244 log::info!("rumdl Language Server initialized");
245
246 self.client
247 .log_message(MessageType::INFO, "rumdl Language Server started")
248 .await;
249 }
250
251 async fn did_change_workspace_folders(&self, _params: DidChangeWorkspaceFoldersParams) {
252 self.reload_configuration().await;
254 }
255
256 async fn shutdown(&self) -> JsonRpcResult<()> {
257 log::info!("Shutting down rumdl Language Server");
258 Ok(())
259 }
260
261 async fn did_open(&self, params: DidOpenTextDocumentParams) {
262 let uri = params.text_document.uri;
263 let text = params.text_document.text;
264
265 self.documents.write().await.insert(uri.clone(), text.clone());
267
268 self.update_diagnostics(uri, text).await;
270 }
271
272 async fn did_change(&self, params: DidChangeTextDocumentParams) {
273 let uri = params.text_document.uri;
274
275 if let Some(change) = params.content_changes.into_iter().next() {
277 let text = change.text;
278
279 self.documents.write().await.insert(uri.clone(), text.clone());
281
282 self.update_diagnostics(uri, text).await;
284 }
285 }
286
287 async fn did_save(&self, params: DidSaveTextDocumentParams) {
288 let config_guard = self.config.read().await;
289 let enable_auto_fix = config_guard.enable_auto_fix;
290 drop(config_guard);
291
292 if enable_auto_fix && let Some(text) = self.documents.read().await.get(¶ms.text_document.uri) {
294 match self.apply_all_fixes(¶ms.text_document.uri, text).await {
295 Ok(Some(fixed_text)) => {
296 let edit = TextEdit {
298 range: Range {
299 start: Position { line: 0, character: 0 },
300 end: self.get_end_position(text),
301 },
302 new_text: fixed_text.clone(),
303 };
304
305 let mut changes = std::collections::HashMap::new();
306 changes.insert(params.text_document.uri.clone(), vec![edit]);
307
308 let workspace_edit = WorkspaceEdit {
309 changes: Some(changes),
310 document_changes: None,
311 change_annotations: None,
312 };
313
314 match self.client.apply_edit(workspace_edit).await {
316 Ok(response) => {
317 if response.applied {
318 log::info!("Auto-fix applied successfully");
319 self.documents
321 .write()
322 .await
323 .insert(params.text_document.uri.clone(), fixed_text);
324 } else {
325 log::warn!("Auto-fix was not applied: {:?}", response.failure_reason);
326 }
327 }
328 Err(e) => {
329 log::error!("Failed to apply auto-fix: {e}");
330 }
331 }
332 }
333 Ok(None) => {
334 log::debug!("No fixes to apply");
335 }
336 Err(e) => {
337 log::error!("Failed to generate fixes: {e}");
338 }
339 }
340 }
341
342 if let Some(text) = self.documents.read().await.get(¶ms.text_document.uri) {
344 self.update_diagnostics(params.text_document.uri, text.clone()).await;
345 }
346 }
347
348 async fn did_close(&self, params: DidCloseTextDocumentParams) {
349 self.documents.write().await.remove(¶ms.text_document.uri);
351
352 self.client
354 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
355 .await;
356 }
357
358 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
359 let uri = params.text_document.uri;
360 let range = params.range;
361
362 if let Some(text) = self.documents.read().await.get(&uri) {
363 match self.get_code_actions(&uri, text, range).await {
364 Ok(actions) => {
365 let response: Vec<CodeActionOrCommand> =
366 actions.into_iter().map(CodeActionOrCommand::CodeAction).collect();
367 Ok(Some(response))
368 }
369 Err(e) => {
370 log::error!("Failed to get code actions: {e}");
371 Ok(None)
372 }
373 }
374 } else {
375 Ok(None)
376 }
377 }
378
379 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
380 let uri = params.text_document.uri;
381
382 if let Some(text) = self.documents.read().await.get(&uri) {
383 match self.lint_document(&uri, text).await {
384 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
385 RelatedFullDocumentDiagnosticReport {
386 related_documents: None,
387 full_document_diagnostic_report: FullDocumentDiagnosticReport {
388 result_id: None,
389 items: diagnostics,
390 },
391 },
392 ))),
393 Err(e) => {
394 log::error!("Failed to get diagnostics: {e}");
395 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
396 RelatedFullDocumentDiagnosticReport {
397 related_documents: None,
398 full_document_diagnostic_report: FullDocumentDiagnosticReport {
399 result_id: None,
400 items: Vec::new(),
401 },
402 },
403 )))
404 }
405 }
406 } else {
407 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
408 RelatedFullDocumentDiagnosticReport {
409 related_documents: None,
410 full_document_diagnostic_report: FullDocumentDiagnosticReport {
411 result_id: None,
412 items: Vec::new(),
413 },
414 },
415 )))
416 }
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::rule::LintWarning;
424 use tower_lsp::LspService;
425
426 fn create_test_server() -> RumdlLanguageServer {
427 let (service, _socket) = LspService::new(RumdlLanguageServer::new);
428 service.inner().clone()
429 }
430
431 #[tokio::test]
432 async fn test_server_creation() {
433 let server = create_test_server();
434
435 let config = server.config.read().await;
437 assert!(config.enable_linting);
438 assert!(!config.enable_auto_fix);
439 }
440
441 #[tokio::test]
442 async fn test_lint_document() {
443 let server = create_test_server();
444
445 let uri = Url::parse("file:///test.md").unwrap();
447 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
448
449 let diagnostics = server.lint_document(&uri, text).await.unwrap();
450
451 assert!(!diagnostics.is_empty());
453 assert!(diagnostics.iter().any(|d| d.message.contains("trailing")));
454 }
455
456 #[tokio::test]
457 async fn test_lint_document_disabled() {
458 let server = create_test_server();
459
460 server.config.write().await.enable_linting = false;
462
463 let uri = Url::parse("file:///test.md").unwrap();
464 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
465
466 let diagnostics = server.lint_document(&uri, text).await.unwrap();
467
468 assert!(diagnostics.is_empty());
470 }
471
472 #[tokio::test]
473 async fn test_get_code_actions() {
474 let server = create_test_server();
475
476 let uri = Url::parse("file:///test.md").unwrap();
477 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
478
479 let range = Range {
481 start: Position { line: 0, character: 0 },
482 end: Position { line: 3, character: 21 },
483 };
484
485 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
486
487 assert!(!actions.is_empty());
489 assert!(actions.iter().any(|a| a.title.contains("trailing")));
490 }
491
492 #[tokio::test]
493 async fn test_get_code_actions_outside_range() {
494 let server = create_test_server();
495
496 let uri = Url::parse("file:///test.md").unwrap();
497 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
498
499 let range = Range {
501 start: Position { line: 0, character: 0 },
502 end: Position { line: 0, character: 6 },
503 };
504
505 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
506
507 assert!(actions.is_empty());
509 }
510
511 #[tokio::test]
512 async fn test_document_storage() {
513 let server = create_test_server();
514
515 let uri = Url::parse("file:///test.md").unwrap();
516 let text = "# Test Document";
517
518 server.documents.write().await.insert(uri.clone(), text.to_string());
520
521 let stored = server.documents.read().await.get(&uri).cloned();
523 assert_eq!(stored, Some(text.to_string()));
524
525 server.documents.write().await.remove(&uri);
527
528 let stored = server.documents.read().await.get(&uri).cloned();
530 assert_eq!(stored, None);
531 }
532
533 #[tokio::test]
534 async fn test_configuration_loading() {
535 let server = create_test_server();
536
537 server.load_configuration(false).await;
539
540 let rumdl_config = server.rumdl_config.read().await;
543 drop(rumdl_config); }
546
547 #[tokio::test]
548 async fn test_load_config_for_lsp() {
549 let result = RumdlLanguageServer::load_config_for_lsp(None);
551 assert!(result.is_ok());
552
553 let result = RumdlLanguageServer::load_config_for_lsp(Some("/nonexistent/config.toml"));
555 assert!(result.is_err());
556 }
557
558 #[tokio::test]
559 async fn test_warning_conversion() {
560 let warning = LintWarning {
561 message: "Test warning".to_string(),
562 line: 1,
563 column: 1,
564 end_line: 1,
565 end_column: 10,
566 severity: crate::rule::Severity::Warning,
567 fix: None,
568 rule_name: Some("MD001"),
569 };
570
571 let diagnostic = warning_to_diagnostic(&warning);
573 assert_eq!(diagnostic.message, "Test warning");
574 assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::WARNING));
575 assert_eq!(diagnostic.code, Some(NumberOrString::String("MD001".to_string())));
576
577 let uri = Url::parse("file:///test.md").unwrap();
579 let action = warning_to_code_action(&warning, &uri, "Test content");
580 assert!(action.is_none());
581 }
582
583 #[tokio::test]
584 async fn test_multiple_documents() {
585 let server = create_test_server();
586
587 let uri1 = Url::parse("file:///test1.md").unwrap();
588 let uri2 = Url::parse("file:///test2.md").unwrap();
589 let text1 = "# Document 1";
590 let text2 = "# Document 2";
591
592 {
594 let mut docs = server.documents.write().await;
595 docs.insert(uri1.clone(), text1.to_string());
596 docs.insert(uri2.clone(), text2.to_string());
597 }
598
599 let docs = server.documents.read().await;
601 assert_eq!(docs.len(), 2);
602 assert_eq!(docs.get(&uri1).map(|s| s.as_str()), Some(text1));
603 assert_eq!(docs.get(&uri2).map(|s| s.as_str()), Some(text2));
604 }
605
606 #[tokio::test]
607 async fn test_auto_fix_on_save() {
608 let server = create_test_server();
609
610 {
612 let mut config = server.config.write().await;
613 config.enable_auto_fix = true;
614 }
615
616 let uri = Url::parse("file:///test.md").unwrap();
617 let text = "#Heading without space"; server.documents.write().await.insert(uri.clone(), text.to_string());
621
622 let fixed = server.apply_all_fixes(&uri, text).await.unwrap();
624 assert!(fixed.is_some());
625 assert_eq!(fixed.unwrap(), "# Heading without space");
626 }
627
628 #[tokio::test]
629 async fn test_get_end_position() {
630 let server = create_test_server();
631
632 let pos = server.get_end_position("Hello");
634 assert_eq!(pos.line, 0);
635 assert_eq!(pos.character, 5);
636
637 let pos = server.get_end_position("Hello\nWorld\nTest");
639 assert_eq!(pos.line, 2);
640 assert_eq!(pos.character, 4);
641
642 let pos = server.get_end_position("");
644 assert_eq!(pos.line, 0);
645 assert_eq!(pos.character, 0);
646
647 let pos = server.get_end_position("Hello\n");
649 assert_eq!(pos.line, 0);
650 assert_eq!(pos.character, 5);
651 }
652
653 #[tokio::test]
654 async fn test_empty_document_handling() {
655 let server = create_test_server();
656
657 let uri = Url::parse("file:///empty.md").unwrap();
658 let text = "";
659
660 let diagnostics = server.lint_document(&uri, text).await.unwrap();
662 assert!(diagnostics.is_empty());
663
664 let range = Range {
666 start: Position { line: 0, character: 0 },
667 end: Position { line: 0, character: 0 },
668 };
669 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
670 assert!(actions.is_empty());
671 }
672
673 #[tokio::test]
674 async fn test_config_update() {
675 let server = create_test_server();
676
677 {
679 let mut config = server.config.write().await;
680 config.enable_auto_fix = true;
681 config.config_path = Some("/custom/path.toml".to_string());
682 }
683
684 let config = server.config.read().await;
686 assert!(config.enable_auto_fix);
687 assert_eq!(config.config_path, Some("/custom/path.toml".to_string()));
688 }
689}