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