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::rule::Rule;
18use crate::rules;
19
20#[derive(Clone, Debug, PartialEq)]
22struct DocumentEntry {
23 content: String,
25 version: Option<i32>,
27 from_disk: bool,
29}
30
31#[derive(Clone)]
39pub struct RumdlLanguageServer {
40 client: Client,
41 config: Arc<RwLock<RumdlLspConfig>>,
43 rumdl_config: Arc<RwLock<Config>>,
45 documents: Arc<RwLock<HashMap<Url, DocumentEntry>>>,
47}
48
49impl RumdlLanguageServer {
50 pub fn new(client: Client) -> Self {
51 Self {
52 client,
53 config: Arc::new(RwLock::new(RumdlLspConfig::default())),
54 rumdl_config: Arc::new(RwLock::new(Config::default())),
55 documents: Arc::new(RwLock::new(HashMap::new())),
56 }
57 }
58
59 async fn get_document_content(&self, uri: &Url) -> Option<String> {
65 {
67 let docs = self.documents.read().await;
68 if let Some(entry) = docs.get(uri) {
69 return Some(entry.content.clone());
70 }
71 }
72
73 if let Ok(path) = uri.to_file_path() {
75 if let Ok(content) = tokio::fs::read_to_string(&path).await {
76 let entry = DocumentEntry {
78 content: content.clone(),
79 version: None,
80 from_disk: true,
81 };
82
83 let mut docs = self.documents.write().await;
84 docs.insert(uri.clone(), entry);
85
86 log::debug!("Loaded document from disk and cached: {uri}");
87 return Some(content);
88 } else {
89 log::debug!("Failed to read file from disk: {uri}");
90 }
91 }
92
93 None
94 }
95
96 fn apply_lsp_config_overrides(
98 &self,
99 mut filtered_rules: Vec<Box<dyn Rule>>,
100 lsp_config: &RumdlLspConfig,
101 ) -> Vec<Box<dyn Rule>> {
102 if let Some(enable) = &lsp_config.enable_rules
104 && !enable.is_empty()
105 {
106 let enable_set: std::collections::HashSet<String> = enable.iter().cloned().collect();
107 filtered_rules.retain(|rule| enable_set.contains(rule.name()));
108 }
109
110 if let Some(disable) = &lsp_config.disable_rules
112 && !disable.is_empty()
113 {
114 let disable_set: std::collections::HashSet<String> = disable.iter().cloned().collect();
115 filtered_rules.retain(|rule| !disable_set.contains(rule.name()));
116 }
117
118 filtered_rules
119 }
120
121 async fn lint_document(&self, uri: &Url, text: &str) -> Result<Vec<Diagnostic>> {
123 let config_guard = self.config.read().await;
124
125 if !config_guard.enable_linting {
127 return Ok(Vec::new());
128 }
129
130 let lsp_config = config_guard.clone();
131 drop(config_guard); let rumdl_config = self.rumdl_config.read().await;
135 let all_rules = rules::all_rules(&rumdl_config);
136 let flavor = rumdl_config.markdown_flavor();
137
138 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
140 drop(rumdl_config); filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
144
145 match crate::lint(text, &filtered_rules, false, flavor) {
147 Ok(warnings) => {
148 let diagnostics = warnings.iter().map(warning_to_diagnostic).collect();
149 Ok(diagnostics)
150 }
151 Err(e) => {
152 log::error!("Failed to lint document {uri}: {e}");
153 Ok(Vec::new())
154 }
155 }
156 }
157
158 async fn update_diagnostics(&self, uri: Url, text: String) {
160 match self.lint_document(&uri, &text).await {
161 Ok(diagnostics) => {
162 self.client.publish_diagnostics(uri, diagnostics, None).await;
163 }
164 Err(e) => {
165 log::error!("Failed to update diagnostics: {e}");
166 }
167 }
168 }
169
170 async fn apply_all_fixes(&self, _uri: &Url, text: &str) -> Result<Option<String>> {
172 let config_guard = self.config.read().await;
173 let lsp_config = config_guard.clone();
174 drop(config_guard);
175
176 let rumdl_config = self.rumdl_config.read().await;
177 let all_rules = rules::all_rules(&rumdl_config);
178 let flavor = rumdl_config.markdown_flavor();
179
180 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
182 drop(rumdl_config);
183
184 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
186
187 let mut fixed_text = text.to_string();
189 let mut any_changes = false;
190
191 for rule in &filtered_rules {
192 let ctx = crate::lint_context::LintContext::new(&fixed_text, flavor);
193 match rule.fix(&ctx) {
194 Ok(new_text) => {
195 if new_text != fixed_text {
196 fixed_text = new_text;
197 any_changes = true;
198 }
199 }
200 Err(e) => {
201 log::warn!("Failed to apply fix for rule {}: {}", rule.name(), e);
202 }
203 }
204 }
205
206 if any_changes { Ok(Some(fixed_text)) } else { Ok(None) }
207 }
208
209 fn get_end_position(&self, text: &str) -> Position {
211 let mut line = 0u32;
212 let mut character = 0u32;
213
214 for ch in text.chars() {
215 if ch == '\n' {
216 line += 1;
217 character = 0;
218 } else {
219 character += 1;
220 }
221 }
222
223 Position { line, character }
224 }
225
226 async fn get_code_actions(&self, uri: &Url, text: &str, range: Range) -> Result<Vec<CodeAction>> {
228 let config_guard = self.config.read().await;
229 let lsp_config = config_guard.clone();
230 drop(config_guard);
231
232 let rumdl_config = self.rumdl_config.read().await;
233 let all_rules = rules::all_rules(&rumdl_config);
234 let flavor = rumdl_config.markdown_flavor();
235
236 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
238 drop(rumdl_config);
239
240 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
242
243 match crate::lint(text, &filtered_rules, false, flavor) {
244 Ok(warnings) => {
245 let mut actions = Vec::new();
246 let mut fixable_count = 0;
247
248 for warning in &warnings {
249 let warning_line = (warning.line.saturating_sub(1)) as u32;
251 if warning_line >= range.start.line
252 && warning_line <= range.end.line
253 && let Some(action) = warning_to_code_action(warning, uri, text)
254 {
255 actions.push(action);
256 if warning.fix.is_some() {
257 fixable_count += 1;
258 }
259 }
260 }
261
262 if fixable_count > 1 {
264 let total_fixable = warnings.iter().filter(|w| w.fix.is_some()).count();
266
267 if let Ok(fixed_content) = crate::utils::fix_utils::apply_warning_fixes(text, &warnings)
268 && fixed_content != text
269 {
270 let mut line = 0u32;
272 let mut character = 0u32;
273 for ch in text.chars() {
274 if ch == '\n' {
275 line += 1;
276 character = 0;
277 } else {
278 character += 1;
279 }
280 }
281
282 let fix_all_action = CodeAction {
283 title: format!("Fix all rumdl issues ({total_fixable} fixable)"),
284 kind: Some(CodeActionKind::QUICKFIX),
285 diagnostics: Some(Vec::new()),
286 edit: Some(WorkspaceEdit {
287 changes: Some(
288 [(
289 uri.clone(),
290 vec![TextEdit {
291 range: Range {
292 start: Position { line: 0, character: 0 },
293 end: Position { line, character },
294 },
295 new_text: fixed_content,
296 }],
297 )]
298 .into_iter()
299 .collect(),
300 ),
301 ..Default::default()
302 }),
303 command: None,
304 is_preferred: Some(true),
305 disabled: None,
306 data: None,
307 };
308
309 actions.insert(0, fix_all_action);
311 }
312 }
313
314 Ok(actions)
315 }
316 Err(e) => {
317 log::error!("Failed to get code actions: {e}");
318 Ok(Vec::new())
319 }
320 }
321 }
322
323 async fn load_configuration(&self, notify_client: bool) {
325 let config_guard = self.config.read().await;
326 let explicit_config_path = config_guard.config_path.clone();
327 drop(config_guard);
328
329 match Self::load_config_for_lsp(explicit_config_path.as_deref()) {
331 Ok(sourced_config) => {
332 let loaded_files = sourced_config.loaded_files.clone();
333 *self.rumdl_config.write().await = sourced_config.into();
334
335 if !loaded_files.is_empty() {
336 let message = format!("Loaded rumdl config from: {}", loaded_files.join(", "));
337 log::info!("{message}");
338 if notify_client {
339 self.client.log_message(MessageType::INFO, &message).await;
340 }
341 } else {
342 log::info!("Using default rumdl configuration (no config files found)");
343 }
344 }
345 Err(e) => {
346 let message = format!("Failed to load rumdl config: {e}");
347 log::warn!("{message}");
348 if notify_client {
349 self.client.log_message(MessageType::WARNING, &message).await;
350 }
351 *self.rumdl_config.write().await = crate::config::Config::default();
353 }
354 }
355 }
356
357 async fn reload_configuration(&self) {
359 self.load_configuration(true).await;
360 }
361
362 fn load_config_for_lsp(
364 config_path: Option<&str>,
365 ) -> Result<crate::config::SourcedConfig, crate::config::ConfigError> {
366 crate::config::SourcedConfig::load_with_discovery(config_path, None, false)
368 }
369}
370
371#[tower_lsp::async_trait]
372impl LanguageServer for RumdlLanguageServer {
373 async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
374 log::info!("Initializing rumdl Language Server");
375
376 if let Some(options) = params.initialization_options
378 && let Ok(config) = serde_json::from_value::<RumdlLspConfig>(options)
379 {
380 *self.config.write().await = config;
381 }
382
383 self.load_configuration(false).await;
385
386 Ok(InitializeResult {
387 capabilities: ServerCapabilities {
388 text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
389 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
390 document_formatting_provider: Some(OneOf::Left(true)),
391 document_range_formatting_provider: Some(OneOf::Left(true)),
392 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
393 identifier: Some("rumdl".to_string()),
394 inter_file_dependencies: false,
395 workspace_diagnostics: false,
396 work_done_progress_options: WorkDoneProgressOptions::default(),
397 })),
398 workspace: Some(WorkspaceServerCapabilities {
399 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
400 supported: Some(true),
401 change_notifications: Some(OneOf::Left(true)),
402 }),
403 file_operations: None,
404 }),
405 ..Default::default()
406 },
407 server_info: Some(ServerInfo {
408 name: "rumdl".to_string(),
409 version: Some(env!("CARGO_PKG_VERSION").to_string()),
410 }),
411 })
412 }
413
414 async fn initialized(&self, _: InitializedParams) {
415 log::info!("rumdl Language Server initialized");
416
417 self.client
418 .log_message(MessageType::INFO, "rumdl Language Server started")
419 .await;
420 }
421
422 async fn did_change_workspace_folders(&self, _params: DidChangeWorkspaceFoldersParams) {
423 self.reload_configuration().await;
425 }
426
427 async fn shutdown(&self) -> JsonRpcResult<()> {
428 log::info!("Shutting down rumdl Language Server");
429 Ok(())
430 }
431
432 async fn did_open(&self, params: DidOpenTextDocumentParams) {
433 let uri = params.text_document.uri;
434 let text = params.text_document.text;
435 let version = params.text_document.version;
436
437 let entry = DocumentEntry {
439 content: text.clone(),
440 version: Some(version),
441 from_disk: false,
442 };
443 self.documents.write().await.insert(uri.clone(), entry);
444
445 self.update_diagnostics(uri, text).await;
447 }
448
449 async fn did_change(&self, params: DidChangeTextDocumentParams) {
450 let uri = params.text_document.uri;
451 let version = params.text_document.version;
452
453 if let Some(change) = params.content_changes.into_iter().next() {
455 let text = change.text;
456
457 let entry = DocumentEntry {
459 content: text.clone(),
460 version: Some(version),
461 from_disk: false,
462 };
463 self.documents.write().await.insert(uri.clone(), entry);
464
465 self.update_diagnostics(uri, text).await;
467 }
468 }
469
470 async fn did_save(&self, params: DidSaveTextDocumentParams) {
471 let config_guard = self.config.read().await;
472 let enable_auto_fix = config_guard.enable_auto_fix;
473 drop(config_guard);
474
475 if enable_auto_fix && let Some(entry) = self.documents.read().await.get(¶ms.text_document.uri) {
477 let text = &entry.content;
478 match self.apply_all_fixes(¶ms.text_document.uri, text).await {
479 Ok(Some(fixed_text)) => {
480 let edit = TextEdit {
482 range: Range {
483 start: Position { line: 0, character: 0 },
484 end: self.get_end_position(text),
485 },
486 new_text: fixed_text.clone(),
487 };
488
489 let mut changes = std::collections::HashMap::new();
490 changes.insert(params.text_document.uri.clone(), vec![edit]);
491
492 let workspace_edit = WorkspaceEdit {
493 changes: Some(changes),
494 document_changes: None,
495 change_annotations: None,
496 };
497
498 match self.client.apply_edit(workspace_edit).await {
500 Ok(response) => {
501 if response.applied {
502 log::info!("Auto-fix applied successfully");
503 let entry = DocumentEntry {
505 content: fixed_text,
506 version: None, from_disk: false,
508 };
509 self.documents
510 .write()
511 .await
512 .insert(params.text_document.uri.clone(), entry);
513 } else {
514 log::warn!("Auto-fix was not applied: {:?}", response.failure_reason);
515 }
516 }
517 Err(e) => {
518 log::error!("Failed to apply auto-fix: {e}");
519 }
520 }
521 }
522 Ok(None) => {
523 log::debug!("No fixes to apply");
524 }
525 Err(e) => {
526 log::error!("Failed to generate fixes: {e}");
527 }
528 }
529 }
530
531 if let Some(entry) = self.documents.read().await.get(¶ms.text_document.uri) {
533 self.update_diagnostics(params.text_document.uri, entry.content.clone())
534 .await;
535 }
536 }
537
538 async fn did_close(&self, params: DidCloseTextDocumentParams) {
539 self.documents.write().await.remove(¶ms.text_document.uri);
541
542 self.client
544 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
545 .await;
546 }
547
548 async fn code_action(&self, params: CodeActionParams) -> JsonRpcResult<Option<CodeActionResponse>> {
549 let uri = params.text_document.uri;
550 let range = params.range;
551
552 if let Some(text) = self.get_document_content(&uri).await {
553 match self.get_code_actions(&uri, &text, range).await {
554 Ok(actions) => {
555 let response: Vec<CodeActionOrCommand> =
556 actions.into_iter().map(CodeActionOrCommand::CodeAction).collect();
557 Ok(Some(response))
558 }
559 Err(e) => {
560 log::error!("Failed to get code actions: {e}");
561 Ok(None)
562 }
563 }
564 } else {
565 Ok(None)
566 }
567 }
568
569 async fn range_formatting(&self, params: DocumentRangeFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
570 log::debug!(
575 "Range formatting requested for {:?}, formatting entire document due to rule interdependencies",
576 params.range
577 );
578
579 let formatting_params = DocumentFormattingParams {
580 text_document: params.text_document,
581 options: params.options,
582 work_done_progress_params: params.work_done_progress_params,
583 };
584
585 self.formatting(formatting_params).await
586 }
587
588 async fn formatting(&self, params: DocumentFormattingParams) -> JsonRpcResult<Option<Vec<TextEdit>>> {
589 let uri = params.text_document.uri;
590
591 log::debug!("Formatting request for: {uri}");
592
593 if let Some(text) = self.get_document_content(&uri).await {
594 let config_guard = self.config.read().await;
596 let lsp_config = config_guard.clone();
597 drop(config_guard);
598
599 let rumdl_config = self.rumdl_config.read().await;
601 let all_rules = rules::all_rules(&rumdl_config);
602 let flavor = rumdl_config.markdown_flavor();
603
604 let mut filtered_rules = rules::filter_rules(&all_rules, &rumdl_config.global);
606
607 drop(rumdl_config);
608
609 filtered_rules = self.apply_lsp_config_overrides(filtered_rules, &lsp_config);
611
612 match crate::lint(&text, &filtered_rules, false, flavor) {
614 Ok(warnings) => {
615 log::debug!(
616 "Found {} warnings, {} with fixes",
617 warnings.len(),
618 warnings.iter().filter(|w| w.fix.is_some()).count()
619 );
620
621 let has_fixes = warnings.iter().any(|w| w.fix.is_some());
622 if has_fixes {
623 match crate::utils::fix_utils::apply_warning_fixes(&text, &warnings) {
624 Ok(fixed_content) => {
625 if fixed_content != text {
626 log::debug!("Returning formatting edits");
627 let end_position = self.get_end_position(&text);
628 let edit = TextEdit {
629 range: Range {
630 start: Position { line: 0, character: 0 },
631 end: end_position,
632 },
633 new_text: fixed_content,
634 };
635 return Ok(Some(vec![edit]));
636 }
637 }
638 Err(e) => {
639 log::error!("Failed to apply fixes: {e}");
640 }
641 }
642 }
643 Ok(Some(Vec::new()))
644 }
645 Err(e) => {
646 log::error!("Failed to format document: {e}");
647 Ok(Some(Vec::new()))
648 }
649 }
650 } else {
651 log::warn!("Document not found: {uri}");
652 Ok(None)
653 }
654 }
655
656 async fn diagnostic(&self, params: DocumentDiagnosticParams) -> JsonRpcResult<DocumentDiagnosticReportResult> {
657 let uri = params.text_document.uri;
658
659 if let Some(text) = self.get_document_content(&uri).await {
660 match self.lint_document(&uri, &text).await {
661 Ok(diagnostics) => Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
662 RelatedFullDocumentDiagnosticReport {
663 related_documents: None,
664 full_document_diagnostic_report: FullDocumentDiagnosticReport {
665 result_id: None,
666 items: diagnostics,
667 },
668 },
669 ))),
670 Err(e) => {
671 log::error!("Failed to get diagnostics: {e}");
672 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
673 RelatedFullDocumentDiagnosticReport {
674 related_documents: None,
675 full_document_diagnostic_report: FullDocumentDiagnosticReport {
676 result_id: None,
677 items: Vec::new(),
678 },
679 },
680 )))
681 }
682 }
683 } else {
684 Ok(DocumentDiagnosticReportResult::Report(DocumentDiagnosticReport::Full(
685 RelatedFullDocumentDiagnosticReport {
686 related_documents: None,
687 full_document_diagnostic_report: FullDocumentDiagnosticReport {
688 result_id: None,
689 items: Vec::new(),
690 },
691 },
692 )))
693 }
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use crate::rule::LintWarning;
701 use tower_lsp::LspService;
702
703 fn create_test_server() -> RumdlLanguageServer {
704 let (service, _socket) = LspService::new(RumdlLanguageServer::new);
705 service.inner().clone()
706 }
707
708 #[tokio::test]
709 async fn test_server_creation() {
710 let server = create_test_server();
711
712 let config = server.config.read().await;
714 assert!(config.enable_linting);
715 assert!(!config.enable_auto_fix);
716 }
717
718 #[tokio::test]
719 async fn test_lint_document() {
720 let server = create_test_server();
721
722 let uri = Url::parse("file:///test.md").unwrap();
724 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
725
726 let diagnostics = server.lint_document(&uri, text).await.unwrap();
727
728 assert!(!diagnostics.is_empty());
730 assert!(diagnostics.iter().any(|d| d.message.contains("trailing")));
731 }
732
733 #[tokio::test]
734 async fn test_lint_document_disabled() {
735 let server = create_test_server();
736
737 server.config.write().await.enable_linting = false;
739
740 let uri = Url::parse("file:///test.md").unwrap();
741 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
742
743 let diagnostics = server.lint_document(&uri, text).await.unwrap();
744
745 assert!(diagnostics.is_empty());
747 }
748
749 #[tokio::test]
750 async fn test_get_code_actions() {
751 let server = create_test_server();
752
753 let uri = Url::parse("file:///test.md").unwrap();
754 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
755
756 let range = Range {
758 start: Position { line: 0, character: 0 },
759 end: Position { line: 3, character: 21 },
760 };
761
762 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
763
764 assert!(!actions.is_empty());
766 assert!(actions.iter().any(|a| a.title.contains("trailing")));
767 }
768
769 #[tokio::test]
770 async fn test_get_code_actions_outside_range() {
771 let server = create_test_server();
772
773 let uri = Url::parse("file:///test.md").unwrap();
774 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
775
776 let range = Range {
778 start: Position { line: 0, character: 0 },
779 end: Position { line: 0, character: 6 },
780 };
781
782 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
783
784 assert!(actions.is_empty());
786 }
787
788 #[tokio::test]
789 async fn test_document_storage() {
790 let server = create_test_server();
791
792 let uri = Url::parse("file:///test.md").unwrap();
793 let text = "# Test Document";
794
795 let entry = DocumentEntry {
797 content: text.to_string(),
798 version: Some(1),
799 from_disk: false,
800 };
801 server.documents.write().await.insert(uri.clone(), entry);
802
803 let stored = server.documents.read().await.get(&uri).map(|e| e.content.clone());
805 assert_eq!(stored, Some(text.to_string()));
806
807 server.documents.write().await.remove(&uri);
809
810 let stored = server.documents.read().await.get(&uri).cloned();
812 assert_eq!(stored, None);
813 }
814
815 #[tokio::test]
816 async fn test_configuration_loading() {
817 let server = create_test_server();
818
819 server.load_configuration(false).await;
821
822 let rumdl_config = server.rumdl_config.read().await;
825 drop(rumdl_config); }
828
829 #[tokio::test]
830 async fn test_load_config_for_lsp() {
831 let result = RumdlLanguageServer::load_config_for_lsp(None);
833 assert!(result.is_ok());
834
835 let result = RumdlLanguageServer::load_config_for_lsp(Some("/nonexistent/config.toml"));
837 assert!(result.is_err());
838 }
839
840 #[tokio::test]
841 async fn test_warning_conversion() {
842 let warning = LintWarning {
843 message: "Test warning".to_string(),
844 line: 1,
845 column: 1,
846 end_line: 1,
847 end_column: 10,
848 severity: crate::rule::Severity::Warning,
849 fix: None,
850 rule_name: Some("MD001"),
851 };
852
853 let diagnostic = warning_to_diagnostic(&warning);
855 assert_eq!(diagnostic.message, "Test warning");
856 assert_eq!(diagnostic.severity, Some(DiagnosticSeverity::WARNING));
857 assert_eq!(diagnostic.code, Some(NumberOrString::String("MD001".to_string())));
858
859 let uri = Url::parse("file:///test.md").unwrap();
861 let action = warning_to_code_action(&warning, &uri, "Test content");
862 assert!(action.is_none());
863 }
864
865 #[tokio::test]
866 async fn test_multiple_documents() {
867 let server = create_test_server();
868
869 let uri1 = Url::parse("file:///test1.md").unwrap();
870 let uri2 = Url::parse("file:///test2.md").unwrap();
871 let text1 = "# Document 1";
872 let text2 = "# Document 2";
873
874 {
876 let mut docs = server.documents.write().await;
877 let entry1 = DocumentEntry {
878 content: text1.to_string(),
879 version: Some(1),
880 from_disk: false,
881 };
882 let entry2 = DocumentEntry {
883 content: text2.to_string(),
884 version: Some(1),
885 from_disk: false,
886 };
887 docs.insert(uri1.clone(), entry1);
888 docs.insert(uri2.clone(), entry2);
889 }
890
891 let docs = server.documents.read().await;
893 assert_eq!(docs.len(), 2);
894 assert_eq!(docs.get(&uri1).map(|s| s.content.as_str()), Some(text1));
895 assert_eq!(docs.get(&uri2).map(|s| s.content.as_str()), Some(text2));
896 }
897
898 #[tokio::test]
899 async fn test_auto_fix_on_save() {
900 let server = create_test_server();
901
902 {
904 let mut config = server.config.write().await;
905 config.enable_auto_fix = true;
906 }
907
908 let uri = Url::parse("file:///test.md").unwrap();
909 let text = "#Heading without space"; let entry = DocumentEntry {
913 content: text.to_string(),
914 version: Some(1),
915 from_disk: false,
916 };
917 server.documents.write().await.insert(uri.clone(), entry);
918
919 let fixed = server.apply_all_fixes(&uri, text).await.unwrap();
921 assert!(fixed.is_some());
922 assert_eq!(fixed.unwrap(), "# Heading without space");
923 }
924
925 #[tokio::test]
926 async fn test_get_end_position() {
927 let server = create_test_server();
928
929 let pos = server.get_end_position("Hello");
931 assert_eq!(pos.line, 0);
932 assert_eq!(pos.character, 5);
933
934 let pos = server.get_end_position("Hello\nWorld\nTest");
936 assert_eq!(pos.line, 2);
937 assert_eq!(pos.character, 4);
938
939 let pos = server.get_end_position("");
941 assert_eq!(pos.line, 0);
942 assert_eq!(pos.character, 0);
943
944 let pos = server.get_end_position("Hello\n");
946 assert_eq!(pos.line, 1);
947 assert_eq!(pos.character, 0);
948 }
949
950 #[tokio::test]
951 async fn test_empty_document_handling() {
952 let server = create_test_server();
953
954 let uri = Url::parse("file:///empty.md").unwrap();
955 let text = "";
956
957 let diagnostics = server.lint_document(&uri, text).await.unwrap();
959 assert!(diagnostics.is_empty());
960
961 let range = Range {
963 start: Position { line: 0, character: 0 },
964 end: Position { line: 0, character: 0 },
965 };
966 let actions = server.get_code_actions(&uri, text, range).await.unwrap();
967 assert!(actions.is_empty());
968 }
969
970 #[tokio::test]
971 async fn test_config_update() {
972 let server = create_test_server();
973
974 {
976 let mut config = server.config.write().await;
977 config.enable_auto_fix = true;
978 config.config_path = Some("/custom/path.toml".to_string());
979 }
980
981 let config = server.config.read().await;
983 assert!(config.enable_auto_fix);
984 assert_eq!(config.config_path, Some("/custom/path.toml".to_string()));
985 }
986
987 #[tokio::test]
988 async fn test_document_formatting() {
989 let server = create_test_server();
990 let uri = Url::parse("file:///test.md").unwrap();
991 let text = "# Test\n\nThis is a test \nWith trailing spaces ";
992
993 let entry = DocumentEntry {
995 content: text.to_string(),
996 version: Some(1),
997 from_disk: false,
998 };
999 server.documents.write().await.insert(uri.clone(), entry);
1000
1001 let params = DocumentFormattingParams {
1003 text_document: TextDocumentIdentifier { uri: uri.clone() },
1004 options: FormattingOptions {
1005 tab_size: 4,
1006 insert_spaces: true,
1007 properties: HashMap::new(),
1008 trim_trailing_whitespace: Some(true),
1009 insert_final_newline: Some(true),
1010 trim_final_newlines: Some(true),
1011 },
1012 work_done_progress_params: WorkDoneProgressParams::default(),
1013 };
1014
1015 let result = server.formatting(params).await.unwrap();
1017
1018 assert!(result.is_some());
1020 let edits = result.unwrap();
1021 assert!(!edits.is_empty());
1022
1023 let edit = &edits[0];
1025 let expected = "# Test\n\nThis is a test \nWith trailing spaces\n";
1028 assert_eq!(edit.new_text, expected);
1029 }
1030}