Cargo.lock 🔗
@@ -3736,6 +3736,7 @@ dependencies = [
"node_runtime",
"parking_lot",
"paths",
+ "pretty_assertions",
"project",
"rpc",
"semver",
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>
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(-)
@@ -3736,6 +3736,7 @@ dependencies = [
"node_runtime",
"parking_lot",
"paths",
+ "pretty_assertions",
"project",
"rpc",
"semver",
@@ -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.
@@ -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");
@@ -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();
@@ -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
@@ -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);
+ });
+ }
}
@@ -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()
@@ -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, |_| {});
@@ -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");
@@ -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;
@@ -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 {
@@ -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,
- ¬ification_serializers,
- &executor,
- (),
- );
-
- let server = self.server.clone();
- let name = self.name.clone();
- let server_id = self.server_id;
- let mut timer = self.executor.timer(SERVER_SHUTDOWN_TIMEOUT).fuse();
- Some(async move {
- log::debug!("language server shutdown started");
-
- select! {
- request_result = shutdown_request.fuse() => {
- match request_result {
- ConnectionResult::Timeout => {
- log::warn!("timeout waiting for language server {name} (id {server_id}) to shutdown");
- },
- ConnectionResult::ConnectionReset => {
- log::warn!("language server {name} (id {server_id}) closed the shutdown request connection");
- },
- ConnectionResult::Result(Err(e)) => {
- log::error!("Shutdown request failure, server {name} (id {server_id}): {e:#}");
- },
- ConnectionResult::Result(Ok(())) => {}
- }
- }
+ let tasks = self.io_tasks.lock().take()?;
- _ = timer => {
- log::info!("timeout waiting for language server {name} (id {server_id}) to shutdown");
- },
+ let response_handlers = self.response_handlers.clone();
+ let next_id = AtomicI32::new(self.next_id.load(SeqCst));
+ let outbound_tx = self.outbound_tx.clone();
+ let executor = self.executor.clone();
+ let notification_serializers = self.notification_tx.clone();
+ let mut output_done = self.output_done_rx.lock().take().unwrap();
+ let shutdown_request = Self::request_internal::<request::Shutdown>(
+ &next_id,
+ &response_handlers,
+ &outbound_tx,
+ ¬ification_serializers,
+ &executor,
+ SERVER_SHUTDOWN_TIMEOUT,
+ (),
+ );
+
+ let server = self.server.clone();
+ let name = self.name.clone();
+ let server_id = self.server_id;
+ let mut timer = self.executor.timer(SERVER_SHUTDOWN_TIMEOUT).fuse();
+ Some(async move {
+ log::debug!("language server shutdown started");
+
+ select! {
+ request_result = shutdown_request.fuse() => {
+ match request_result {
+ ConnectionResult::Timeout => {
+ log::warn!("timeout waiting for language server {name} (id {server_id}) to shutdown");
+ },
+ ConnectionResult::ConnectionReset => {
+ log::warn!("language server {name} (id {server_id}) closed the shutdown request connection");
+ },
+ ConnectionResult::Result(Err(e)) => {
+ log::error!("Shutdown request failure, server {name} (id {server_id}): {e:#}");
+ },
+ ConnectionResult::Result(Ok(())) => {}
+ }
}
- response_handlers.lock().take();
- Self::notify_internal::<notification::Exit>(¬ification_serializers, ()).ok();
- notification_serializers.close();
- output_done.recv().await;
- server.lock().take().map(|mut child| child.kill());
- drop(tasks);
- log::debug!("language server shutdown finished");
- Some(())
- })
- } else {
- None
- }
+ _ = timer => {
+ log::info!("timeout waiting for language server {name} (id {server_id}) to shutdown");
+ },
+ }
+
+ response_handlers.lock().take();
+ Self::notify_internal::<notification::Exit>(¬ification_serializers, ()).ok();
+ notification_serializers.close();
+ output_done.recv().await;
+ server.lock().take().map(|mut child| child.kill());
+ drop(tasks);
+ log::debug!("language server shutdown finished");
+ Some(())
+ })
}
/// Register a handler to handle incoming LSP notifications.
@@ -1192,6 +1226,7 @@ impl LanguageServer {
Res: Serialize,
{
let outbound_tx = self.outbound_tx.clone();
+ let pending_respond_tasks = self.pending_respond_tasks.clone();
let prev_handler = self.notification_handlers.lock().insert(
method,
Box::new(move |id, params, cx| {
@@ -1199,34 +1234,36 @@ impl LanguageServer {
match serde_json::from_value(params) {
Ok(params) => {
let response = f(params, cx);
- cx.foreground_executor()
- .spawn({
- let outbound_tx = outbound_tx.clone();
- async move {
- let response = match response.await {
- Ok(result) => Response {
- jsonrpc: JSON_RPC_VERSION,
- id,
- value: LspResult::Ok(Some(result)),
- },
- Err(error) => Response {
- jsonrpc: JSON_RPC_VERSION,
- id,
- value: LspResult::Error(Some(Error {
- code: lsp_types::error_codes::REQUEST_FAILED,
- message: error.to_string(),
- data: None,
- })),
- },
- };
- if let Some(response) =
- serde_json::to_string(&response).log_err()
- {
- outbound_tx.try_send(response).ok();
- }
+ let task = cx.foreground_executor().spawn({
+ let outbound_tx = outbound_tx.clone();
+ let pending_respond_tasks = pending_respond_tasks.clone();
+ let id = id.clone();
+ async move {
+ let response = match response.await {
+ Ok(result) => Response {
+ jsonrpc: JSON_RPC_VERSION,
+ id: id.clone(),
+ value: LspResult::Ok(Some(result)),
+ },
+ Err(error) => Response {
+ jsonrpc: JSON_RPC_VERSION,
+ id: id.clone(),
+ value: LspResult::Error(Some(Error {
+ code: lsp_types::error_codes::REQUEST_FAILED,
+ message: error.to_string(),
+ data: None,
+ })),
+ },
+ };
+ if let Some(response) =
+ serde_json::to_string(&response).log_err()
+ {
+ outbound_tx.try_send(response).ok();
}
- })
- .detach();
+ pending_respond_tasks.lock().remove(&id);
+ }
+ });
+ pending_respond_tasks.lock().insert(id, task);
}
Err(error) => {
@@ -1269,6 +1306,7 @@ impl LanguageServer {
self.version.clone()
}
+ /// Get the process name of the running language server.
pub fn process_name(&self) -> &str {
&self.process_name
}
@@ -1287,15 +1325,18 @@ impl LanguageServer {
}
}
+ /// Update the capabilities of the running language server.
pub fn update_capabilities(&self, update: impl FnOnce(&mut ServerCapabilities)) {
update(self.capabilities.write().deref_mut());
}
+ /// Get the individual configuration settings for the running language server.
+ /// Does not include globally applied settings (which are stored in ProjectSettings::GlobalLspSettings).
pub fn configuration(&self) -> &Value {
&self.configuration.settings
}
- /// Get the id of the running language server.
+ /// Get the ID of the running language server.
pub fn server_id(&self) -> LanguageServerId {
self.server_id
}
@@ -1305,17 +1346,18 @@ impl LanguageServer {
self.server.lock().as_ref().map(|child| child.id())
}
- /// Language server's binary information.
+ /// Get the binary information of the running language server.
pub fn binary(&self) -> &LanguageServerBinary {
&self.binary
}
- /// Sends a RPC request to the language server.
+ /// Send a RPC request to the language server.
///
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
pub fn request<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();
@@ -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"),
@@ -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(())
})
@@ -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)),
);
@@ -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)),
);
@@ -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)),
);
@@ -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
@@ -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)),
);
@@ -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();
}
@@ -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(¤t_worktree_id)
- .iter()
- .flat_map(|prettier_paths| prettier_paths.iter())
- .flatten()
- .filter_map(|prettier_path| {
- Some((
- current_worktree_id,
- Some(prettier_path.clone()),
- self.prettier_instances.get(prettier_path)?.clone(),
- ))
- })
- .chain(self.default_prettier.instance().map(|default_prettier| {
- (current_worktree_id, None, default_prettier.clone())
- }))
- .collect::<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(¤t_worktree_id)
+ .iter()
+ .flat_map(|prettier_paths| prettier_paths.iter())
+ .flatten()
+ .filter_map(|prettier_path| {
+ Some((
+ current_worktree_id,
+ Some(prettier_path.clone()),
+ self.prettier_instances.get(prettier_path)?.clone(),
+ ))
+ })
+ .chain(
+ self.default_prettier
+ .instance()
+ .map(|default_prettier| (current_worktree_id, None, default_prettier.clone())),
+ )
+ .collect::<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));
@@ -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
@@ -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()
@@ -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;
@@ -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.
@@ -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.