From db53a65ab6c84978b62999437f8aa7c807e85931 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:36:37 -0500 Subject: [PATCH] Add configurable LSP timeout setting (#44745) Fixes #36818 Release Notes: - Added new `global_lsp_settings.request_timeout` setting to configure the maximum timeout duration for LSP-related operations. Code inspired by [prior implementation](https://github.com/zed-industries/zed/pull/38443), though with a few tweaks here & there (like using `serde:default` and keeping the pre-defined constant in the LSP file). --------- Co-authored-by: Kirill Bulatov Co-authored-by: Kirill Bulatov --- Cargo.lock | 1 + assets/settings/default.json | 5 + .../collab/tests/integration/editor_tests.rs | 18 +- .../tests/integration/integration_tests.rs | 11 +- crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 200 +++-- crates/copilot_ui/src/sign_in.rs | 6 + crates/editor/src/editor_tests.rs | 223 +++++- crates/editor/src/inlays/inlay_hints.rs | 19 +- crates/editor/src/lsp_ext.rs | 2 +- crates/lsp/src/input_handler.rs | 19 +- crates/lsp/src/lsp.rs | 295 ++++--- crates/prettier/src/prettier.rs | 13 +- crates/project/src/lsp_store.rs | 733 +++++++++++------- crates/project/src/lsp_store/code_lens.rs | 9 +- .../project/src/lsp_store/document_colors.rs | 14 +- .../project/src/lsp_store/folding_ranges.rs | 9 +- crates/project/src/lsp_store/inlay_hints.rs | 10 +- .../project/src/lsp_store/semantic_tokens.rs | 7 +- .../src/lsp_store/vue_language_server_ext.rs | 179 +++-- crates/project/src/prettier_store.rs | 146 ++-- crates/project/src/project_settings.rs | 46 +- .../tests/integration/project_tests.rs | 98 +-- .../remote_server/src/remote_editing_tests.rs | 8 +- crates/settings_content/src/project.rs | 5 + docs/src/reference/all-settings.md | 2 + 26 files changed, 1351 insertions(+), 728 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c243209655452fdf9c7362fce5bf7f5d3f07d998..a7fdc32d03091956aa49bed4041dd1b87cf46792 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3736,6 +3736,7 @@ dependencies = [ "node_runtime", "parking_lot", "paths", + "pretty_assertions", "project", "rpc", "semver", diff --git a/assets/settings/default.json b/assets/settings/default.json index 5a8e0047b401a5edbc2755dda2af4183fd4b6f5b..8541c1fb749946de04caed2f185d4c3bd3d4c292 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -2230,6 +2230,11 @@ "global_lsp_settings": { // Whether to show the LSP servers button in the status bar. "button": true, + // The maximum amount of time to wait for responses from language servers, in seconds. + // + // A value of `0` will result in no timeout being applied (causing all LSP responses to wait + // indefinitely until completed). + "request_timeout": 120, "notifications": { // Timeout in milliseconds for automatically dismissing language server notifications. // Set to 0 to disable auto-dismiss. diff --git a/crates/collab/tests/integration/editor_tests.rs b/crates/collab/tests/integration/editor_tests.rs index 2ef5f52ea625e2266ab96ec0f285b48b6dfa3d55..1612e32833dd07dd5fa2294d5bb5a90442883f71 100644 --- a/crates/collab/tests/integration/editor_tests.rs +++ b/crates/collab/tests/integration/editor_tests.rs @@ -23,7 +23,7 @@ use gpui::{ }; use indoc::indoc; use language::{FakeLspAdapter, language_settings::language_settings, rust_lang}; -use lsp::LSP_REQUEST_TIMEOUT; +use lsp::DEFAULT_LSP_REQUEST_TIMEOUT; use multi_buffer::{AnchorRangeExt as _, MultiBufferRow}; use pretty_assertions::assert_eq; use project::{ @@ -1255,7 +1255,7 @@ async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte cx_a.run_until_parked(); cx_b.run_until_parked(); - let long_request_time = LSP_REQUEST_TIMEOUT / 2; + let long_request_time = DEFAULT_LSP_REQUEST_TIMEOUT / 2; let (request_started_tx, mut request_started_rx) = mpsc::unbounded(); let requests_started = Arc::new(AtomicUsize::new(0)); let requests_completed = Arc::new(AtomicUsize::new(0)); @@ -1362,8 +1362,8 @@ async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte ); assert_eq!( requests_completed.load(atomic::Ordering::Acquire), - 3, - "After enough time, all 3 LSP requests should have been served by the language server" + 1, + "After enough time, a single, deduplicated, LSP request should have been served by the language server" ); let resulting_lens_actions = editor_b .update(cx_b, |editor, cx| { @@ -1382,7 +1382,7 @@ async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte ); assert_eq!( resulting_lens_actions.first().unwrap().lsp_action.title(), - "LSP Command 3", + "LSP Command 1", "Only the final code lens action should be in the data" ) } @@ -2164,7 +2164,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1; fake_language_server - .request::(()) + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) .await .into_response() .expect("inlay refresh request failed"); @@ -2375,7 +2375,7 @@ async fn test_inlay_hint_refresh_is_forwarded( other_hints.fetch_or(true, atomic::Ordering::Release); fake_language_server - .request::(()) + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) .await .into_response() .expect("inlay refresh request failed"); @@ -3414,7 +3414,7 @@ async fn test_lsp_pull_diagnostics( } fake_language_server - .request::(()) + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) .await .into_response() .expect("workspace diagnostics refresh request failed"); @@ -5185,7 +5185,7 @@ async fn test_semantic_token_refresh_is_forwarded( other_tokens.fetch_or(true, atomic::Ordering::Release); fake_language_server - .request::(()) + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) .await .into_response() .expect("semantic tokens refresh request failed"); diff --git a/crates/collab/tests/integration/integration_tests.rs b/crates/collab/tests/integration/integration_tests.rs index 18d6f2bb495626672dafbf8031186a4d4c7ddd5e..766585b3b6828c16d51a8e0e0657e622d39e0861 100644 --- a/crates/collab/tests/integration/integration_tests.rs +++ b/crates/collab/tests/integration/integration_tests.rs @@ -26,7 +26,7 @@ use language::{ language_settings::{Formatter, FormatterList}, rust_lang, tree_sitter_rust, tree_sitter_typescript, }; -use lsp::{LanguageServerId, OneOf}; +use lsp::{DEFAULT_LSP_REQUEST_TIMEOUT, LanguageServerId, OneOf}; use parking_lot::Mutex; use pretty_assertions::assert_eq; use project::{ @@ -4358,9 +4358,12 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering( let fake_language_server = fake_language_servers.next().await.unwrap(); executor.run_until_parked(); fake_language_server - .request::(lsp::WorkDoneProgressCreateParams { - token: lsp::NumberOrString::String("the-disk-based-token".to_string()), - }) + .request::( + lsp::WorkDoneProgressCreateParams { + token: lsp::NumberOrString::String("the-disk-based-token".to_string()), + }, + DEFAULT_LSP_REQUEST_TIMEOUT, + ) .await .into_response() .unwrap(); diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 6e5144ff6346375e90ea368074b5da7c4e7a5d87..236216a8d9a64f736c76399867f0b8766c93c16b 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -64,6 +64,7 @@ indoc.workspace = true language = { workspace = true, features = ["test-support"] } lsp = { workspace = true, features = ["test-support"] } node_runtime = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } rpc = { workspace = true, features = ["test-support"] } serde_json.workspace = true diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index fd389b4e7be6506e1ffec78d84c1dd416e4e0bf4..fb815e04a6eb9f3d713c593a3549a66c479cfb9c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -24,6 +24,7 @@ use language::{ use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; use node_runtime::{NodeRuntime, VersionStrategy}; use parking_lot::Mutex; +use project::project_settings::ProjectSettings; use project::{DisableAiSettings, Project}; use request::DidChangeStatus; use semver::Version; @@ -347,6 +348,9 @@ impl Copilot { let global_authentication_events = cx.try_global::().cloned().map(|auth| { cx.subscribe(&auth.0, |_, _, _: &Event, cx| { + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); cx.spawn(async move |this, cx| { let Some(server) = this .update(cx, |this, _| this.language_server().cloned()) @@ -356,9 +360,12 @@ impl Copilot { return; }; let status = server - .request::(request::CheckStatusParams { - local_checks_only: false, - }) + .request::( + request::CheckStatusParams { + local_checks_only: false, + }, + request_timeout, + ) .await .into_response() .ok(); @@ -584,10 +591,18 @@ impl Copilot { .ok() .flatten(); let Some(lsp) = lsp else { return }; + let request_timeout = cx.update(|cx| { + ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout() + }); let status = lsp - .request::(request::CheckStatusParams { - local_checks_only: false, - }) + .request::( + request::CheckStatusParams { + local_checks_only: false, + }, + request_timeout, + ) .await .into_response() .ok(); @@ -630,6 +645,12 @@ impl Copilot { }; let editor_info_json = serde_json::to_value(&editor_info)?; + let request_timeout = cx.update(|app| { + ProjectSettings::get_global(app) + .global_lsp_settings + .get_request_timeout() + }); + let server = cx .update(|cx| { let mut params = server.default_initialize_params(false, false, cx); @@ -640,7 +661,7 @@ impl Copilot { .get_or_insert_with(Default::default) .show_document = Some(lsp::ShowDocumentClientCapabilities { support: true }); - server.initialize(params, configuration.into(), cx) + server.initialize(params, configuration.into(), request_timeout, cx) }) .await?; @@ -648,9 +669,12 @@ impl Copilot { .context("copilot: did change configuration")?; let status = server - .request::(request::CheckStatusParams { - local_checks_only: false, - }) + .request::( + request::CheckStatusParams { + local_checks_only: false, + }, + request_timeout, + ) .await .into_response() .context("copilot: check status")?; @@ -710,11 +734,18 @@ impl Copilot { SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized => { let lsp = server.lsp.clone(); + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + let task = cx .spawn(async move |this, cx| { let sign_in = async { let flow = lsp - .request::(request::SignInParams {}) + .request::( + request::SignInParams {}, + request_timeout, + ) .await .into_response() .context("copilot sign-in")?; @@ -771,10 +802,14 @@ impl Copilot { self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx); match &self.server { CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) => { + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + let server = server.clone(); cx.background_spawn(async move { server - .request::(request::SignOutParams {}) + .request::(request::SignOutParams {}, request_timeout) .await .into_response() .context("copilot: sign in confirm")?; @@ -987,6 +1022,10 @@ impl Copilot { let hard_tabs = settings.hard_tabs; drop(settings); + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + let nes_enabled = AllLanguageSettings::get_global(cx) .edit_predictions .copilot @@ -998,13 +1037,16 @@ impl Copilot { let lsp_position = point_to_lsp(position); let nes_fut = if nes_enabled { - lsp.request::(request::NextEditSuggestionsParams { - text_document: lsp::VersionedTextDocumentIdentifier { - uri: uri.clone(), - version, + lsp.request::( + request::NextEditSuggestionsParams { + text_document: lsp::VersionedTextDocumentIdentifier { + uri: uri.clone(), + version, + }, + position: lsp_position, }, - position: lsp_position, - }) + request_timeout, + ) .map(|resp| { resp.into_response() .ok() @@ -1044,20 +1086,23 @@ impl Copilot { }; let inline_fut = lsp - .request::(request::InlineCompletionsParams { - text_document: lsp::VersionedTextDocumentIdentifier { - uri: uri.clone(), - version, - }, - position: lsp_position, - context: InlineCompletionContext { - trigger_kind: InlineCompletionTriggerKind::Automatic, + .request::( + request::InlineCompletionsParams { + text_document: lsp::VersionedTextDocumentIdentifier { + uri: uri.clone(), + version, + }, + position: lsp_position, + context: InlineCompletionContext { + trigger_kind: InlineCompletionTriggerKind::Automatic, + }, + formatting_options: Some(FormattingOptions { + tab_size, + insert_spaces: !hard_tabs, + }), }, - formatting_options: Some(FormattingOptions { - tab_size, - insert_spaces: !hard_tabs, - }), - }) + request_timeout, + ) .map(|resp| { resp.into_response() .ok() @@ -1135,13 +1180,18 @@ impl Copilot { Err(error) => return Task::ready(Err(error)), }; if let Some(command) = &completion.command { - let request = server - .lsp - .request::(lsp::ExecuteCommandParams { + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + + let request = server.lsp.request::( + lsp::ExecuteCommandParams { command: command.command.clone(), arguments: command.arguments.clone().unwrap_or_default(), ..Default::default() - }); + }, + request_timeout, + ); cx.background_spawn(async move { request .await @@ -1402,6 +1452,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_buffer_management(cx: &mut TestAppContext) { + init_test(cx); let (copilot, mut lsp) = Copilot::fake(cx); let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx)); @@ -1496,19 +1547,24 @@ mod tests { .update(cx, |copilot, cx| copilot.sign_out(cx)) .await .unwrap(); - assert_eq!( + let mut received_close_notifications = vec![ lsp.receive_notification::() .await, - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()), - } - ); - assert_eq!( lsp.receive_notification::() .await, - lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()), - } + ]; + received_close_notifications + .sort_by_key(|notification| notification.text_document.uri.clone()); + assert_eq!( + received_close_notifications, + vec![ + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()), + }, + lsp::DidCloseTextDocumentParams { + text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()), + }, + ], ); // Ensure all previously-registered buffers are re-opened when signing in. @@ -1537,29 +1593,34 @@ mod tests { ); }); - assert_eq!( + let mut received_open_notifications = vec![ lsp.receive_notification::() .await, - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - buffer_1_uri.clone(), - "plaintext".into(), - 0, - "Hello world".into() - ), - } - ); - assert_eq!( lsp.receive_notification::() .await, - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - buffer_2_uri.clone(), - "plaintext".into(), - 0, - "Goodbye".into() - ), - } + ]; + received_open_notifications + .sort_by_key(|notification| notification.text_document.uri.clone()); + assert_eq!( + received_open_notifications, + vec![ + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_2_uri.clone(), + "plaintext".into(), + 0, + "Goodbye".into() + ), + }, + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + buffer_1_uri.clone(), + "plaintext".into(), + 0, + "Hello world".into() + ), + } + ] ); // Dropping a buffer causes it to be closed on the LSP side as well. cx.update(|_| drop(buffer_2)); @@ -1630,10 +1691,13 @@ mod tests { unimplemented!() } } -} -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - zlog::init_test(); + fn init_test(cx: &mut TestAppContext) { + zlog::init_test(); + + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + }); + } } diff --git a/crates/copilot_ui/src/sign_in.rs b/crates/copilot_ui/src/sign_in.rs index b6f12ea899d08221ec02ab216cbb673056d0b3f2..dd48f95e0af6daeaf2a0a15b7b9595cb4c08aba2 100644 --- a/crates/copilot_ui/src/sign_in.rs +++ b/crates/copilot_ui/src/sign_in.rs @@ -8,6 +8,8 @@ use gpui::{ Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled, Subscription, Window, WindowBounds, WindowOptions, div, point, }; +use project::project_settings::ProjectSettings; +use settings::Settings as _; use ui::{ButtonLike, CommonAnimationExt, ConfiguredApiCard, Vector, VectorName, prelude::*}; use util::ResultExt as _; use workspace::{AppState, Toast, Workspace, notifications::NotificationId}; @@ -270,6 +272,9 @@ impl CopilotCodeVerification { cx.listener(move |this, _, _window, cx| { let command = command.clone(); let copilot_clone = copilot.clone(); + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); copilot.update(cx, |copilot, cx| { if let Some(server) = copilot.language_server() { let server = server.clone(); @@ -284,6 +289,7 @@ impl CopilotCodeVerification { .unwrap_or_default(), ..Default::default() }, + request_timeout, ) .await .into_response() diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 8e559f7039d65b60ebf7607488925b8f25468a29..997058fbd8c41fd98743fc9a783c97332bcc1ddb 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -34,7 +34,7 @@ use language::{ use language_settings::Formatter; use languages::markdown_lang; use languages::rust_lang; -use lsp::CompletionParams; +use lsp::{CompletionParams, DEFAULT_LSP_REQUEST_TIMEOUT}; use multi_buffer::{ ExcerptRange, IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey, }; @@ -48,9 +48,9 @@ use project::{ }; use serde_json::{self, json}; use settings::{ - AllLanguageSettingsContent, DelayMs, EditorSettingsContent, IndentGuideBackgroundColoring, - IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent, - SettingsStore, + AllLanguageSettingsContent, DelayMs, EditorSettingsContent, GlobalLspSettingsContent, + IndentGuideBackgroundColoring, IndentGuideColoring, InlayHintSettingsContent, + ProjectSettingsContent, SearchSettingsContent, SettingsStore, }; use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant}; use std::{ @@ -13089,26 +13089,29 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { async move { lock.lock().await; fake.server - .request::(lsp::ApplyWorkspaceEditParams { - label: None, - edit: lsp::WorkspaceEdit { - changes: Some( - [( - lsp::Uri::from_file_path(path!("/file.rs")).unwrap(), - vec![lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 0), - ), - new_text: "applied-code-action-1-command\n".into(), - }], - )] - .into_iter() - .collect(), - ), - ..Default::default() + .request::( + lsp::ApplyWorkspaceEditParams { + label: None, + edit: lsp::WorkspaceEdit { + changes: Some( + [( + lsp::Uri::from_file_path(path!("/file.rs")).unwrap(), + vec![lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "applied-code-action-1-command\n".into(), + }], + )] + .into_iter() + .collect(), + ), + ..Default::default() + }, }, - }) + DEFAULT_LSP_REQUEST_TIMEOUT, + ) .await .into_response() .unwrap(); @@ -16625,10 +16628,10 @@ async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) { lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { trigger_characters: Some(vec![".".to_string()]), - resolve_provider: Some(true), - ..Default::default() + resolve_provider: Some(false), + ..lsp::CompletionOptions::default() }), - ..Default::default() + ..lsp::ServerCapabilities::default() }, cx, ) @@ -25316,6 +25319,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex ..lsp::WorkspaceEdit::default() }, }, + DEFAULT_LSP_REQUEST_TIMEOUT, ) .await .into_response() @@ -27963,6 +27967,173 @@ async fn test_insert_snippet(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) { + use crate::inlays::inlay_hints::InlayHintRefreshReason; + use crate::inlays::inlay_hints::tests::{cached_hint_labels, init_test, visible_hint_labels}; + use settings::InlayHintSettingsContent; + use std::sync::atomic::AtomicU32; + use std::time::Duration; + + const BASE_TIMEOUT_SECS: u64 = 1; + + let request_count = Arc::new(AtomicU32::new(0)); + let closure_request_count = request_count.clone(); + + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + enabled: Some(true), + ..InlayHintSettingsContent::default() + }) + }); + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.global_lsp_settings = Some(GlobalLspSettingsContent { + request_timeout: Some(BASE_TIMEOUT_SECS), + button: Some(true), + notifications: None, + semantic_token_rules: None, + }); + }); + }); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/a"), + json!({ + "main.rs": "fn main() { let a = 5; }", + }), + ) + .await; + + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + initializer: Some(Box::new(move |fake_server| { + let request_count = closure_request_count.clone(); + fake_server.set_request_handler::( + move |params, cx| { + let request_count = request_count.clone(); + async move { + cx.background_executor() + .timer(Duration::from_secs(BASE_TIMEOUT_SECS * 2)) + .await; + let count = request_count.fetch_add(1, atomic::Ordering::Release) + 1; + assert_eq!( + params.text_document.uri, + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), + ); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, 1), + label: lsp::InlayHintLabel::String(count.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }, + ); + })), + ..FakeLspAdapter::default() + }, + ); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/a/main.rs"), cx) + }) + .await + .unwrap(); + let editor = cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx)); + + cx.executor().run_until_parked(); + let fake_server = fake_servers.next().await.unwrap(); + + cx.executor() + .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100)); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + assert!( + cached_hint_labels(editor, cx).is_empty(), + "First request should time out, no hints cached" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, _window, cx| { + editor.refresh_inlay_hints( + InlayHintRefreshReason::RefreshRequested { + server_id: fake_server.server.server_id(), + request_id: Some(1), + }, + cx, + ); + }) + .unwrap(); + cx.executor() + .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100)); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + assert!( + cached_hint_labels(editor, cx).is_empty(), + "Second request should also time out with BASE_TIMEOUT, no hints cached" + ); + }) + .unwrap(); + + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.global_lsp_settings = Some(GlobalLspSettingsContent { + request_timeout: Some(BASE_TIMEOUT_SECS * 4), + button: Some(true), + notifications: None, + semantic_token_rules: None, + }); + }); + }); + }); + editor + .update(cx, |editor, _window, cx| { + editor.refresh_inlay_hints( + InlayHintRefreshReason::RefreshRequested { + server_id: fake_server.server.server_id(), + request_id: Some(2), + }, + cx, + ); + }) + .unwrap(); + cx.executor() + .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS * 4) + Duration::from_millis(100)); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "With extended timeout (BASE * 4), hints should arrive successfully" + ); + assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx)); + }) + .unwrap(); +} + #[gpui::test] async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/inlays/inlay_hints.rs b/crates/editor/src/inlays/inlay_hints.rs index 92d6199669464e5e81ffa4e42ccec7eecfeea6fa..3922132cc83f387eec9543fe8b80b7c330c25678 100644 --- a/crates/editor/src/inlays/inlay_hints.rs +++ b/crates/editor/src/inlays/inlay_hints.rs @@ -959,7 +959,7 @@ pub mod tests { use language::{Capability, FakeLspAdapter}; use language::{Language, LanguageConfig, LanguageMatcher}; use languages::rust_lang; - use lsp::FakeLanguageServer; + use lsp::{DEFAULT_LSP_REQUEST_TIMEOUT, FakeLanguageServer}; use multi_buffer::{MultiBuffer, MultiBufferOffset}; use parking_lot::Mutex; use pretty_assertions::assert_eq; @@ -1065,7 +1065,7 @@ pub mod tests { .unwrap(); fake_server - .request::(()) + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) .await .into_response() .expect("inlay refresh request failed"); @@ -1231,9 +1231,12 @@ pub mod tests { let progress_token = 42; fake_server - .request::(lsp::WorkDoneProgressCreateParams { - token: lsp::ProgressToken::Number(progress_token), - }) + .request::( + lsp::WorkDoneProgressCreateParams { + token: lsp::ProgressToken::Number(progress_token), + }, + DEFAULT_LSP_REQUEST_TIMEOUT, + ) .await .into_response() .expect("work done progress create request failed"); @@ -1628,7 +1631,7 @@ pub mod tests { .unwrap(); fake_server - .request::(()) + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) .await .into_response() .expect("inlay refresh request failed"); @@ -1786,7 +1789,7 @@ pub mod tests { .unwrap(); fake_server - .request::(()) + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) .await .into_response() .expect("inlay refresh request failed"); @@ -1859,7 +1862,7 @@ pub mod tests { .unwrap(); fake_server - .request::(()) + .request::((), DEFAULT_LSP_REQUEST_TIMEOUT) .await .into_response() .expect("inlay refresh request failed"); diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index 058a297a974bdb210bdb90b4e3320809f6870641..d9ae428e8ffa62a0bf2756eea834bbc955f6e833 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -162,7 +162,7 @@ pub fn lsp_tasks( lsp_tasks.into_iter().collect() }) .race({ - // `lsp::LSP_REQUEST_TIMEOUT` is larger than we want for the modal to open fast + // `lsp::DEFAULT_LSP_REQUEST_TIMEOUT` is larger than we want for the modal to open fast let timer = cx.background_executor().timer(Duration::from_millis(200)); async move { timer.await; diff --git a/crates/lsp/src/input_handler.rs b/crates/lsp/src/input_handler.rs index 001ebf1fc988ebb30301887d3dadbed76326857c..61cad0c15e605bac34083078535bf11f658c7c96 100644 --- a/crates/lsp/src/input_handler.rs +++ b/crates/lsp/src/input_handler.rs @@ -103,18 +103,19 @@ impl LspStdoutHandler { id, error, result, .. }) = serde_json::from_slice(&buffer) { - let mut response_handlers = response_handlers.lock(); - if let Some(handler) = response_handlers - .as_mut() - .and_then(|handlers| handlers.remove(&id)) - { - drop(response_handlers); + let handler = { + response_handlers + .lock() + .as_mut() + .and_then(|handlers| handlers.remove(&id)) + }; + if let Some(handler) = handler { if let Some(error) = error { - handler(Err(error)); + handler(Err(error)).await; } else if let Some(result) = result { - handler(Ok(result.get().into())); + handler(Ok(result.get().into())).await; } else { - handler(Ok("null".into())); + handler(Ok("null".into())).await; } } } else { diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e2ad995b0f983b9a67d4ae5b03af851412e76337..5993bf7e373550be7c28759ab98c5842ac55be6c 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -8,6 +8,7 @@ use collections::{BTreeMap, HashMap}; use futures::{ AsyncRead, AsyncWrite, Future, FutureExt, channel::oneshot::{self, Canceled}, + future::{self, Either}, io::BufWriter, select, }; @@ -46,11 +47,22 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; -pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); +/// The default amount of time to wait while initializing or fetching LSP servers, in seconds. +/// +/// Should not be used (in favor of DEFAULT_LSP_REQUEST_TIMEOUT) and is exported solely for use inside ProjectSettings defaults. +pub const DEFAULT_LSP_REQUEST_TIMEOUT_SECS: u64 = 120; +/// A timeout representing the value of [DEFAULT_LSP_REQUEST_TIMEOUT_SECS]. +/// +/// Should **only be used** in tests and as a fallback when a corresponding config value cannot be obtained! +pub const DEFAULT_LSP_REQUEST_TIMEOUT: Duration = + Duration::from_secs(DEFAULT_LSP_REQUEST_TIMEOUT_SECS); + +/// The shutdown timeout for LSP servers (including Prettier/Copilot). const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); type NotificationHandler = Box, Value, &mut AsyncApp)>; -type ResponseHandler = Box)>; +type PendingRespondTasks = Arc>>>; +type ResponseHandler = Box) -> Task<()>>; type IoHandler = Box; /// Kind of language server stdio given to an IO handler. @@ -101,6 +113,9 @@ pub struct LanguageServer { code_action_kinds: Option>, notification_handlers: Arc>>, response_handlers: Arc>>>, + /// Tasks spawned by `on_custom_request` to compute responses. Tracked so that + /// incoming `$/cancelRequest` notifications can cancel them by dropping the task. + pending_respond_tasks: PendingRespondTasks, io_handlers: Arc>>, executor: BackgroundExecutor, #[allow(clippy::type_complexity)] @@ -378,6 +393,7 @@ pub const SEMANTIC_TOKEN_MODIFIERS: &[SemanticTokenModifier] = &[ impl LanguageServer { /// Starts a language server process. + /// A request_timeout of zero or Duration::MAX indicates an indefinite timeout. pub fn new( stderr_capture: Arc>>, server_id: LanguageServerId, @@ -473,6 +489,7 @@ impl LanguageServer { Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default())); let response_handlers = Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default()))); + let pending_respond_tasks = PendingRespondTasks::default(); let io_handlers = Arc::new(Mutex::new(HashMap::default())); let stdout_input_task = cx.spawn({ @@ -500,12 +517,14 @@ impl LanguageServer { let notification_handlers = notification_handlers.clone(); let response_handlers = response_handlers.clone(); let io_handlers = io_handlers.clone(); + let pending_respond_tasks = pending_respond_tasks.clone(); async move |cx| { Self::handle_incoming_messages( stdout, unhandled_notification_wrapper, notification_handlers, response_handlers, + pending_respond_tasks, io_handlers, cx, ) @@ -563,6 +582,7 @@ impl LanguageServer { notification_handlers, notification_tx, response_handlers, + pending_respond_tasks, io_handlers, name: server_name, version: None, @@ -596,6 +616,7 @@ impl LanguageServer { on_unhandled_notification: impl AsyncFn(NotificationOrRequest) + 'static + Send, notification_handlers: Arc>>, response_handlers: Arc>>>, + pending_respond_tasks: PendingRespondTasks, io_handlers: Arc>>, cx: &mut AsyncApp, ) -> anyhow::Result<()> @@ -618,6 +639,19 @@ impl LanguageServer { ); while let Some(msg) = input_handler.incoming_messages.next().await { + if msg.method == ::METHOD { + if let Some(params) = msg.params { + if let Ok(cancel_params) = serde_json::from_value::(params) { + let id = match cancel_params.id { + NumberOrString::Number(id) => RequestId::Int(id), + NumberOrString::String(id) => RequestId::Str(id), + }; + pending_respond_tasks.lock().remove(&id); + } + } + continue; + } + let unhandled_message = { let mut notification_handlers = notification_handlers.lock(); if let Some(handler) = notification_handlers.get_mut(msg.method.as_str()) { @@ -1018,11 +1052,12 @@ impl LanguageServer { mut self, params: InitializeParams, configuration: Arc, + timeout: Duration, cx: &App, ) -> Task>> { cx.background_spawn(async move { let response = self - .request::(params) + .request::(params, timeout) .await .into_response() .with_context(|| { @@ -1046,62 +1081,61 @@ impl LanguageServer { /// Sends a shutdown request to the language server process and prepares the [`LanguageServer`] to be dropped. pub fn shutdown(&self) -> Option> + use<>> { - if let Some(tasks) = self.io_tasks.lock().take() { - let response_handlers = self.response_handlers.clone(); - let next_id = AtomicI32::new(self.next_id.load(SeqCst)); - let outbound_tx = self.outbound_tx.clone(); - let executor = self.executor.clone(); - let notification_serializers = self.notification_tx.clone(); - let mut output_done = self.output_done_rx.lock().take().unwrap(); - let shutdown_request = Self::request_internal::( - &next_id, - &response_handlers, - &outbound_tx, - ¬ification_serializers, - &executor, - (), - ); - - let server = self.server.clone(); - let name = self.name.clone(); - let server_id = self.server_id; - let mut timer = self.executor.timer(SERVER_SHUTDOWN_TIMEOUT).fuse(); - Some(async move { - log::debug!("language server shutdown started"); - - select! { - request_result = shutdown_request.fuse() => { - match request_result { - ConnectionResult::Timeout => { - log::warn!("timeout waiting for language server {name} (id {server_id}) to shutdown"); - }, - ConnectionResult::ConnectionReset => { - log::warn!("language server {name} (id {server_id}) closed the shutdown request connection"); - }, - ConnectionResult::Result(Err(e)) => { - log::error!("Shutdown request failure, server {name} (id {server_id}): {e:#}"); - }, - ConnectionResult::Result(Ok(())) => {} - } - } + let tasks = self.io_tasks.lock().take()?; - _ = timer => { - log::info!("timeout waiting for language server {name} (id {server_id}) to shutdown"); - }, + let response_handlers = self.response_handlers.clone(); + let next_id = AtomicI32::new(self.next_id.load(SeqCst)); + let outbound_tx = self.outbound_tx.clone(); + let executor = self.executor.clone(); + let notification_serializers = self.notification_tx.clone(); + let mut output_done = self.output_done_rx.lock().take().unwrap(); + let shutdown_request = Self::request_internal::( + &next_id, + &response_handlers, + &outbound_tx, + ¬ification_serializers, + &executor, + SERVER_SHUTDOWN_TIMEOUT, + (), + ); + + let server = self.server.clone(); + let name = self.name.clone(); + let server_id = self.server_id; + let mut timer = self.executor.timer(SERVER_SHUTDOWN_TIMEOUT).fuse(); + Some(async move { + log::debug!("language server shutdown started"); + + select! { + request_result = shutdown_request.fuse() => { + match request_result { + ConnectionResult::Timeout => { + log::warn!("timeout waiting for language server {name} (id {server_id}) to shutdown"); + }, + ConnectionResult::ConnectionReset => { + log::warn!("language server {name} (id {server_id}) closed the shutdown request connection"); + }, + ConnectionResult::Result(Err(e)) => { + log::error!("Shutdown request failure, server {name} (id {server_id}): {e:#}"); + }, + ConnectionResult::Result(Ok(())) => {} + } } - response_handlers.lock().take(); - Self::notify_internal::(¬ification_serializers, ()).ok(); - notification_serializers.close(); - output_done.recv().await; - server.lock().take().map(|mut child| child.kill()); - drop(tasks); - log::debug!("language server shutdown finished"); - Some(()) - }) - } else { - None - } + _ = timer => { + log::info!("timeout waiting for language server {name} (id {server_id}) to shutdown"); + }, + } + + response_handlers.lock().take(); + Self::notify_internal::(¬ification_serializers, ()).ok(); + notification_serializers.close(); + output_done.recv().await; + server.lock().take().map(|mut child| child.kill()); + drop(tasks); + log::debug!("language server shutdown finished"); + Some(()) + }) } /// Register a handler to handle incoming LSP notifications. @@ -1192,6 +1226,7 @@ impl LanguageServer { Res: Serialize, { let outbound_tx = self.outbound_tx.clone(); + let pending_respond_tasks = self.pending_respond_tasks.clone(); let prev_handler = self.notification_handlers.lock().insert( method, Box::new(move |id, params, cx| { @@ -1199,34 +1234,36 @@ impl LanguageServer { match serde_json::from_value(params) { Ok(params) => { let response = f(params, cx); - cx.foreground_executor() - .spawn({ - let outbound_tx = outbound_tx.clone(); - async move { - let response = match response.await { - Ok(result) => Response { - jsonrpc: JSON_RPC_VERSION, - id, - value: LspResult::Ok(Some(result)), - }, - Err(error) => Response { - jsonrpc: JSON_RPC_VERSION, - id, - value: LspResult::Error(Some(Error { - code: lsp_types::error_codes::REQUEST_FAILED, - message: error.to_string(), - data: None, - })), - }, - }; - if let Some(response) = - serde_json::to_string(&response).log_err() - { - outbound_tx.try_send(response).ok(); - } + let task = cx.foreground_executor().spawn({ + let outbound_tx = outbound_tx.clone(); + let pending_respond_tasks = pending_respond_tasks.clone(); + let id = id.clone(); + async move { + let response = match response.await { + Ok(result) => Response { + jsonrpc: JSON_RPC_VERSION, + id: id.clone(), + value: LspResult::Ok(Some(result)), + }, + Err(error) => Response { + jsonrpc: JSON_RPC_VERSION, + id: id.clone(), + value: LspResult::Error(Some(Error { + code: lsp_types::error_codes::REQUEST_FAILED, + message: error.to_string(), + data: None, + })), + }, + }; + if let Some(response) = + serde_json::to_string(&response).log_err() + { + outbound_tx.try_send(response).ok(); } - }) - .detach(); + pending_respond_tasks.lock().remove(&id); + } + }); + pending_respond_tasks.lock().insert(id, task); } Err(error) => { @@ -1269,6 +1306,7 @@ impl LanguageServer { self.version.clone() } + /// Get the process name of the running language server. pub fn process_name(&self) -> &str { &self.process_name } @@ -1287,15 +1325,18 @@ impl LanguageServer { } } + /// Update the capabilities of the running language server. pub fn update_capabilities(&self, update: impl FnOnce(&mut ServerCapabilities)) { update(self.capabilities.write().deref_mut()); } + /// Get the individual configuration settings for the running language server. + /// Does not include globally applied settings (which are stored in ProjectSettings::GlobalLspSettings). pub fn configuration(&self) -> &Value { &self.configuration.settings } - /// Get the id of the running language server. + /// Get the ID of the running language server. pub fn server_id(&self) -> LanguageServerId { self.server_id } @@ -1305,17 +1346,18 @@ impl LanguageServer { self.server.lock().as_ref().map(|child| child.id()) } - /// Language server's binary information. + /// Get the binary information of the running language server. pub fn binary(&self) -> &LanguageServerBinary { &self.binary } - /// Sends a RPC request to the language server. + /// Send a RPC request to the language server. /// /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) pub fn request( &self, params: T::Params, + request_timeout: Duration, ) -> impl LspRequestFuture + use where T::Result: 'static + Send, @@ -1326,12 +1368,13 @@ impl LanguageServer { &self.outbound_tx, &self.notification_tx, &self.executor, + request_timeout, params, ) } - /// Sends a RPC request to the language server, with a custom timer, a future which when becoming - /// ready causes the request to be timed out with the future's output message. + /// Send a RPC request to the language server with a custom timer. + /// Once the attached future becomes ready, the request will time out with the provided output message. /// /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) pub fn request_with_timer>( @@ -1355,7 +1398,7 @@ impl LanguageServer { fn request_internal_with_timer( next_id: &AtomicI32, - response_handlers: &Mutex>>, + response_handlers: &Arc>>>, outbound_tx: &channel::Sender, notification_serializers: &channel::Sender, executor: &BackgroundExecutor, @@ -1374,7 +1417,7 @@ impl LanguageServer { method: T::METHOD, params, }) - .unwrap(); + .expect("LSP message should be serializable to JSON"); let (tx, rx) = oneshot::channel(); let handle_response = response_handlers @@ -1398,9 +1441,8 @@ impl LanguageServer { } Err(error) => Err(anyhow!("{}", error.message)), }; - _ = tx.send(response); + tx.send(response).ok(); }) - .detach(); }), ); }); @@ -1409,6 +1451,7 @@ impl LanguageServer { .try_send(message) .context("failed to write to language server's stdin"); + let response_handlers = Arc::clone(response_handlers); let notification_serializers = notification_serializers.downgrade(); let started = Instant::now(); LspRequest::new(id, async move { @@ -1448,7 +1491,16 @@ impl LanguageServer { message = timer.fuse() => { log::error!("Cancelled LSP request task for {method:?} id {id} {message}"); - ConnectionResult::Timeout + match response_handlers + .lock() + .as_mut() + .context("server shut down") { + Ok(handlers) => { + handlers.remove(&RequestId::Int(id)); + ConnectionResult::Timeout + } + Err(e) => ConnectionResult::Result(Err(e)), + } } } }) @@ -1456,10 +1508,11 @@ impl LanguageServer { fn request_internal( next_id: &AtomicI32, - response_handlers: &Mutex>>, + response_handlers: &Arc>>>, outbound_tx: &channel::Sender, notification_serializers: &channel::Sender, executor: &BackgroundExecutor, + request_timeout: Duration, params: T::Params, ) -> impl LspRequestFuture + use where @@ -1472,15 +1525,31 @@ impl LanguageServer { outbound_tx, notification_serializers, executor, - Self::default_request_timer(executor.clone()), + Self::request_timeout_future(executor.clone(), request_timeout), params, ) } - pub fn default_request_timer(executor: BackgroundExecutor) -> impl Future { - executor - .timer(LSP_REQUEST_TIMEOUT) - .map(|_| format!("which took over {LSP_REQUEST_TIMEOUT:?}")) + /// Internal function to return a Future from a configured timeout duration. + /// If the duration is zero or `Duration::MAX`, the returned future never completes. + fn request_timeout_future( + executor: BackgroundExecutor, + request_timeout: Duration, + ) -> impl Future { + if request_timeout == Duration::MAX || request_timeout == Duration::ZERO { + return Either::Left(future::pending::()); + } + + Either::Right( + executor + .timer(request_timeout) + .map(move |_| format!("which took over {request_timeout:?}")), + ) + } + + /// Obtain a request timer for the LSP. + pub fn request_timer(&self, timeout: Duration) -> impl Future { + Self::request_timeout_future(self.executor.clone(), timeout) } /// Sends a RPC notification to the language server. @@ -1851,12 +1920,16 @@ impl FakeLanguageServer { } /// See [`LanguageServer::request`]. - pub async fn request(&self, params: T::Params) -> ConnectionResult + pub async fn request( + &self, + params: T::Params, + timeout: Duration, + ) -> ConnectionResult where T: request::Request, T::Result: 'static + Send, { - self.server.request::(params).await + self.server.request::(params, timeout).await } /// Attempts [`Self::try_receive_notification`], unwrapping if it has not received the specified type yet. @@ -1938,18 +2011,23 @@ impl FakeLanguageServer { /// Simulate that the server has started work and notifies about its progress with the specified token. pub async fn start_progress(&self, token: impl Into) { - self.start_progress_with(token, Default::default()).await + self.start_progress_with(token, Default::default(), Default::default()) + .await } pub async fn start_progress_with( &self, token: impl Into, progress: WorkDoneProgressBegin, + request_timeout: Duration, ) { let token = token.into(); - self.request::(WorkDoneProgressCreateParams { - token: NumberOrString::String(token.clone()), - }) + self.request::( + WorkDoneProgressCreateParams { + token: NumberOrString::String(token.clone()), + }, + request_timeout, + ) .await .into_response() .unwrap(); @@ -2015,7 +2093,12 @@ mod tests { let configuration = DidChangeConfigurationParams { settings: Default::default(), }; - server.initialize(params, configuration.into(), cx) + server.initialize( + params, + configuration.into(), + DEFAULT_LSP_REQUEST_TIMEOUT, + cx, + ) }) .await .unwrap(); diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 92ddfbc8b84e46cabc03001286a4c51d87896f5a..90f512a5931fa89ac9b8a2216091f3633f872b6b 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -12,6 +12,7 @@ use std::{ ops::ControlFlow, path::{Path, PathBuf}, sync::Arc, + time::Duration, }; use util::{ paths::{PathMatcher, PathStyle}, @@ -273,6 +274,7 @@ impl Prettier { _: LanguageServerId, prettier_dir: PathBuf, _: NodeRuntime, + _: Duration, _: AsyncApp, ) -> anyhow::Result { Ok(Self::Test(TestPrettier { @@ -286,6 +288,7 @@ impl Prettier { server_id: LanguageServerId, prettier_dir: PathBuf, node: NodeRuntime, + request_timeout: Duration, mut cx: AsyncApp, ) -> anyhow::Result { use lsp::{LanguageServerBinary, LanguageServerName}; @@ -310,6 +313,7 @@ impl Prettier { arguments: vec![prettier_server.into(), prettier_dir.as_path().into()], env: None, }; + let server = LanguageServer::new( Arc::new(parking_lot::Mutex::new(None)), server_id, @@ -328,7 +332,7 @@ impl Prettier { let configuration = lsp::DidChangeConfigurationParams { settings: Default::default(), }; - executor.spawn(server.initialize(params, configuration.into(), cx)) + executor.spawn(server.initialize(params, configuration.into(), request_timeout, cx)) }) .await .context("prettier server initialization")?; @@ -344,6 +348,7 @@ impl Prettier { buffer: &Entity, buffer_path: Option, ignore_dir: Option, + request_timeout: Duration, cx: &mut AsyncApp, ) -> anyhow::Result { match self { @@ -480,7 +485,7 @@ impl Prettier { let response = local .server - .request::(params) + .request::(params, request_timeout) .await .into_response()?; let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx)); @@ -525,11 +530,11 @@ impl Prettier { } } - pub async fn clear_cache(&self) -> anyhow::Result<()> { + pub async fn clear_cache(&self, request_timeout: Duration) -> anyhow::Result<()> { match self { Self::Real(local) => local .server - .request::(()) + .request::((), request_timeout) .await .into_response() .context("prettier clear cache"), diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index bfea28006a4ece0512caeb3a4e90f9b490c54a49..e27e46e955bcecede6f0457af065b743fb9690ec 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -87,13 +87,13 @@ use language::{ }; use lsp::{ AdapterServerCapabilities, CodeActionKind, CompletionContext, CompletionOptions, - DiagnosticServerCapabilities, DiagnosticSeverity, DiagnosticTag, + DEFAULT_LSP_REQUEST_TIMEOUT, DiagnosticServerCapabilities, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, - FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LSP_REQUEST_TIMEOUT, - LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, - LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, - OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, Uri, - WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, + FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer, + LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, + LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, OneOf, + RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, Uri, WillRenameFiles, + WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -579,9 +579,14 @@ impl LocalLspStore { }; let language_server = cx .update(|cx| { + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + language_server.initialize( initialization_params, Arc::new(did_change_configuration_params.clone()), + request_timeout, cx, ) }) @@ -1302,10 +1307,7 @@ impl LocalLspStore { clangd_ext::register_notifications(lsp_store, language_server, adapter); } - fn shutdown_language_servers_on_quit( - &mut self, - _: &mut Context, - ) -> impl Future + use<> { + fn shutdown_language_servers_on_quit(&mut self) -> impl Future + use<> { let shutdown_futures = self .language_servers .drain() @@ -1587,20 +1589,24 @@ impl LocalLspStore { logger: zlog::Logger, cx: &mut AsyncApp, ) -> Result<()> { - let (adapters_and_servers, settings) = lsp_store.update(cx, |lsp_store, cx| { - buffer.handle.update(cx, |buffer, cx| { - let adapters_and_servers = lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>(); - let settings = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - .into_owned(); - (adapters_and_servers, settings) - }) - })?; + let (adapters_and_servers, settings, request_timeout) = + lsp_store.update(cx, |lsp_store, cx| { + buffer.handle.update(cx, |buffer, cx| { + let adapters_and_servers = lsp_store + .as_local() + .unwrap() + .language_servers_for_buffer(buffer, cx) + .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) + .collect::>(); + let settings = + language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + .into_owned(); + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + (adapters_and_servers, settings, request_timeout) + }) + })?; /// Apply edits to the buffer that will become part of the formatting transaction. /// Fails if the buffer has been edited since the start of that transaction. @@ -1899,7 +1905,10 @@ impl LocalLspStore { zlog::trace!(logger => "Executing {}", describe_code_action(&action)); - if let Err(err) = Self::try_resolve_code_action(server, &mut action).await { + if let Err(err) = + Self::try_resolve_code_action(server, &mut action, request_timeout) + .await + { zlog::error!( logger => "Failed to resolve {}. Error: {}", @@ -2061,125 +2070,131 @@ impl LocalLspStore { )?; } - if let Some(command) = action.lsp_action.command() { + // bail early if command is invalid + let Some(command) = action.lsp_action.command() else { + continue; + }; + + zlog::warn!( + logger => + "Executing code action command '{}'. This may cause formatting to abort unnecessarily as well as splitting formatting into two entries in the undo history", + &command.command, + ); + + let server_capabilities = server.capabilities(); + let available_commands = server_capabilities + .execute_command_provider + .as_ref() + .map(|options| options.commands.as_slice()) + .unwrap_or_default(); + if !available_commands.contains(&command.command) { zlog::warn!( logger => - "Executing code action command '{}'. This may cause formatting to abort unnecessarily as well as splitting formatting into two entries in the undo history", - &command.command, + "Cannot execute a command {} not listed in the language server capabilities of server {}", + command.command, + server.name(), ); + continue; + } - // bail early if command is invalid - let server_capabilities = server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - if !available_commands.contains(&command.command) { - zlog::warn!( - logger => - "Cannot execute a command {} not listed in the language server capabilities of server {}", - command.command, - server.name(), - ); - continue; - } + // noop so we just ensure buffer hasn't been edited since resolving code actions + extend_formatting_transaction( + buffer, + formatting_transaction_id, + cx, + |_, _| {}, + )?; + zlog::info!(logger => "Executing command {}", &command.command); - // noop so we just ensure buffer hasn't been edited since resolving code actions - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |_, _| {}, - )?; - zlog::info!(logger => "Executing command {}", &command.command); - - lsp_store.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&server.server_id()); - })?; - - let execute_command_result = server - .request::( - lsp::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..Default::default() - }, - ) - .await - .into_response(); + lsp_store.update(cx, |this, _| { + this.as_local_mut() + .unwrap() + .last_workspace_edits_by_language_server + .remove(&server.server_id()); + })?; - if execute_command_result.is_err() { - zlog::error!( - logger => - "Failed to execute command '{}' as part of {}", - &command.command, - describe_code_action(&action), - ); - continue 'actions; - } + let execute_command_result = server + .request::( + lsp::ExecuteCommandParams { + command: command.command.clone(), + arguments: command.arguments.clone().unwrap_or_default(), + ..Default::default() + }, + request_timeout, + ) + .await + .into_response(); - let mut project_transaction_command = - lsp_store.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&server.server_id()) - .unwrap_or_default() - })?; + if execute_command_result.is_err() { + zlog::error!( + logger => + "Failed to execute command '{}' as part of {}", + &command.command, + describe_code_action(&action), + ); + continue 'actions; + } - if let Some(transaction) = - project_transaction_command.0.remove(&buffer.handle) - { - zlog::trace!( - logger => - "Successfully captured {} edits that resulted from command {}", - transaction.edit_ids.len(), - &command.command, - ); - let transaction_id_project_transaction = transaction.id; - buffer.handle.update(cx, |buffer, _| { - // it may have been removed from history if push_to_history was - // false in deserialize_workspace_edit. If so push it so we - // can merge it with the format transaction - // and pop the combined transaction off the history stack - // later if push_to_history is false - if buffer.get_transaction(transaction.id).is_none() { - buffer.push_transaction(transaction, Instant::now()); - } - buffer.merge_transactions( - transaction_id_project_transaction, - formatting_transaction_id, - ); - }); - } + let mut project_transaction_command = lsp_store.update(cx, |this, _| { + this.as_local_mut() + .unwrap() + .last_workspace_edits_by_language_server + .remove(&server.server_id()) + .unwrap_or_default() + })?; - if !project_transaction_command.0.is_empty() { - let mut extra_buffers = String::new(); - for buffer in project_transaction_command.0.keys() { - buffer.read_with(cx, |b, cx| { - if let Some(path) = b.project_path(cx) { - if !extra_buffers.is_empty() { - extra_buffers.push_str(", "); - } - extra_buffers.push_str(path.path.as_unix_str()); - } - }); + if let Some(transaction) = + project_transaction_command.0.remove(&buffer.handle) + { + zlog::trace!( + logger => + "Successfully captured {} edits that resulted from command {}", + transaction.edit_ids.len(), + &command.command, + ); + let transaction_id_project_transaction = transaction.id; + buffer.handle.update(cx, |buffer, _| { + // it may have been removed from history if push_to_history was + // false in deserialize_workspace_edit. If so push it so we + // can merge it with the format transaction + // and pop the combined transaction off the history stack + // later if push_to_history is false + if buffer.get_transaction(transaction.id).is_none() { + buffer.push_transaction(transaction, Instant::now()); } - zlog::warn!( - logger => - "Unexpected edits to buffers other than the buffer actively being formatted due to command {}. Impacted buffers: [{}].", - &command.command, - extra_buffers, + buffer.merge_transactions( + transaction_id_project_transaction, + formatting_transaction_id, ); - // NOTE: if this case is hit, the proper thing to do is to for each buffer, merge the extra transaction - // into the existing transaction in project_transaction if there is one, and if there isn't one in project_transaction, - // add it so it's included, and merge it into the format transaction when its created later - } + }); } + + if project_transaction_command.0.is_empty() { + continue; + } + + let mut extra_buffers = String::new(); + for buffer in project_transaction_command.0.keys() { + buffer.read_with(cx, |b, cx| { + let Some(path) = b.project_path(cx) else { + return; + }; + + if !extra_buffers.is_empty() { + extra_buffers.push_str(", "); + } + extra_buffers.push_str(path.path.as_unix_str()); + }); + } + zlog::warn!( + logger => + "Unexpected edits to buffers other than the buffer actively being formatted due to command {}. Impacted buffers: [{}].", + &command.command, + extra_buffers, + ); + // NOTE: if this case is hit, the proper thing to do is to for each buffer, merge the extra transaction + // into the existing transaction in project_transaction if there is one, and if there isn't one in project_transaction, + // add it so it's included, and merge it into the format transaction when its created later } } } @@ -2209,6 +2224,11 @@ impl LocalLspStore { let uri = file_path_to_lsp_url(abs_path)?; let text_document = lsp::TextDocumentIdentifier::new(uri); + let request_timeout = cx.update(|app| { + ProjectSettings::get_global(app) + .global_lsp_settings + .get_request_timeout() + }); let lsp_edits = { let mut lsp_ranges = Vec::new(); this.update(cx, |_this, cx| { @@ -2228,12 +2248,15 @@ impl LocalLspStore { let mut edits = None; for range in lsp_ranges { if let Some(mut edit) = language_server - .request::(lsp::DocumentRangeFormattingParams { - text_document: text_document.clone(), - range, - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) + .request::( + lsp::DocumentRangeFormattingParams { + text_document: text_document.clone(), + range, + options: lsp_command::lsp_formatting_options(settings), + work_done_progress_params: Default::default(), + }, + request_timeout, + ) .await .into_response()? { @@ -2277,14 +2300,23 @@ impl LocalLspStore { let formatting_provider = capabilities.document_formatting_provider.as_ref(); let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); + let request_timeout = cx.update(|app| { + ProjectSettings::get_global(app) + .global_lsp_settings + .get_request_timeout() + }); + let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) { let _timer = zlog::time!(logger => "format-full"); language_server - .request::(lsp::DocumentFormattingParams { - text_document, - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) + .request::( + lsp::DocumentFormattingParams { + text_document, + options: lsp_command::lsp_formatting_options(settings), + work_done_progress_params: Default::default(), + }, + request_timeout, + ) .await .into_response()? } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) { @@ -2292,12 +2324,15 @@ impl LocalLspStore { let buffer_start = lsp::Position::new(0, 0); let buffer_end = buffer.read_with(cx, |b, _| point_to_lsp(b.max_point_utf16())); language_server - .request::(lsp::DocumentRangeFormattingParams { - text_document: text_document.clone(), - range: lsp::Range::new(buffer_start, buffer_end), - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) + .request::( + lsp::DocumentRangeFormattingParams { + text_document: text_document.clone(), + range: lsp::Range::new(buffer_start, buffer_end), + options: lsp_command::lsp_formatting_options(settings), + work_done_progress_params: Default::default(), + }, + request_timeout, + ) .await .into_response()? } else { @@ -2392,6 +2427,7 @@ impl LocalLspStore { async fn try_resolve_code_action( lang_server: &LanguageServer, action: &mut CodeAction, + request_timeout: Duration, ) -> anyhow::Result<()> { match &mut action.lsp_action { LspAction::Action(lsp_action) => { @@ -2401,7 +2437,10 @@ impl LocalLspStore { && (lsp_action.command.is_none() || lsp_action.edit.is_none()) { **lsp_action = lang_server - .request::(*lsp_action.clone()) + .request::( + *lsp_action.clone(), + request_timeout, + ) .await .into_response()?; } @@ -2409,7 +2448,7 @@ impl LocalLspStore { LspAction::CodeLens(lens) => { if !action.resolved && GetCodeLens::can_resolve_lens(&lang_server.capabilities()) { *lens = lang_server - .request::(lens.clone()) + .request::(lens.clone(), request_timeout) .await .into_response()?; } @@ -2972,14 +3011,19 @@ impl LocalLspStore { pub async fn execute_code_actions_on_server( lsp_store: &WeakEntity, language_server: &Arc, - actions: Vec, push_to_history: bool, project_transaction: &mut ProjectTransaction, cx: &mut AsyncApp, ) -> anyhow::Result<()> { + let request_timeout = cx.update(|app| { + ProjectSettings::get_global(app) + .global_lsp_settings + .get_request_timeout() + }); + for mut action in actions { - Self::try_resolve_code_action(language_server, &mut action) + Self::try_resolve_code_action(language_server, &mut action, request_timeout) .await .context("resolving a formatting code action")?; @@ -2999,48 +3043,54 @@ impl LocalLspStore { project_transaction.0.extend(new.0); } - if let Some(command) = action.lsp_action.command() { - let server_capabilities = language_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - if available_commands.contains(&command.command) { - lsp_store.update(cx, |lsp_store, _| { - if let LspStoreMode::Local(mode) = &mut lsp_store.mode { - mode.last_workspace_edits_by_language_server - .remove(&language_server.server_id()); - } - })?; + let Some(command) = action.lsp_action.command() else { + continue; + }; - language_server - .request::(lsp::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..Default::default() - }) - .await - .into_response() - .context("execute command")?; - - lsp_store.update(cx, |this, _| { - if let LspStoreMode::Local(mode) = &mut this.mode { - project_transaction.0.extend( - mode.last_workspace_edits_by_language_server - .remove(&language_server.server_id()) - .unwrap_or_default() - .0, - ) - } - })?; - } else { - log::warn!( - "Cannot execute a command {} not listed in the language server capabilities", - command.command + let server_capabilities = language_server.capabilities(); + let available_commands = server_capabilities + .execute_command_provider + .as_ref() + .map(|options| options.commands.as_slice()) + .unwrap_or_default(); + if !available_commands.contains(&command.command) { + log::warn!( + "Cannot execute a command {} not listed in the language server capabilities", + command.command + ); + continue; + } + + lsp_store.update(cx, |lsp_store, _| { + if let LspStoreMode::Local(mode) = &mut lsp_store.mode { + mode.last_workspace_edits_by_language_server + .remove(&language_server.server_id()); + } + })?; + + language_server + .request::( + lsp::ExecuteCommandParams { + command: command.command.clone(), + arguments: command.arguments.clone().unwrap_or_default(), + ..Default::default() + }, + request_timeout, + ) + .await + .into_response() + .context("execute command")?; + + lsp_store.update(cx, |this, _| { + if let LspStoreMode::Local(mode) = &mut this.mode { + project_transaction.0.extend( + mode.last_workspace_edits_by_language_server + .remove(&language_server.server_id()) + .unwrap_or_default() + .0, ) } - } + })?; } Ok(()) } @@ -4143,10 +4193,10 @@ impl LspStore { yarn, next_diagnostic_group_id: Default::default(), diagnostics: Default::default(), - _subscription: cx.on_app_quit(|this, cx| { + _subscription: cx.on_app_quit(|this, _| { this.as_local_mut() .unwrap() - .shutdown_language_servers_on_quit(cx) + .shutdown_language_servers_on_quit() }), lsp_tree: LanguageServerTree::new( manifest_tree, @@ -4943,8 +4993,13 @@ impl LspStore { if !request.check_capabilities(language_server.adapter_server_capabilities()) { return Task::ready(Ok(Default::default())); } + + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + cx.spawn(async move |this, cx| { - let lsp_request = language_server.request::(lsp_params); + let lsp_request = language_server.request::(lsp_params, request_timeout); let id = lsp_request.id(); let _cleanup = if status.is_some() { @@ -5235,14 +5290,18 @@ impl LspStore { .await }) } else if self.mode.is_local() { - let Some((_, lang_server)) = buffer_handle.update(cx, |buffer, cx| { + let Some((_, lang_server, request_timeout)) = buffer_handle.update(cx, |buffer, cx| { + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); self.language_server_for_local_buffer(buffer, action.server_id, cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) + .map(|(adapter, server)| (adapter.clone(), server.clone(), request_timeout)) }) else { return Task::ready(Ok(ProjectTransaction::default())); }; - cx.spawn(async move |this, cx| { - LocalLspStore::try_resolve_code_action(&lang_server, &mut action) + + cx.spawn(async move |this, cx| { + LocalLspStore::try_resolve_code_action(&lang_server, &mut action, request_timeout) .await .context("resolving a code action")?; if let Some(edit) = action.lsp_action.edit() @@ -5258,43 +5317,51 @@ impl LspStore { .await; } - if let Some(command) = action.lsp_action.command() { - let server_capabilities = lang_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - if available_commands.contains(&command.command) { - this.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&lang_server.server_id()); - })?; + let Some(command) = action.lsp_action.command() else { + return Ok(ProjectTransaction::default()) + }; - let _result = lang_server - .request::(lsp::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..lsp::ExecuteCommandParams::default() - }) - .await.into_response() - .context("execute command")?; + let server_capabilities = lang_server.capabilities(); + let available_commands = server_capabilities + .execute_command_provider + .as_ref() + .map(|options| options.commands.as_slice()) + .unwrap_or_default(); - return this.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&lang_server.server_id()) - .unwrap_or_default() - }); - } else { - log::warn!("Cannot execute a command {} not listed in the language server capabilities", command.command); - } + if !available_commands.contains(&command.command) { + log::warn!("Cannot execute a command {} not listed in the language server capabilities", command.command); + return Ok(ProjectTransaction::default()) } - Ok(ProjectTransaction::default()) + let request_timeout = cx.update(|app| + ProjectSettings::get_global(app) + .global_lsp_settings + .get_request_timeout() + ); + + this.update(cx, |this, _| { + this.as_local_mut() + .unwrap() + .last_workspace_edits_by_language_server + .remove(&lang_server.server_id()); + })?; + + let _result = lang_server + .request::(lsp::ExecuteCommandParams { + command: command.command.clone(), + arguments: command.arguments.clone().unwrap_or_default(), + ..lsp::ExecuteCommandParams::default() + }, request_timeout) + .await.into_response() + .context("execute command")?; + + return this.update(cx, |this, _| { + this.as_local_mut() + .unwrap() + .last_workspace_edits_by_language_server + .remove(&lang_server.server_id()) + .unwrap_or_default() + }); }) } else { Task::ready(Err(anyhow!("no upstream client and not local"))) @@ -5603,10 +5670,15 @@ impl LspStore { if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(None)); } + + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + let request_task = upstream_client.request_lsp( project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); @@ -5669,10 +5741,13 @@ impl LspStore { if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(None)); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = upstream_client.request_lsp( project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); @@ -5735,10 +5810,13 @@ impl LspStore { if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(None)); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = upstream_client.request_lsp( project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); @@ -5801,10 +5879,14 @@ impl LspStore { if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(None)); } + + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = upstream_client.request_lsp( project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); @@ -5868,10 +5950,13 @@ impl LspStore { return Task::ready(Ok(None)); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = upstream_client.request_lsp( project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); @@ -5936,10 +6021,13 @@ impl LspStore { if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(None)); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = upstream_client.request_lsp( project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); @@ -6206,6 +6294,12 @@ impl LspStore { return Task::ready(Ok(false)); } cx.spawn(async move |lsp_store, cx| { + let request_timeout = cx.update(|app| { + ProjectSettings::get_global(app) + .global_lsp_settings + .get_request_timeout() + }); + let mut did_resolve = false; if let Some((client, project_id)) = client { for completion_index in completion_indices { @@ -6259,6 +6353,7 @@ impl LspStore { server, completions.clone(), completion_index, + request_timeout, ) .await .log_err() @@ -6291,6 +6386,7 @@ impl LspStore { server: Arc, completions: Rc>>, completion_index: usize, + request_timeout: Duration, ) -> Result<()> { let server_id = server.server_id(); if !GetCompletions::can_resolve_completions(&server.capabilities()) { @@ -6313,7 +6409,10 @@ impl LspStore { server_id == *completion_server_id, "server_id mismatch, querying completion resolve for {server_id} but completion server id is {completion_server_id}" ); - server.request::(*lsp_completion.clone()) + server.request::( + *lsp_completion.clone(), + request_timeout, + ) } CompletionSource::BufferWord { .. } | CompletionSource::Dap { .. } @@ -6549,25 +6648,29 @@ impl LspStore { } }; - if let Some(transaction) = client.request(request).await?.transaction { - let transaction = language::proto::deserialize_transaction(transaction)?; - buffer_handle - .update(cx, |buffer, _| { - buffer.wait_for_edits(transaction.edit_ids.iter().copied()) - }) - .await?; - if push_to_history { - buffer_handle.update(cx, |buffer, _| { - buffer.push_transaction(transaction.clone(), Instant::now()); - buffer.finalize_last_transaction(); - }); - } - Ok(Some(transaction)) - } else { - Ok(None) + let Some(transaction) = client.request(request).await?.transaction else { + return Ok(None); + }; + + let transaction = language::proto::deserialize_transaction(transaction)?; + buffer_handle + .update(cx, |buffer, _| { + buffer.wait_for_edits(transaction.edit_ids.iter().copied()) + }) + .await?; + if push_to_history { + buffer_handle.update(cx, |buffer, _| { + buffer.push_transaction(transaction.clone(), Instant::now()); + buffer.finalize_last_transaction(); + }); } + Ok(Some(transaction)) }) } else { + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + let Some(server) = buffer_handle.update(cx, |buffer, cx| { let completion = &completions.borrow()[completion_index]; let server_id = completion.source.server_id()?; @@ -6585,6 +6688,7 @@ impl LspStore { server.clone(), completions.clone(), completion_index, + request_timeout, ) .await .context("resolving completion")?; @@ -6701,10 +6805,13 @@ impl LspStore { identifier, registration_id: None, }; + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = client.request_lsp( upstream_project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), ); @@ -7007,10 +7114,13 @@ impl LspStore { if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(HashMap::default())); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = upstream_client.request_lsp( project_id, for_server.map(|id| id.to_proto()), - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); @@ -7242,10 +7352,13 @@ impl LspStore { if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(None); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = client.request_lsp( upstream_project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), ); @@ -7306,10 +7419,13 @@ impl LspStore { if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(None); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = client.request_lsp( upstream_project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), ); @@ -7395,6 +7511,10 @@ impl LspStore { let mut requests = Vec::new(); let mut requested_servers = BTreeSet::new(); + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + for (seed, state) in local.language_server_ids.iter() { let Some(worktree_handle) = self .worktree_store @@ -7403,6 +7523,7 @@ impl LspStore { else { continue; }; + let worktree = worktree_handle.read(cx); if !worktree.is_visible() { continue; @@ -7419,15 +7540,18 @@ impl LspStore { _ => continue, }; + let supports_workspace_symbol_request = match server.capabilities().workspace_symbol_provider { Some(OneOf::Left(supported)) => supported, Some(OneOf::Right(_)) => true, None => false, }; + if !supports_workspace_symbol_request { continue; } + let worktree_handle = worktree_handle.clone(); let server_id = server.server_id(); requests.push( @@ -7437,6 +7561,7 @@ impl LspStore { query: query.to_string(), ..Default::default() }, + request_timeout, ) .map(move |response| { let lsp_symbols = response @@ -9589,36 +9714,43 @@ impl LspStore { continue; }; - if filter.should_send_will_rename(&old_uri, is_dir) { - let apply_edit = cx.spawn({ - let old_uri = old_uri.clone(); - let new_uri = new_uri.clone(); - let language_server = language_server.clone(); - async move |this, cx| { - let edit = language_server - .request::(RenameFilesParams { + if !filter.should_send_will_rename(&old_uri, is_dir) { + continue; + } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + + let apply_edit = cx.spawn({ + let old_uri = old_uri.clone(); + let new_uri = new_uri.clone(); + let language_server = language_server.clone(); + async move |this, cx| { + let edit = language_server + .request::( + RenameFilesParams { files: vec![FileRename { old_uri, new_uri }], - }) - .await - .into_response() - .context("will rename files") - .log_err() - .flatten()?; - - let transaction = LocalLspStore::deserialize_workspace_edit( - this.upgrade()?, - edit, - false, - language_server.clone(), - cx, + }, + request_timeout, ) .await - .ok()?; - Some(transaction) - } - }); - tasks.push(apply_edit); - } + .into_response() + .context("will rename files") + .log_err() + .flatten()?; + + LocalLspStore::deserialize_workspace_edit( + this.upgrade()?, + edit, + false, + language_server.clone(), + cx, + ) + .await + .ok() + } + }); + tasks.push(apply_edit); } Some(()) }) @@ -9911,6 +10043,10 @@ impl LspStore { .language_server_for_id(id) .with_context(|| format!("No language server {id}"))?; + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + anyhow::Ok(cx.background_spawn(async move { let can_resolve = server .capabilities() @@ -9920,7 +10056,10 @@ impl LspStore { .unwrap_or(false); if can_resolve { server - .request::(lsp_completion) + .request::( + lsp_completion, + request_timeout, + ) .await .into_response() .context("resolve completion item") @@ -10545,9 +10684,8 @@ impl LspStore { None => None, }; - if let Some(server) = server - && let Some(shutdown) = server.shutdown() - { + let Some(server) = server else { return }; + if let Some(shutdown) = server.shutdown() { shutdown.await; } } @@ -12961,6 +13099,18 @@ fn lsp_workspace_diagnostics_refresh( let (mut refresh_tx, mut refresh_rx) = mpsc::channel(1); refresh_tx.try_send(()).ok(); + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + + // Clamp timeout duration at a minimum of [`DEFAULT_LSP_REQUEST_TIMEOUT`] to mitigate useless loops from re-trying connections with smaller timeouts from project settings. + // This allows users to increase the duration if need be + let timeout = if request_timeout != Duration::ZERO { + request_timeout.max(DEFAULT_LSP_REQUEST_TIMEOUT) + } else { + request_timeout + }; + let workspace_query_language_server = cx.spawn(async move |lsp_store, cx| { let mut attempts = 0; let max_attempts = 50; @@ -13011,8 +13161,7 @@ fn lsp_workspace_diagnostics_refresh( }; progress_rx.try_recv().ok(); - let timer = - LanguageServer::default_request_timer(cx.background_executor().clone()).fuse(); + let timer = server.request_timer(timeout).fuse(); let progress = pin!(progress_rx.recv().fuse()); let response_result = server .request_with_timer::( diff --git a/crates/project/src/lsp_store/code_lens.rs b/crates/project/src/lsp_store/code_lens.rs index d28868359787dfabbe3c1944da0ba02f9601a400..756c2dec06ea9d60c164f177ab26e6497b1bb5d3 100644 --- a/crates/project/src/lsp_store/code_lens.rs +++ b/crates/project/src/lsp_store/code_lens.rs @@ -9,13 +9,15 @@ use futures::{ }; use gpui::{AppContext as _, AsyncApp, Context, Entity, Task}; use language::Buffer; -use lsp::{LSP_REQUEST_TIMEOUT, LanguageServerId}; +use lsp::LanguageServerId; use rpc::{TypedEnvelope, proto}; +use settings::Settings as _; use std::time::Duration; use crate::{ CodeAction, LspStore, LspStoreEvent, lsp_command::{GetCodeLens, LspCommand as _}, + project_settings::ProjectSettings, }; pub(super) type CodeLensTask = @@ -139,10 +141,13 @@ impl LspStore { if !self.is_capable_for_proto_request(buffer, &request, cx) { return Task::ready(Ok(None)); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = upstream_client.request_lsp( project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); diff --git a/crates/project/src/lsp_store/document_colors.rs b/crates/project/src/lsp_store/document_colors.rs index e0607e4f16c162d3b8a79ae52500dadd78116682..c181fbf3ef26031e12cd88fc4a3e7431aa49ae01 100644 --- a/crates/project/src/lsp_store/document_colors.rs +++ b/crates/project/src/lsp_store/document_colors.rs @@ -12,8 +12,9 @@ use language::{ Buffer, LocalFile as _, PointUtf16, point_to_lsp, proto::{deserialize_lsp_edit, serialize_lsp_edit}, }; -use lsp::{LSP_REQUEST_TIMEOUT, LanguageServerId}; +use lsp::LanguageServerId; use rpc::{TypedEnvelope, proto}; +use settings::Settings as _; use text::BufferId; use util::ResultExt as _; use worktree::File; @@ -21,6 +22,7 @@ use worktree::File; use crate::{ ColorPresentation, DocumentColor, LspStore, lsp_command::{GetDocumentColor, LspCommand as _, make_text_document_identifier}, + project_settings::ProjectSettings, }; #[derive(Debug, Default, Clone)] @@ -227,6 +229,10 @@ impl LspStore { }) else { return Task::ready(Ok(color)); }; + + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); cx.background_spawn(async move { let resolve_task = lang_server.request::( lsp::ColorPresentationParams { @@ -236,6 +242,7 @@ impl LspStore { work_done_progress_params: Default::default(), partial_result_params: Default::default(), }, + request_timeout, ); color.color_presentations = resolve_task .await @@ -267,10 +274,13 @@ impl LspStore { return Task::ready(Ok(None)); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = client.request_lsp( project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); diff --git a/crates/project/src/lsp_store/folding_ranges.rs b/crates/project/src/lsp_store/folding_ranges.rs index e5df7c85948ed5551be5b2e956534f67d7d45062..237ba9bbc02421d4772e2cfae44ce497b2de9113 100644 --- a/crates/project/src/lsp_store/folding_ranges.rs +++ b/crates/project/src/lsp_store/folding_ranges.rs @@ -10,11 +10,13 @@ use futures::future::{Shared, join_all}; use gpui::{AppContext as _, Context, Entity, SharedString, Task}; use itertools::Itertools; use language::Buffer; -use lsp::{LSP_REQUEST_TIMEOUT, LanguageServerId}; +use lsp::LanguageServerId; +use settings::Settings as _; use text::Anchor; use crate::lsp_command::{GetFoldingRanges, LspCommand as _}; use crate::lsp_store::LspStore; +use crate::project_settings::ProjectSettings; #[derive(Clone, Debug)] pub struct LspFoldingRange { @@ -162,10 +164,13 @@ impl LspStore { return Task::ready(Ok(None)); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = client.request_lsp( project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), ); diff --git a/crates/project/src/lsp_store/inlay_hints.rs b/crates/project/src/lsp_store/inlay_hints.rs index 4f400f55ca7e6692915e3afa7419dd5c91945389..a5fddd11f414f53011cd9f4d4ec72bd5d46f7c21 100644 --- a/crates/project/src/lsp_store/inlay_hints.rs +++ b/crates/project/src/lsp_store/inlay_hints.rs @@ -10,9 +10,13 @@ use language::{ }; use lsp::LanguageServerId; use rpc::{TypedEnvelope, proto}; +use settings::Settings as _; use text::{BufferId, Point}; -use crate::{InlayHint, InlayId, LspStore, LspStoreEvent, ResolveState, lsp_command::InlayHints}; +use crate::{ + InlayHint, InlayId, LspStore, LspStoreEvent, ResolveState, lsp_command::InlayHints, + project_settings::ProjectSettings, +}; pub type CacheInlayHints = HashMap>; pub type CacheInlayHintsTask = Shared>>>; @@ -269,9 +273,13 @@ impl LspStore { return Task::ready(Ok(hint)); } let buffer_snapshot = buffer.read(cx).snapshot(); + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); cx.spawn(async move |_, cx| { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), + request_timeout, ); let resolved_hint = resolve_task .await diff --git a/crates/project/src/lsp_store/semantic_tokens.rs b/crates/project/src/lsp_store/semantic_tokens.rs index fcfb7df6e6fd3b613dedb4db26dad0449be0099e..46434d7fe69af25ad3dd12e435b635e3a58d1d91 100644 --- a/crates/project/src/lsp_store/semantic_tokens.rs +++ b/crates/project/src/lsp_store/semantic_tokens.rs @@ -11,7 +11,7 @@ use futures::{ use gpui::{App, AppContext, AsyncApp, Context, Entity, ReadGlobal as _, SharedString, Task}; use itertools::Itertools; use language::{Buffer, LanguageName, language_settings::all_language_settings}; -use lsp::{AdapterServerCapabilities, LSP_REQUEST_TIMEOUT, LanguageServerId}; +use lsp::{AdapterServerCapabilities, LanguageServerId}; use rpc::{TypedEnvelope, proto}; use settings::{SemanticTokenRule, SemanticTokenRules, Settings as _, SettingsStore}; use smol::future::yield_now; @@ -206,10 +206,13 @@ impl LspStore { return Task::ready(None); } + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); let request_task = client.request_lsp( upstream_project_id, None, - LSP_REQUEST_TIMEOUT, + request_timeout, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), ); diff --git a/crates/project/src/lsp_store/vue_language_server_ext.rs b/crates/project/src/lsp_store/vue_language_server_ext.rs index 28249745403d2c6afe3532582ee92bb94de7dde7..2c32d8046daa7c5334f21e987dba9f7c40fe5a88 100644 --- a/crates/project/src/lsp_store/vue_language_server_ext.rs +++ b/crates/project/src/lsp_store/vue_language_server_ext.rs @@ -3,7 +3,8 @@ use gpui::{AppContext, WeakEntity}; use lsp::{LanguageServer, LanguageServerName}; use serde_json::Value; -use crate::LspStore; +use crate::{LspStore, ProjectSettings}; +use settings::Settings; struct VueServerRequest; struct TypescriptServerResponse; @@ -26,99 +27,107 @@ const TS_LS: LanguageServerName = LanguageServerName::new_static("typescript-lan pub fn register_requests(lsp_store: WeakEntity, language_server: &LanguageServer) { let language_server_name = language_server.name(); - if language_server_name == VUE_SERVER_NAME { - let vue_server_id = language_server.server_id(); - language_server - .on_notification::({ - move |params, cx| { - let lsp_store = lsp_store.clone(); - let Ok(Some(vue_server)) = lsp_store.read_with(cx, |this, _| { - this.language_server_for_id(vue_server_id) - }) else { - return; - }; + if language_server_name != VUE_SERVER_NAME { + return; + } + + let vue_server_id = language_server.server_id(); + language_server + .on_notification::({ + move |params, cx| { + let lsp_store = lsp_store.clone(); + let Ok(Some(vue_server)) = lsp_store.read_with(cx, |this, _| { + this.language_server_for_id(vue_server_id) + }) else { + return; + }; - let requests = params; - let target_server = match lsp_store.read_with(cx, |this, _| { - let language_server_id = this - .as_local() - .and_then(|local| { - local.language_server_ids.iter().find_map(|(seed, v)| { - [VTSLS, TS_LS].contains(&seed.name).then_some(v.id) - }) + let requests = params; + let target_server = match lsp_store.read_with(cx, |this, _| { + let language_server_id = this + .as_local() + .and_then(|local| { + local.language_server_ids.iter().find_map(|(seed, v)| { + [VTSLS, TS_LS].contains(&seed.name).then_some(v.id) }) - .context("Could not find language server")?; + }) + .context("Could not find language server")?; - this.language_server_for_id(language_server_id) - .context("language server not found") - }) { - Ok(Ok(server)) => server, - other => { - log::warn!( - "vue-language-server forwarding skipped: {other:?}. \ - Returning null tsserver responses" - ); - if !requests.is_empty() { - let null_responses = requests - .into_iter() - .map(|(id, _, _)| (id, Value::Null)) - .collect::>(); - let _ = vue_server - .notify::(null_responses); - } - return; + this.language_server_for_id(language_server_id) + .context("language server not found") + }) { + Ok(Ok(server)) => server, + other => { + log::warn!( + "vue-language-server forwarding skipped: {other:?}. \ + Returning null tsserver responses" + ); + if !requests.is_empty() { + let null_responses = requests + .into_iter() + .map(|(id, _, _)| (id, Value::Null)) + .collect::>(); + let _ = vue_server + .notify::(null_responses); } - }; + return; + } + }; - let cx = cx.clone(); - for (request_id, command, payload) in requests.into_iter() { - let target_server = target_server.clone(); - let vue_server = vue_server.clone(); - cx.background_spawn(async move { - let response = target_server - .request::( - lsp::ExecuteCommandParams { - command: "typescript.tsserverRequest".to_owned(), - arguments: vec![Value::String(command), payload], - ..Default::default() - }, - ) - .await; + let cx = cx.clone(); + let request_timeout = cx.update(|app| + ProjectSettings::get_global(app) + .global_lsp_settings + .get_request_timeout() + ); - let response_body = match response { - util::ConnectionResult::Result(Ok(result)) => match result { - Some(Value::Object(mut map)) => map - .remove("body") - .unwrap_or(Value::Object(map)), - Some(other) => other, - None => Value::Null, - }, - util::ConnectionResult::Result(Err(error)) => { - log::warn!( - "typescript.tsserverRequest failed: {error:?} for request {request_id}" - ); - Value::Null - } - other => { - log::warn!( - "typescript.tsserverRequest did not return a response: {other:?} for request {request_id}" - ); - Value::Null - } - }; + for (request_id, command, payload) in requests.into_iter() { + let target_server = target_server.clone(); + let vue_server = vue_server.clone(); + cx.background_spawn(async move { + let response = target_server + .request::( + lsp::ExecuteCommandParams { + command: "typescript.tsserverRequest".to_owned(), + arguments: vec![Value::String(command), payload], + ..Default::default() + }, request_timeout + ) + .await; - if let Err(err) = vue_server - .notify::(vec![(request_id, response_body)]) - { + let response_body = match response { + util::ConnectionResult::Result(Ok(result)) => match result { + Some(Value::Object(mut map)) => map + .remove("body") + .unwrap_or(Value::Object(map)), + Some(other) => other, + None => Value::Null, + }, + util::ConnectionResult::Result(Err(error)) => { log::warn!( - "Failed to notify vue-language-server of tsserver response: {err:?}" + "typescript.tsserverRequest failed: {error:?} for request {request_id}" ); + Value::Null } - }) - .detach(); - } + other => { + log::warn!( + "typescript.tsserverRequest did not return a response: {other:?} for request {request_id}" + ); + Value::Null + } + }; + + if let Err(err) = vue_server + .notify::(vec![(request_id, response_body)]) + { + log::warn!( + "Failed to notify vue-language-server of tsserver response: {err:?}" + ); + } + }) + .detach(); } - }) - .detach(); - } + } + }) + .detach(); } diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 0a39b36c87bdaf0dd04c08387eceee8c23eb2f39..95150fda070e488cd9d6d43238c5aa99515aa271 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -22,12 +22,13 @@ use lsp::{LanguageServer, LanguageServerId, LanguageServerName}; use node_runtime::NodeRuntime; use paths::default_prettier_dir; use prettier::Prettier; +use settings::Settings; use smol::stream::StreamExt; use util::{ResultExt, TryFutureExt, rel_path::RelPath}; use crate::{ File, PathChange, ProjectEntryId, Worktree, lsp_store::WorktreeId, - worktree_store::WorktreeStore, + project_settings::ProjectSettings, worktree_store::WorktreeStore, }; pub struct PrettierStore { @@ -280,17 +281,27 @@ impl PrettierStore { worktree_id: Option, cx: &mut Context, ) -> PrettierTask { + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + cx.spawn(async move |prettier_store, cx| { log::info!("Starting prettier at path {prettier_dir:?}"); let new_server_id = prettier_store.read_with(cx, |prettier_store, _| { prettier_store.languages.next_language_server_id() })?; - let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone()) - .await - .context("default prettier spawn") - .map(Arc::new) - .map_err(Arc::new)?; + let new_prettier = Prettier::start( + new_server_id, + prettier_dir, + node, + request_timeout, + cx.clone(), + ) + .await + .context("default prettier spawn") + .map(Arc::new) + .map_err(Arc::new)?; Self::register_new_prettier( &prettier_store, &new_prettier, @@ -454,62 +465,75 @@ impl PrettierStore { let prettier_config_file_changed = changes .iter() - .filter(|(_, _, change)| !matches!(change, PathChange::Loaded)) - .filter(|(path, _, _)| { - !path - .components() - .any(|component| component == "node_modules") + .filter(|(path, _, change)| { + !matches!(change, PathChange::Loaded) + && !path + .components() + .any(|component| component == "node_modules") }) .find(|(path, _, _)| prettier_config_files.contains(path.as_ref())); + + let Some((config_path, _, _)) = prettier_config_file_changed else { + return; + }; + let current_worktree_id = worktree.read(cx).id(); - if let Some((config_path, _, _)) = prettier_config_file_changed { - log::info!( - "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" - ); - let prettiers_to_reload = - self.prettiers_per_worktree - .get(¤t_worktree_id) - .iter() - .flat_map(|prettier_paths| prettier_paths.iter()) - .flatten() - .filter_map(|prettier_path| { - Some(( - current_worktree_id, - Some(prettier_path.clone()), - self.prettier_instances.get(prettier_path)?.clone(), - )) - }) - .chain(self.default_prettier.instance().map(|default_prettier| { - (current_worktree_id, None, default_prettier.clone()) - })) - .collect::>(); - - cx.background_spawn(async move { - let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| { - async move { - if let Some(instance) = prettier_instance.prettier { - match instance.await { - Ok(prettier) => { - prettier.clear_cache().log_err().await; - }, - Err(e) => { - match prettier_path { - Some(prettier_path) => log::error!( - "Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}" - ), - None => log::error!( - "Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}" - ), - } - }, + + log::info!( + "Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}" + ); + + let prettiers_to_reload = self + .prettiers_per_worktree + .get(¤t_worktree_id) + .iter() + .flat_map(|prettier_paths| prettier_paths.iter()) + .flatten() + .filter_map(|prettier_path| { + Some(( + current_worktree_id, + Some(prettier_path.clone()), + self.prettier_instances.get(prettier_path)?.clone(), + )) + }) + .chain( + self.default_prettier + .instance() + .map(|default_prettier| (current_worktree_id, None, default_prettier.clone())), + ) + .collect::>(); + + let request_timeout = ProjectSettings::get_global(cx) + .global_lsp_settings + .get_request_timeout(); + + cx.background_spawn(async move { + let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| { + async move { + let Some(instance) = prettier_instance.prettier else { + return + }; + + match instance.await { + Ok(prettier) => { + prettier.clear_cache(request_timeout).log_err().await; + }, + Err(e) => { + match prettier_path { + Some(prettier_path) => log::error!( + "Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}" + ), + None => log::error!( + "Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}" + ), } - } + }, } - })) - .await; - }) - .detach(); - } + } + })) + .await; + }) + .detach(); } pub fn install_default_prettier( @@ -735,6 +759,12 @@ pub(super) async fn format_with_prettier( None => "default prettier instance".to_string(), }; + let request_timeout: Duration = cx.update(|app| { + ProjectSettings::get_global(app) + .global_lsp_settings + .get_request_timeout() + }); + match prettier_task.await { Ok(prettier) => { let buffer_path = buffer.update(cx, |buffer, cx| { @@ -742,7 +772,7 @@ pub(super) async fn format_with_prettier( }); let format_result = prettier - .format(buffer, buffer_path, ignore_dir, cx) + .format(buffer, buffer_path, ignore_dir, request_timeout, cx) .await .with_context(|| format!("{} failed to format buffer", prettier_description)); diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 3ab4297e1e7bccf8a9c3af4f702cfa7415e70ee9..c295938ef56f69101a7210db63f4f7ecdc8517e4 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -5,7 +5,7 @@ use dap::adapters::DebugAdapterName; use fs::Fs; use futures::StreamExt as _; use gpui::{AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Subscription, Task}; -use lsp::LanguageServerName; +use lsp::{DEFAULT_LSP_REQUEST_TIMEOUT_SECS, LanguageServerName}; use paths::{ EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path, local_tasks_file_relative_path, local_vscode_launch_file_relative_path, @@ -118,18 +118,46 @@ impl From for NodeBinarySettings { } /// Common language server settings. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(default)] pub struct GlobalLspSettings { /// Whether to show the LSP servers button in the status bar. /// /// Default: `true` pub button: bool, + /// The maximum amount of time to wait for responses from language servers, in seconds. + /// A value of `0` will result in no timeout being applied (causing all LSP responses to wait + /// indefinitely until completed). + /// This should not be used outside of serialization/de-serialization in favor of get_request_timeout. + /// + /// Default: `120` + pub request_timeout: u64, pub notifications: LspNotificationSettings, /// Rules for highlighting semantic tokens. pub semantic_token_rules: SemanticTokenRules, } +impl Default for GlobalLspSettings { + fn default() -> Self { + Self { + button: true, + request_timeout: DEFAULT_LSP_REQUEST_TIMEOUT_SECS, + notifications: LspNotificationSettings::default(), + semantic_token_rules: SemanticTokenRules::default(), + } + } +} + +impl GlobalLspSettings { + /// Returns the timeout duration for LSP-related interactions, or Duration::ZERO if no timeout should be applied. + /// Zero durations are treated as no timeout by language servers, so code using this in an async context can + /// simply call unwrap_or_default. + pub const fn get_request_timeout(&self) -> Duration { + Duration::from_secs(self.request_timeout) + } +} + #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)] #[serde(tag = "source", rename_all = "snake_case")] pub struct LspNotificationSettings { @@ -140,6 +168,14 @@ pub struct LspNotificationSettings { pub dismiss_timeout_ms: Option, } +impl Default for LspNotificationSettings { + fn default() -> Self { + Self { + dismiss_timeout_ms: Some(5000), + } + } +} + #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)] #[serde(tag = "source", rename_all = "snake_case")] pub enum ContextServerSettings { @@ -629,6 +665,12 @@ impl Settings for ProjectSettings { .unwrap() .button .unwrap(), + request_timeout: content + .global_lsp_settings + .as_ref() + .unwrap() + .request_timeout + .unwrap(), notifications: LspNotificationSettings { dismiss_timeout_ms: content .global_lsp_settings diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index 95163ca863330afdda645647dc5dfe30f3d7141e..f70f2589d158705b96928473b10fc7737632fd6c 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -48,9 +48,9 @@ use language::{ markdown_lang, rust_lang, tree_sitter_typescript, }; use lsp::{ - CodeActionKind, DiagnosticSeverity, DocumentChanges, FileOperationFilter, LanguageServerId, - LanguageServerName, NumberOrString, TextDocumentEdit, Uri, WillRenameFiles, - notification::DidRenameFiles, + CodeActionKind, DEFAULT_LSP_REQUEST_TIMEOUT, DiagnosticSeverity, DocumentChanges, + FileOperationFilter, LanguageServerId, LanguageServerName, NumberOrString, TextDocumentEdit, + Uri, WillRenameFiles, notification::DidRenameFiles, }; use parking_lot::Mutex; use paths::{config_dir, global_gitignore_path, tasks_file}; @@ -2202,49 +2202,52 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon // Keep track of the FS events reported to the language server. let file_changes = Arc::new(Mutex::new(Vec::new())); fake_server - .request::(lsp::RegistrationParams { - registrations: vec![lsp::Registration { - id: Default::default(), - method: "workspace/didChangeWatchedFiles".to_string(), - register_options: serde_json::to_value( - lsp::DidChangeWatchedFilesRegistrationOptions { - watchers: vec![ - lsp::FileSystemWatcher { - glob_pattern: lsp::GlobPattern::String( - path!("/the-root/Cargo.toml").to_string(), - ), - kind: None, - }, - lsp::FileSystemWatcher { - glob_pattern: lsp::GlobPattern::String( - path!("/the-root/src/*.{rs,c}").to_string(), - ), - kind: None, - }, - lsp::FileSystemWatcher { - glob_pattern: lsp::GlobPattern::String( - path!("/the-root/target/y/**/*.rs").to_string(), - ), - kind: None, - }, - lsp::FileSystemWatcher { - glob_pattern: lsp::GlobPattern::String( - path!("/the/stdlib/src/**/*.rs").to_string(), - ), - kind: None, - }, - lsp::FileSystemWatcher { - glob_pattern: lsp::GlobPattern::String( - path!("**/Cargo.lock").to_string(), - ), - kind: None, - }, - ], - }, - ) - .ok(), - }], - }) + .request::( + lsp::RegistrationParams { + registrations: vec![lsp::Registration { + id: Default::default(), + method: "workspace/didChangeWatchedFiles".to_string(), + register_options: serde_json::to_value( + lsp::DidChangeWatchedFilesRegistrationOptions { + watchers: vec![ + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + path!("/the-root/Cargo.toml").to_string(), + ), + kind: None, + }, + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + path!("/the-root/src/*.{rs,c}").to_string(), + ), + kind: None, + }, + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + path!("/the-root/target/y/**/*.rs").to_string(), + ), + kind: None, + }, + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + path!("/the/stdlib/src/**/*.rs").to_string(), + ), + kind: None, + }, + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + path!("**/Cargo.lock").to_string(), + ), + kind: None, + }, + ], + }, + ) + .ok(), + }], + }, + DEFAULT_LSP_REQUEST_TIMEOUT, + ) .await .into_response() .unwrap(); @@ -3025,6 +3028,7 @@ async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) { cancellable: Some(false), ..Default::default() }, + DEFAULT_LSP_REQUEST_TIMEOUT, ) .await; // Ensure progress notification is fully processed before starting the next one @@ -3037,6 +3041,7 @@ async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) { cancellable: Some(true), ..Default::default() }, + DEFAULT_LSP_REQUEST_TIMEOUT, ) .await; // Ensure progress notification is fully processed before cancelling @@ -4672,6 +4677,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { ..Default::default() }, }, + DEFAULT_LSP_REQUEST_TIMEOUT, ) .await .into_response() diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 084d92c43188ecb66852b497d29afeb87507a04f..b1b0d83a871a9659f22a813a3d9b4c65424c45ab 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -17,7 +17,10 @@ use language::{ Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, language_settings::{AllLanguageSettings, language_settings}, }; -use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName}; +use lsp::{ + CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT, + LanguageServerName, +}; use node_runtime::NodeRuntime; use project::{ ProgressToken, Project, @@ -816,6 +819,7 @@ async fn test_remote_cancel_language_server_work( cancellable: Some(false), ..Default::default() }, + DEFAULT_LSP_REQUEST_TIMEOUT, ) .await; @@ -827,6 +831,7 @@ async fn test_remote_cancel_language_server_work( cancellable: Some(true), ..Default::default() }, + DEFAULT_LSP_REQUEST_TIMEOUT, ) .await; @@ -860,6 +865,7 @@ async fn test_remote_cancel_language_server_work( cancellable: Some(true), ..Default::default() }, + DEFAULT_LSP_REQUEST_TIMEOUT, ) .await; diff --git a/crates/settings_content/src/project.rs b/crates/settings_content/src/project.rs index d033d56d9c76aa3c0126a031214483b6bd8519d4..1bcacbd325460457d285ec0577db4d37e25fb17a 100644 --- a/crates/settings_content/src/project.rs +++ b/crates/settings_content/src/project.rs @@ -200,6 +200,11 @@ pub struct GlobalLspSettingsContent { /// /// Default: `true` pub button: Option, + /// The maximum amount of time to wait for responses from language servers, in seconds. + /// A value of `0` will result in no timeout being applied (causing all LSP responses to wait indefinitely until completed). + /// + /// Default: `120` + pub request_timeout: Option, /// Settings for language server notifications pub notifications: Option, /// Rules for rendering LSP semantic tokens. diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index 504d43d8c8c263d8fecac1acd29bf3c90e0d9403..72f5bace87e5f41fddcbac043465b455d40a91bd 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -1599,6 +1599,7 @@ While other options may be changed at a runtime and should be placed under `sett { "global_lsp_settings": { "button": true, + "request_timeout": 120, "notifications": { // Timeout in milliseconds for automatically dismissing language server notifications. // Set to 0 to disable auto-dismiss. @@ -1611,6 +1612,7 @@ While other options may be changed at a runtime and should be placed under `sett **Options** - `button`: Whether to show the LSP status button in the status bar +- `request_timeout`: The maximum amount of time to wait for responses from language servers, in seconds. A value of `0` will result in no timeout being applied (causing all LSP responses to wait indefinitely until completed). Default: `120` - `notifications`: Notification-related settings. - `dismiss_timeout_ms`: Timeout in milliseconds for automatically dismissing language server notifications. Set to 0 to disable auto-dismiss.