Add configurable LSP timeout setting (#44745)

Bertie690 , Kirill Bulatov , and Kirill Bulatov created

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 <mail4score@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>

Change summary

Cargo.lock                                              |   1 
assets/settings/default.json                            |   5 
crates/collab/tests/integration/editor_tests.rs         |  18 
crates/collab/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                         | 704 ++++++----
crates/project/src/lsp_store/code_lens.rs               |   9 
crates/project/src/lsp_store/document_colors.rs         |  14 
crates/project/src/lsp_store/folding_ranges.rs          |   9 
crates/project/src/lsp_store/inlay_hints.rs             |  10 
crates/project/src/lsp_store/semantic_tokens.rs         |   7 
crates/project/src/lsp_store/vue_language_server_ext.rs | 179 +-
crates/project/src/prettier_store.rs                    | 146 +
crates/project/src/project_settings.rs                  |  46 
crates/project/tests/integration/project_tests.rs       |  98 
crates/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, 1,328 insertions(+), 722 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3736,6 +3736,7 @@ dependencies = [
  "node_runtime",
  "parking_lot",
  "paths",
+ "pretty_assertions",
  "project",
  "rpc",
  "semver",

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.

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::<lsp::request::InlayHintRefreshRequest>(())
+        .request::<lsp::request::InlayHintRefreshRequest>((), 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::<lsp::request::InlayHintRefreshRequest>(())
+        .request::<lsp::request::InlayHintRefreshRequest>((), 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::<lsp::request::WorkspaceDiagnosticRefresh>(())
+        .request::<lsp::request::WorkspaceDiagnosticRefresh>((), 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::<lsp::request::SemanticTokensRefresh>(())
+        .request::<lsp::request::SemanticTokensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
         .await
         .into_response()
         .expect("semantic tokens refresh request failed");

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::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
-            token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
-        })
+        .request::<lsp::request::WorkDoneProgressCreate>(
+            lsp::WorkDoneProgressCreateParams {
+                token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
+            },
+            DEFAULT_LSP_REQUEST_TIMEOUT,
+        )
         .await
         .into_response()
         .unwrap();

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

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::<GlobalCopilotAuth>().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::CheckStatus>(request::CheckStatusParams {
-                                local_checks_only: false,
-                            })
+                            .request::<request::CheckStatus>(
+                                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::CheckStatus>(request::CheckStatusParams {
-                                        local_checks_only: false,
-                                    })
+                                    .request::<request::CheckStatus>(
+                                        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::CheckStatus>(request::CheckStatusParams {
-                    local_checks_only: false,
-                })
+                .request::<request::CheckStatus>(
+                    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::SignIn>(request::SignInParams {})
+                                    .request::<request::SignIn>(
+                                        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::SignOut>(request::SignOutParams {})
+                        .request::<request::SignOut>(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::<NextEditSuggestions>(request::NextEditSuggestionsParams {
-                    text_document: lsp::VersionedTextDocumentIdentifier {
-                        uri: uri.clone(),
-                        version,
+                lsp.request::<NextEditSuggestions>(
+                    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::<InlineCompletions>(request::InlineCompletionsParams {
-                    text_document: lsp::VersionedTextDocumentIdentifier {
-                        uri: uri.clone(),
-                        version,
-                    },
-                    position: lsp_position,
-                    context: InlineCompletionContext {
-                        trigger_kind: InlineCompletionTriggerKind::Automatic,
+                .request::<InlineCompletions>(
+                    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::ExecuteCommand>(lsp::ExecuteCommandParams {
+            let request_timeout = ProjectSettings::get_global(cx)
+                .global_lsp_settings
+                .get_request_timeout();
+
+            let request = server.lsp.request::<lsp::ExecuteCommand>(
+                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::<lsp::notification::DidCloseTextDocument>()
                 .await,
-            lsp::DidCloseTextDocumentParams {
-                text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
-            }
-        );
-        assert_eq!(
             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
                 .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::<lsp::notification::DidOpenTextDocument>()
                 .await,
-            lsp::DidOpenTextDocumentParams {
-                text_document: lsp::TextDocumentItem::new(
-                    buffer_1_uri.clone(),
-                    "plaintext".into(),
-                    0,
-                    "Hello world".into()
-                ),
-            }
-        );
-        assert_eq!(
             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
                 .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);
+        });
+    }
 }

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()

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::request::ApplyWorkspaceEdit>(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::request::ApplyWorkspaceEdit>(
+                        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::<lsp::request::InlayHintRequest, _, _>(
+                    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, |_| {});

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::<lsp::request::InlayHintRefreshRequest>(())
+            .request::<lsp::request::InlayHintRefreshRequest>((), 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::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
-                token: lsp::ProgressToken::Number(progress_token),
-            })
+            .request::<lsp::request::WorkDoneProgressCreate>(
+                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::<lsp::request::InlayHintRefreshRequest>(())
+            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
             .await
             .into_response()
             .expect("inlay refresh request failed");
@@ -1786,7 +1789,7 @@ pub mod tests {
             .unwrap();
 
         fake_server
-            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
             .await
             .into_response()
             .expect("inlay refresh request failed");
@@ -1859,7 +1862,7 @@ pub mod tests {
             .unwrap();
 
         fake_server
-            .request::<lsp::request::InlayHintRefreshRequest>(())
+            .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
             .await
             .into_response()
             .expect("inlay refresh request failed");

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;

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 {

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<dyn Send + FnMut(Option<RequestId>, Value, &mut AsyncApp)>;
-type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
+type PendingRespondTasks = Arc<Mutex<HashMap<RequestId, Task<()>>>>;
+type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>) -> Task<()>>;
 type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
 
 /// Kind of language server stdio given to an IO handler.
@@ -101,6 +113,9 @@ pub struct LanguageServer {
     code_action_kinds: Option<Vec<CodeActionKind>>,
     notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
     response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
+    /// 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<Mutex<HashMap<i32, IoHandler>>>,
     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<Mutex<Option<String>>>,
         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<Mutex<HashMap<&'static str, NotificationHandler>>>,
         response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
+        pending_respond_tasks: PendingRespondTasks,
         io_handlers: Arc<Mutex<HashMap<i32, IoHandler>>>,
         cx: &mut AsyncApp,
     ) -> anyhow::Result<()>
@@ -618,6 +639,19 @@ impl LanguageServer {
         );
 
         while let Some(msg) = input_handler.incoming_messages.next().await {
+            if msg.method == <notification::Cancel as notification::Notification>::METHOD {
+                if let Some(params) = msg.params {
+                    if let Ok(cancel_params) = serde_json::from_value::<CancelParams>(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<DidChangeConfigurationParams>,
+        timeout: Duration,
         cx: &App,
     ) -> Task<Result<Arc<Self>>> {
         cx.background_spawn(async move {
             let response = self
-                .request::<request::Initialize>(params)
+                .request::<request::Initialize>(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<impl 'static + Send + Future<Output = 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::<request::Shutdown>(
-                &next_id,
-                &response_handlers,
-                &outbound_tx,
-                &notification_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::<request::Shutdown>(
+            &next_id,
+            &response_handlers,
+            &outbound_tx,
+            &notification_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::<notification::Exit>(&notification_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::<notification::Exit>(&notification_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<T: request::Request>(
         &self,
         params: T::Params,
+        request_timeout: Duration,
     ) -> impl LspRequestFuture<T::Result> + use<T>
     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<T: request::Request, U: Future<Output = String>>(
@@ -1355,7 +1398,7 @@ impl LanguageServer {
 
     fn request_internal_with_timer<T, U>(
         next_id: &AtomicI32,
-        response_handlers: &Mutex<Option<HashMap<RequestId, ResponseHandler>>>,
+        response_handlers: &Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
         outbound_tx: &channel::Sender<String>,
         notification_serializers: &channel::Sender<NotificationSerializer>,
         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<T>(
         next_id: &AtomicI32,
-        response_handlers: &Mutex<Option<HashMap<RequestId, ResponseHandler>>>,
+        response_handlers: &Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
         outbound_tx: &channel::Sender<String>,
         notification_serializers: &channel::Sender<NotificationSerializer>,
         executor: &BackgroundExecutor,
+        request_timeout: Duration,
         params: T::Params,
     ) -> impl LspRequestFuture<T::Result> + use<T>
     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<Output = String> {
-        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<Output = String> {
+        if request_timeout == Duration::MAX || request_timeout == Duration::ZERO {
+            return Either::Left(future::pending::<String>());
+        }
+
+        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<Output = String> {
+        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<T>(&self, params: T::Params) -> ConnectionResult<T::Result>
+    pub async fn request<T>(
+        &self,
+        params: T::Params,
+        timeout: Duration,
+    ) -> ConnectionResult<T::Result>
     where
         T: request::Request,
         T::Result: 'static + Send,
     {
-        self.server.request::<T>(params).await
+        self.server.request::<T>(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<String>) {
-        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<String>,
         progress: WorkDoneProgressBegin,
+        request_timeout: Duration,
     ) {
         let token = token.into();
-        self.request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
-            token: NumberOrString::String(token.clone()),
-        })
+        self.request::<request::WorkDoneProgressCreate>(
+            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();

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<Self> {
         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<Self> {
         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>,
         buffer_path: Option<PathBuf>,
         ignore_dir: Option<PathBuf>,
+        request_timeout: Duration,
         cx: &mut AsyncApp,
     ) -> anyhow::Result<Diff> {
         match self {
@@ -480,7 +485,7 @@ impl Prettier {
 
                 let response = local
                     .server
-                    .request::<Format>(params)
+                    .request::<Format>(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::<ClearCache>(())
+                .request::<ClearCache>((), request_timeout)
                 .await
                 .into_response()
                 .context("prettier clear cache"),

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<LspStore>,
-    ) -> impl Future<Output = ()> + use<> {
+    fn shutdown_language_servers_on_quit(&mut self) -> impl Future<Output = ()> + 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::<Vec<_>>();
-                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::<Vec<_>>();
+                    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::request::ExecuteCommand>(
-                                    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::request::ExecuteCommand>(
+                                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::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
-                        text_document: text_document.clone(),
-                        range,
-                        options: lsp_command::lsp_formatting_options(settings),
-                        work_done_progress_params: Default::default(),
-                    })
+                    .request::<lsp::request::RangeFormatting>(
+                        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::request::Formatting>(lsp::DocumentFormattingParams {
-                    text_document,
-                    options: lsp_command::lsp_formatting_options(settings),
-                    work_done_progress_params: Default::default(),
-                })
+                .request::<lsp::request::Formatting>(
+                    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::request::RangeFormatting>(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::request::RangeFormatting>(
+                    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::request::CodeActionResolveRequest>(*lsp_action.clone())
+                        .request::<lsp::request::CodeActionResolveRequest>(
+                            *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::<lsp::request::CodeLensResolve>(lens.clone())
+                        .request::<lsp::request::CodeLensResolve>(lens.clone(), request_timeout)
                         .await
                         .into_response()?;
                 }
@@ -2972,14 +3011,19 @@ impl LocalLspStore {
     pub async fn execute_code_actions_on_server(
         lsp_store: &WeakEntity<LspStore>,
         language_server: &Arc<LanguageServer>,
-
         actions: Vec<CodeAction>,
         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::request::ExecuteCommand>(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::request::ExecuteCommand>(
+                    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::<R::LspRequest>(lsp_params);
+            let lsp_request = language_server.request::<R::LspRequest>(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::request::ExecuteCommand>(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::request::ExecuteCommand>(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<lsp::LanguageServer>,
         completions: Rc<RefCell<Box<[Completion]>>>,
         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::request::ResolveCompletionItem>(*lsp_completion.clone())
+                    server.request::<lsp::request::ResolveCompletionItem>(
+                        *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::<WillRenameFiles>(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::<WillRenameFiles>(
+                                    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(())
             })

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)),
             );

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::request::ColorPresentationRequest>(
                     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)),
             );

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)),
             );

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<LanguageServerId, Vec<(InlayId, InlayHint)>>;
 pub type CacheInlayHintsTask = Shared<Task<Result<CacheInlayHints, Arc<anyhow::Error>>>>;
@@ -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::<lsp::request::InlayHintResolveRequest>(
                     InlayHints::project_to_lsp_hint(hint, &buffer_snapshot),
+                    request_timeout,
                 );
                 let resolved_hint = resolve_task
                     .await

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)),
             );

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<LspStore>, 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::<VueServerRequest, _>({
-                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::<VueServerRequest, _>({
+            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::<Vec<_>>();
-                                let _ = vue_server
-                                    .notify::<TypescriptServerResponse>(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::<Vec<_>>();
+                            let _ = vue_server
+                                .notify::<TypescriptServerResponse>(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::request::ExecuteCommand>(
-                                    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::request::ExecuteCommand>(
+                                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::<TypescriptServerResponse>(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::<TypescriptServerResponse>(vec![(request_id, response_body)])
+                        {
+                            log::warn!(
+                                "Failed to notify vue-language-server of tsserver response: {err:?}"
+                            );
+                        }
+                    })
+                    .detach();
                 }
-            })
-            .detach();
-    }
+            }
+        })
+        .detach();
 }

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<WorktreeId>,
         cx: &mut Context<Self>,
     ) -> 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(&current_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::<Vec<_>>();
-
-            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(&current_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::<Vec<_>>();
+
+        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));
 

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<settings::NodeBinarySettings> 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<u64>,
 }
 
+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

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::request::RegisterCapability>(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::request::RegisterCapability>(
+            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()

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;
 

crates/settings_content/src/project.rs 🔗

@@ -200,6 +200,11 @@ pub struct GlobalLspSettingsContent {
     ///
     /// Default: `true`
     pub button: Option<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).
+    ///
+    /// Default: `120`
+    pub request_timeout: Option<u64>,
     /// Settings for language server notifications
     pub notifications: Option<LspNotificationSettingsContent>,
     /// Rules for rendering LSP semantic tokens.

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.