Unnamed repository; edit this file 'description' to name the repository.
| -rw-r--r-- | crates/rust-analyzer/src/bin/main.rs | 176 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/rust-analyzer/src/session.rs | 212 |
3 files changed, 221 insertions, 168 deletions
diff --git a/crates/rust-analyzer/src/bin/main.rs b/crates/rust-analyzer/src/bin/main.rs index f175bb06bf..e05385528a 100644 --- a/crates/rust-analyzer/src/bin/main.rs +++ b/crates/rust-analyzer/src/bin/main.rs @@ -14,14 +14,8 @@ use std::{env, fs, path::PathBuf, process::ExitCode, sync::Arc}; use anyhow::Context; use lsp_server::Connection; -use paths::Utf8PathBuf; -use rust_analyzer::{ - cli::flags, - config::{Config, ConfigChange, ConfigErrors}, - from_json, -}; +use rust_analyzer::{cli::flags, config::Config}; use tracing_subscriber::fmt::writer::BoxMakeWriter; -use vfs::AbsPathBuf; #[cfg(feature = "mimalloc")] #[global_allocator] @@ -71,7 +65,7 @@ fn actual_main() -> anyhow::Result<ExitCode> { with_extra_thread( "LspServer", stdx::thread::ThreadIntent::LatencySensitive, - run_server, + move || run_server(None), )?; } flags::RustAnalyzerCmd::Parse(cmd) => cmd.run()?, @@ -190,171 +184,17 @@ fn with_extra_thread( Ok(()) } -fn run_server() -> anyhow::Result<()> { - tracing::info!("server version {} will start", rust_analyzer::version()); - +fn run_server(startup_notice: Option<String>) -> anyhow::Result<()> { let (connection, io_threads) = Connection::stdio(); - let (initialize_id, initialize_params) = match connection.initialize_start() { - Ok(it) => it, - Err(e) => { - if e.channel_is_disconnected() { - io_threads.join()?; - } - return Err(e.into()); - } - }; - - tracing::info!("InitializeParams: {}", initialize_params); - let lsp_types::InitializeParams { - #[expect(deprecated, reason = "compatibility with old clients")] - root_uri, - capabilities, - workspace_folders_initialize_params, - initialization_options, - client_info, - .. - } = from_json::<lsp_types::InitializeParams>("InitializeParams", &initialize_params)?; - - let root_path = match root_uri - .and_then(|it| it.to_file_path().ok()) - .map(patch_path_prefix) - .and_then(|it| Utf8PathBuf::from_path_buf(it).ok()) - .and_then(|it| AbsPathBuf::try_from(it).ok()) - { - Some(it) => it, - None => { - let cwd = env::current_dir()?; - AbsPathBuf::assert_utf8(cwd) - } - }; - - if let Some(client_info) = &client_info { - tracing::info!( - "Client '{}' {}", - client_info.name, - client_info.version.as_deref().unwrap_or_default() - ); - } - - let workspace_roots = workspace_folders_initialize_params - .workspace_folders - .and_then(|workspaces| match workspaces { - lsp_types::WorkspaceFolders::WorkspaceFolderList(workspace_folders) => { - Some(workspace_folders) - } - lsp_types::WorkspaceFolders::Null => None, - }) - .map(|workspaces| { - workspaces - .into_iter() - .filter_map(|it| it.uri.to_file_path().ok()) - .map(patch_path_prefix) - .filter_map(|it| Utf8PathBuf::from_path_buf(it).ok()) - .filter_map(|it| AbsPathBuf::try_from(it).ok()) - .collect::<Vec<_>>() - }) - .filter(|workspaces| !workspaces.is_empty()) - .unwrap_or_else(|| vec![root_path.clone()]); - let mut config = Config::new(root_path, capabilities, workspace_roots, client_info); - if let Some(json) = initialization_options { - let mut change = ConfigChange::default(); - change.change_client_config(json); - - let error_sink: ConfigErrors; - (config, error_sink, _) = config.apply_change(change); - - if !error_sink.is_empty() { - use lsp_types::{ - MessageType, Notification as _, ShowMessageNotification, ShowMessageParams, - }; - let not = lsp_server::Notification::new( - ShowMessageNotification::METHOD.into(), - ShowMessageParams { kind: MessageType::Warning, message: error_sink.to_string() }, - ); - connection.sender.send(lsp_server::Message::Notification(not)).unwrap(); - } - } - - let server_capabilities = rust_analyzer::server_capabilities(&config); - - let initialize_result = lsp_types::InitializeResult { - capabilities: server_capabilities, - server_info: Some(lsp_types::ServerInfo { - name: String::from("rust-analyzer"), - version: Some(rust_analyzer::version().to_string()), - }), - }; - - let initialize_result = serde_json::to_value(initialize_result).unwrap(); - - if let Err(e) = connection.initialize_finish(initialize_id, initialize_result) { - if e.channel_is_disconnected() { - io_threads.join()?; - } - return Err(e.into()); - } - - if config.discover_workspace_config().is_none() - && !config.has_linked_projects() - && config.detached_files().is_empty() - { - config.rediscover_workspaces(); - } - rayon::ThreadPoolBuilder::new() .thread_name(|ix| format!("RayonWorker{}", ix)) .build_global() .unwrap(); - // If the io_threads have an error, there's usually an error on the main - // loop too because the channels are closed. Ensure we report both errors. - match (rust_analyzer::main_loop(config, connection), io_threads.join()) { - (Err(loop_e), Err(join_e)) => anyhow::bail!("{loop_e}\n{join_e}"), - (Ok(_), Err(join_e)) => anyhow::bail!("{join_e}"), - (Err(loop_e), Ok(_)) => anyhow::bail!("{loop_e}"), - (Ok(_), Ok(_)) => {} - } - - tracing::info!("server did shut down"); - Ok(()) -} - -fn patch_path_prefix(path: PathBuf) -> PathBuf { - use std::path::{Component, Prefix}; - if cfg!(windows) { - // VSCode might report paths with the file drive in lowercase, but this can mess - // with env vars set by tools and build scripts executed by r-a such that it invalidates - // cargo's compilations unnecessarily. https://github.com/rust-lang/rust-analyzer/issues/14683 - // So we just uppercase the drive letter here unconditionally. - // (doing it conditionally is a pain because std::path::Prefix always reports uppercase letters on windows) - let mut comps = path.components(); - match comps.next() { - Some(Component::Prefix(prefix)) => { - let prefix = match prefix.kind() { - Prefix::Disk(d) => { - format!("{}:", d.to_ascii_uppercase() as char) - } - Prefix::VerbatimDisk(d) => { - format!(r"\\?\{}:", d.to_ascii_uppercase() as char) - } - _ => return path, - }; - let mut path = PathBuf::new(); - path.push(prefix); - path.extend(comps); - path - } - _ => path, - } - } else { - path - } -} - -#[test] -#[cfg(windows)] -fn patch_path_prefix_works() { - assert_eq!(patch_path_prefix(r"c:\foo\bar".into()), PathBuf::from(r"C:\foo\bar")); - assert_eq!(patch_path_prefix(r"\\?\c:\foo\bar".into()), PathBuf::from(r"\\?\C:\foo\bar")); + rust_analyzer::session::run_session( + connection, + rust_analyzer::session::IoThreads::Stdio(io_threads), + startup_notice, + ) } diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index 950592cb26..e953ea2136 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -60,6 +60,7 @@ pub mod tracing { pub mod config; mod global_state; pub mod lsp; +pub mod session; use self::lsp::ext as lsp_ext; diff --git a/crates/rust-analyzer/src/session.rs b/crates/rust-analyzer/src/session.rs new file mode 100644 index 0000000000..3536f47999 --- /dev/null +++ b/crates/rust-analyzer/src/session.rs @@ -0,0 +1,212 @@ +//! Entry point of a single LSP session: initialization handshake, then the main loop. + +use std::{env, path::PathBuf}; + +use anyhow::Context; +use lsp_server::Connection; +use paths::Utf8PathBuf; +use vfs::AbsPathBuf; + +use crate::{ + config::{Config, ConfigChange, ConfigErrors}, + from_json, +}; + +/// Handles to the I/O threads shuttling the messages of a [`Connection`], joined when +/// the session ends to surface transport errors. +pub enum IoThreads { + /// The stdio transport of a standalone server. + Stdio(lsp_server::IoThreads), +} + +impl IoThreads { + fn join(self) -> anyhow::Result<()> { + match self { + IoThreads::Stdio(io_threads) => Ok(io_threads.join()?), + } + } +} + +/// Runs a full LSP session over `connection`: waits for the client's `initialize`, +/// negotiates capabilities, then runs the main loop until the client disconnects or +/// requests shutdown. +/// +/// # Errors +/// +/// Returns an error if the connection breaks down or the main loop exits abnormally. +pub fn run_session( + connection: Connection, + io_threads: IoThreads, + startup_notice: Option<String>, +) -> anyhow::Result<()> { + tracing::info!("server version {} will start", crate::version()); + + let (initialize_id, initialize_params) = match connection.initialize_start() { + Ok(it) => it, + Err(e) => { + if e.channel_is_disconnected() { + io_threads.join()?; + } + return Err(e.into()); + } + }; + + tracing::info!("InitializeParams: {}", initialize_params); + let lsp_types::InitializeParams { + #[expect(deprecated, reason = "compatibility with old clients")] + root_uri, + capabilities, + workspace_folders_initialize_params, + initialization_options, + client_info, + .. + } = from_json::<lsp_types::InitializeParams>("InitializeParams", &initialize_params)?; + + let root_path = match root_uri + .and_then(|it| it.to_file_path().ok()) + .map(patch_path_prefix) + .and_then(|it| Utf8PathBuf::from_path_buf(it).ok()) + .and_then(|it| AbsPathBuf::try_from(it).ok()) + { + Some(it) => it, + None => { + let cwd = env::current_dir().context("couldn't determine working directory")?; + AbsPathBuf::assert_utf8(cwd) + } + }; + + if let Some(client_info) = &client_info { + tracing::info!( + "Client '{}' {}", + client_info.name, + client_info.version.as_deref().unwrap_or_default() + ); + } + + let workspace_roots = workspace_folders_initialize_params + .workspace_folders + .and_then(|workspaces| match workspaces { + lsp_types::WorkspaceFolders::WorkspaceFolderList(workspace_folders) => { + Some(workspace_folders) + } + lsp_types::WorkspaceFolders::Null => None, + }) + .map(|workspaces| { + workspaces + .into_iter() + .filter_map(|it| it.uri.to_file_path().ok()) + .map(patch_path_prefix) + .filter_map(|it| Utf8PathBuf::from_path_buf(it).ok()) + .filter_map(|it| AbsPathBuf::try_from(it).ok()) + .collect::<Vec<_>>() + }) + .filter(|workspaces| !workspaces.is_empty()) + .unwrap_or_else(|| vec![root_path.clone()]); + let mut config = Config::new(root_path, capabilities, workspace_roots, client_info); + if let Some(json) = initialization_options { + let mut change = ConfigChange::default(); + change.change_client_config(json); + + let error_sink: ConfigErrors; + (config, error_sink, _) = config.apply_change(change); + + if !error_sink.is_empty() { + use lsp_types::{ + MessageType, Notification as _, ShowMessageNotification, ShowMessageParams, + }; + let not = lsp_server::Notification::new( + ShowMessageNotification::METHOD.into(), + ShowMessageParams { kind: MessageType::Warning, message: error_sink.to_string() }, + ); + connection.sender.send(lsp_server::Message::Notification(not)).unwrap(); + } + } + + let server_capabilities = crate::server_capabilities(&config); + + let initialize_result = lsp_types::InitializeResult { + capabilities: server_capabilities, + server_info: Some(lsp_types::ServerInfo { + name: String::from("rust-analyzer"), + version: Some(crate::version().to_string()), + }), + }; + + let initialize_result = serde_json::to_value(initialize_result).unwrap(); + + if let Err(e) = connection.initialize_finish(initialize_id, initialize_result) { + if e.channel_is_disconnected() { + io_threads.join()?; + } + return Err(e.into()); + } + + if let Some(notice) = startup_notice { + use lsp_types::{ + MessageType, Notification as _, ShowMessageNotification, ShowMessageParams, + }; + let not = lsp_server::Notification::new( + ShowMessageNotification::METHOD.into(), + ShowMessageParams { kind: MessageType::Warning, message: notice }, + ); + connection.sender.send(lsp_server::Message::Notification(not)).unwrap(); + } + + if config.discover_workspace_config().is_none() + && !config.has_linked_projects() + && config.detached_files().is_empty() + { + config.rediscover_workspaces(); + } + + // If the io_threads have an error, there's usually an error on the main + // loop too because the channels are closed. Ensure we report both errors. + match (crate::main_loop(config, connection), io_threads.join()) { + (Err(loop_e), Err(join_e)) => anyhow::bail!("{loop_e}\n{join_e}"), + (Ok(_), Err(join_e)) => anyhow::bail!("{join_e}"), + (Err(loop_e), Ok(_)) => anyhow::bail!("{loop_e}"), + (Ok(_), Ok(_)) => {} + } + + tracing::info!("server did shut down"); + Ok(()) +} + +fn patch_path_prefix(path: PathBuf) -> PathBuf { + use std::path::{Component, Prefix}; + if cfg!(windows) { + // VSCode might report paths with the file drive in lowercase, but this can mess + // with env vars set by tools and build scripts executed by r-a such that it invalidates + // cargo's compilations unnecessarily. https://github.com/rust-lang/rust-analyzer/issues/14683 + // So we just uppercase the drive letter here unconditionally. + // (doing it conditionally is a pain because std::path::Prefix always reports uppercase letters on windows) + let mut comps = path.components(); + match comps.next() { + Some(Component::Prefix(prefix)) => { + let prefix = match prefix.kind() { + Prefix::Disk(d) => { + format!("{}:", d.to_ascii_uppercase() as char) + } + Prefix::VerbatimDisk(d) => { + format!(r"\\?\{}:", d.to_ascii_uppercase() as char) + } + _ => return path, + }; + let mut path = PathBuf::new(); + path.push(prefix); + path.extend(comps); + path + } + _ => path, + } + } else { + path + } +} + +#[test] +#[cfg(windows)] +fn patch_path_prefix_works() { + assert_eq!(patch_path_prefix(r"c:\foo\bar".into()), PathBuf::from(r"C:\foo\bar")); + assert_eq!(patch_path_prefix(r"\\?\c:\foo\bar".into()), PathBuf::from(r"\\?\C:\foo\bar")); +} |