Skip to main content

rumdl_lib/lsp/
mod.rs

1//! Language Server Protocol implementation for rumdl
2//!
3//! This module provides a Language Server Protocol (LSP) implementation for rumdl,
4//! enabling real-time markdown linting in editors and IDEs.
5//!
6//! Following Ruff's approach, this is built directly into the main rumdl binary
7//! and can be started with `rumdl server`.
8
9mod completion;
10mod configuration;
11pub mod index_worker;
12mod linting;
13mod navigation;
14pub mod server;
15pub mod types;
16
17pub use server::RumdlLanguageServer;
18pub use types::{RumdlLspConfig, warning_to_code_actions, warning_to_diagnostic};
19
20use anyhow::Result;
21use tokio::net::TcpListener;
22use tower_lsp::{LspService, Server};
23
24/// Start the Language Server Protocol server
25/// This is the main entry point for `rumdl server`
26pub async fn start_server(config_path: Option<&str>) -> Result<()> {
27    let stdin = tokio::io::stdin();
28    let stdout = tokio::io::stdout();
29
30    let (service, socket) = LspService::new(|client| RumdlLanguageServer::new(client, config_path));
31
32    log::info!("Starting rumdl Language Server Protocol server");
33
34    Server::new(stdin, stdout, socket).serve(service).await;
35
36    Ok(())
37}
38
39/// Start the LSP server over TCP (useful for debugging)
40pub async fn start_tcp_server(port: u16, config_path: Option<&str>) -> Result<()> {
41    let listener = TcpListener::bind(format!("127.0.0.1:{port}")).await?;
42    log::info!("rumdl LSP server listening on 127.0.0.1:{port}");
43
44    // Clone config_path to owned String so we can move it into the spawned task
45    let config_path_owned = config_path.map(|s| s.to_string());
46
47    loop {
48        let (stream, _) = listener.accept().await?;
49        let config_path_clone = config_path_owned.clone();
50        let (service, socket) =
51            LspService::new(move |client| RumdlLanguageServer::new(client, config_path_clone.as_deref()));
52
53        tokio::spawn(async move {
54            let (read, write) = tokio::io::split(stream);
55            Server::new(read, write, socket).serve(service).await;
56        });
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_module_exports() {
66        // Verify that the module exports are accessible
67        // This ensures the public API is stable
68        fn _check_exports() {
69            // These should compile without errors
70            let _server_type: RumdlLanguageServer;
71            let _config_type: RumdlLspConfig;
72            let _func1: fn(&crate::rule::LintWarning) -> tower_lsp::lsp_types::Diagnostic = warning_to_diagnostic;
73            let _func2: fn(
74                &crate::rule::LintWarning,
75                &tower_lsp::lsp_types::Url,
76                &str,
77            ) -> Vec<tower_lsp::lsp_types::CodeAction> = warning_to_code_actions;
78        }
79    }
80
81    #[tokio::test]
82    async fn test_tcp_server_bind() {
83        use std::net::TcpListener as StdTcpListener;
84
85        // Find an available port
86        let listener = StdTcpListener::bind("127.0.0.1:0").unwrap();
87        let port = listener.local_addr().unwrap().port();
88        drop(listener);
89
90        // Start the server in a background task
91        let server_handle = tokio::spawn(async move {
92            // Server should start without panicking
93            match tokio::time::timeout(std::time::Duration::from_millis(100), start_tcp_server(port, None)).await {
94                Ok(Ok(())) => {} // Server started and stopped normally
95                Ok(Err(_)) => {} // Server had an error (expected in test)
96                Err(_) => {}     // Timeout (expected - server runs forever)
97            }
98        });
99
100        // Give the server time to start
101        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
102
103        // Try to connect to verify it's listening
104        match tokio::time::timeout(
105            std::time::Duration::from_millis(50),
106            tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")),
107        )
108        .await
109        {
110            Ok(Ok(_)) => {
111                // Successfully connected
112            }
113            _ => {
114                // Connection failed or timed out - that's okay for this test
115            }
116        }
117
118        // Cancel the server task
119        server_handle.abort();
120    }
121
122    #[tokio::test]
123    async fn test_tcp_server_invalid_port() {
124        // Port 0 is technically valid (OS assigns), but let's test a privileged port
125        // that we likely can't bind to without root
126        let result = tokio::time::timeout(std::time::Duration::from_millis(100), start_tcp_server(80, None)).await;
127
128        match result {
129            Ok(Err(_)) => {
130                // Expected - should fail to bind to privileged port
131            }
132            Ok(Ok(())) => {
133                panic!("Should not be able to bind to port 80 without privileges");
134            }
135            Err(_) => {
136                // Timeout - server tried to run, which means bind succeeded
137                // This might happen if tests are run as root
138            }
139        }
140    }
141
142    #[tokio::test]
143    async fn test_service_creation() {
144        // Test that we can create the LSP service
145        let (service, _socket) = LspService::new(|client| RumdlLanguageServer::new(client, None));
146
147        // Service should be created successfully
148        // We can't easily test more without a full LSP client
149        drop(service);
150    }
151
152    #[tokio::test]
153    async fn test_multiple_tcp_connections() {
154        use std::net::TcpListener as StdTcpListener;
155
156        // Find an available port
157        let listener = StdTcpListener::bind("127.0.0.1:0").unwrap();
158        let port = listener.local_addr().unwrap().port();
159        drop(listener);
160
161        // Start the server
162        let server_handle = tokio::spawn(async move {
163            let _ = tokio::time::timeout(std::time::Duration::from_millis(500), start_tcp_server(port, None)).await;
164        });
165
166        // Give server time to start
167        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
168
169        // Try multiple connections
170        let mut handles = vec![];
171        for _ in 0..3 {
172            let handle = tokio::spawn(async move {
173                match tokio::time::timeout(
174                    std::time::Duration::from_millis(100),
175                    tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")),
176                )
177                .await
178                {
179                    Ok(Ok(_stream)) => {
180                        // Connection successful
181                        true
182                    }
183                    _ => false,
184                }
185            });
186            handles.push(handle);
187        }
188
189        // Wait for all connections
190        for handle in handles {
191            let _ = handle.await;
192        }
193
194        // Clean up
195        server_handle.abort();
196    }
197
198    #[test]
199    fn test_logging_initialization() {
200        // Verify that starting the server includes proper logging
201        // This is more of a smoke test to ensure logging statements compile
202
203        // The actual log::info! calls are in the async functions,
204        // but we can at least verify the module imports and uses logging
205        let _info_level = log::Level::Info;
206    }
207}