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