Add configurable LSP timeout setting (#44745)

Fixes #36818

Release Notes:

- Added new `global_lsp_settings.request_timeout` setting to configure
the maximum timeout duration for LSP-related operations.

Code inspired by [prior
implementation](https://github.com/zed-industries/zed/pull/38443),
though with a few tweaks here & there (like using `serde:default` and
keeping the pre-defined constant in the LSP file).

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
Bertie690 2026-02-06 19:36:37 -05:00 committed by GitHub
parent 52cddaae37
commit db53a65ab6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1364 additions and 741 deletions

1
Cargo.lock generated
View file

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

View file

@ -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.

View file

@ -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");

View file

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

View file

@ -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

View file

@ -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,
.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,
}),
},
position: lsp_position,
context: InlineCompletionContext {
trigger_kind: InlineCompletionTriggerKind::Automatic,
},
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);
});
}
}

View file

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

View file

@ -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, |_| {});

View file

@ -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");

View file

@ -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;

View file

@ -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 {

View file

@ -8,6 +8,7 @@ use collections::{BTreeMap, HashMap};
use futures::{
AsyncRead, AsyncWrite, Future, FutureExt,
channel::oneshot::{self, Canceled},
future::{self, Either},
io::BufWriter,
select,
};
@ -46,11 +47,22 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact};
const JSON_RPC_VERSION: &str = "2.0";
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
/// The default amount of time to wait while initializing or fetching LSP servers, in seconds.
///
/// Should not be used (in favor of DEFAULT_LSP_REQUEST_TIMEOUT) and is exported solely for use inside ProjectSettings defaults.
pub const DEFAULT_LSP_REQUEST_TIMEOUT_SECS: u64 = 120;
/// A timeout representing the value of [DEFAULT_LSP_REQUEST_TIMEOUT_SECS].
///
/// Should **only be used** in tests and as a fallback when a corresponding config value cannot be obtained!
pub const DEFAULT_LSP_REQUEST_TIMEOUT: Duration =
Duration::from_secs(DEFAULT_LSP_REQUEST_TIMEOUT_SECS);
/// The shutdown timeout for LSP servers (including Prettier/Copilot).
const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
type NotificationHandler = Box<dyn Send + FnMut(Option<RequestId>, Value, &mut AsyncApp)>;
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
type PendingRespondTasks = Arc<Mutex<HashMap<RequestId, Task<()>>>>;
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>) -> Task<()>>;
type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
/// Kind of language server stdio given to an IO handler.
@ -101,6 +113,9 @@ pub struct LanguageServer {
code_action_kinds: Option<Vec<CodeActionKind>>,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
/// Tasks spawned by `on_custom_request` to compute responses. Tracked so that
/// incoming `$/cancelRequest` notifications can cancel them by dropping the task.
pending_respond_tasks: PendingRespondTasks,
io_handlers: Arc<Mutex<HashMap<i32, IoHandler>>>,
executor: BackgroundExecutor,
#[allow(clippy::type_complexity)]
@ -378,6 +393,7 @@ pub const SEMANTIC_TOKEN_MODIFIERS: &[SemanticTokenModifier] = &[
impl LanguageServer {
/// Starts a language server process.
/// A request_timeout of zero or Duration::MAX indicates an indefinite timeout.
pub fn new(
stderr_capture: Arc<Mutex<Option<String>>>,
server_id: LanguageServerId,
@ -473,6 +489,7 @@ impl LanguageServer {
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
let response_handlers =
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
let pending_respond_tasks = PendingRespondTasks::default();
let io_handlers = Arc::new(Mutex::new(HashMap::default()));
let stdout_input_task = cx.spawn({
@ -500,12 +517,14 @@ impl LanguageServer {
let notification_handlers = notification_handlers.clone();
let response_handlers = response_handlers.clone();
let io_handlers = io_handlers.clone();
let pending_respond_tasks = pending_respond_tasks.clone();
async move |cx| {
Self::handle_incoming_messages(
stdout,
unhandled_notification_wrapper,
notification_handlers,
response_handlers,
pending_respond_tasks,
io_handlers,
cx,
)
@ -563,6 +582,7 @@ impl LanguageServer {
notification_handlers,
notification_tx,
response_handlers,
pending_respond_tasks,
io_handlers,
name: server_name,
version: None,
@ -596,6 +616,7 @@ impl LanguageServer {
on_unhandled_notification: impl AsyncFn(NotificationOrRequest) + 'static + Send,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
pending_respond_tasks: PendingRespondTasks,
io_handlers: Arc<Mutex<HashMap<i32, IoHandler>>>,
cx: &mut AsyncApp,
) -> anyhow::Result<()>
@ -618,6 +639,19 @@ impl LanguageServer {
);
while let Some(msg) = input_handler.incoming_messages.next().await {
if msg.method == <notification::Cancel as notification::Notification>::METHOD {
if let Some(params) = msg.params {
if let Ok(cancel_params) = serde_json::from_value::<CancelParams>(params) {
let id = match cancel_params.id {
NumberOrString::Number(id) => RequestId::Int(id),
NumberOrString::String(id) => RequestId::Str(id),
};
pending_respond_tasks.lock().remove(&id);
}
}
continue;
}
let unhandled_message = {
let mut notification_handlers = notification_handlers.lock();
if let Some(handler) = notification_handlers.get_mut(msg.method.as_str()) {
@ -1018,11 +1052,12 @@ impl LanguageServer {
mut self,
params: InitializeParams,
configuration: Arc<DidChangeConfigurationParams>,
timeout: Duration,
cx: &App,
) -> Task<Result<Arc<Self>>> {
cx.background_spawn(async move {
let response = self
.request::<request::Initialize>(params)
.request::<request::Initialize>(params, timeout)
.await
.into_response()
.with_context(|| {
@ -1046,62 +1081,61 @@ impl LanguageServer {
/// Sends a shutdown request to the language server process and prepares the [`LanguageServer`] to be dropped.
pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Option<()>> + use<>> {
if let Some(tasks) = self.io_tasks.lock().take() {
let response_handlers = self.response_handlers.clone();
let next_id = AtomicI32::new(self.next_id.load(SeqCst));
let outbound_tx = self.outbound_tx.clone();
let executor = self.executor.clone();
let notification_serializers = self.notification_tx.clone();
let mut output_done = self.output_done_rx.lock().take().unwrap();
let shutdown_request = Self::request_internal::<request::Shutdown>(
&next_id,
&response_handlers,
&outbound_tx,
&notification_serializers,
&executor,
(),
);
let tasks = self.io_tasks.lock().take()?;
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");
let response_handlers = self.response_handlers.clone();
let next_id = AtomicI32::new(self.next_id.load(SeqCst));
let outbound_tx = self.outbound_tx.clone();
let executor = self.executor.clone();
let notification_serializers = self.notification_tx.clone();
let mut output_done = self.output_done_rx.lock().take().unwrap();
let shutdown_request = Self::request_internal::<request::Shutdown>(
&next_id,
&response_handlers,
&outbound_tx,
&notification_serializers,
&executor,
SERVER_SHUTDOWN_TIMEOUT,
(),
);
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 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(())) => {}
}
_ = timer => {
log::info!("timeout waiting for language server {name} (id {server_id}) to shutdown");
},
}
response_handlers.lock().take();
Self::notify_internal::<notification::Exit>(&notification_serializers, ()).ok();
notification_serializers.close();
output_done.recv().await;
server.lock().take().map(|mut child| child.kill());
drop(tasks);
log::debug!("language server shutdown finished");
Some(())
})
} else {
None
}
_ = timer => {
log::info!("timeout waiting for language server {name} (id {server_id}) to shutdown");
},
}
response_handlers.lock().take();
Self::notify_internal::<notification::Exit>(&notification_serializers, ()).ok();
notification_serializers.close();
output_done.recv().await;
server.lock().take().map(|mut child| child.kill());
drop(tasks);
log::debug!("language server shutdown finished");
Some(())
})
}
/// Register a handler to handle incoming LSP notifications.
@ -1192,6 +1226,7 @@ impl LanguageServer {
Res: Serialize,
{
let outbound_tx = self.outbound_tx.clone();
let pending_respond_tasks = self.pending_respond_tasks.clone();
let prev_handler = self.notification_handlers.lock().insert(
method,
Box::new(move |id, params, cx| {
@ -1199,34 +1234,36 @@ impl LanguageServer {
match serde_json::from_value(params) {
Ok(params) => {
let response = f(params, cx);
cx.foreground_executor()
.spawn({
let outbound_tx = outbound_tx.clone();
async move {
let response = match response.await {
Ok(result) => Response {
jsonrpc: JSON_RPC_VERSION,
id,
value: LspResult::Ok(Some(result)),
},
Err(error) => Response {
jsonrpc: JSON_RPC_VERSION,
id,
value: LspResult::Error(Some(Error {
code: lsp_types::error_codes::REQUEST_FAILED,
message: error.to_string(),
data: None,
})),
},
};
if let Some(response) =
serde_json::to_string(&response).log_err()
{
outbound_tx.try_send(response).ok();
}
let task = cx.foreground_executor().spawn({
let outbound_tx = outbound_tx.clone();
let pending_respond_tasks = pending_respond_tasks.clone();
let id = id.clone();
async move {
let response = match response.await {
Ok(result) => Response {
jsonrpc: JSON_RPC_VERSION,
id: id.clone(),
value: LspResult::Ok(Some(result)),
},
Err(error) => Response {
jsonrpc: JSON_RPC_VERSION,
id: id.clone(),
value: LspResult::Error(Some(Error {
code: lsp_types::error_codes::REQUEST_FAILED,
message: error.to_string(),
data: None,
})),
},
};
if let Some(response) =
serde_json::to_string(&response).log_err()
{
outbound_tx.try_send(response).ok();
}
})
.detach();
pending_respond_tasks.lock().remove(&id);
}
});
pending_respond_tasks.lock().insert(id, task);
}
Err(error) => {
@ -1269,6 +1306,7 @@ impl LanguageServer {
self.version.clone()
}
/// Get the process name of the running language server.
pub fn process_name(&self) -> &str {
&self.process_name
}
@ -1287,15 +1325,18 @@ impl LanguageServer {
}
}
/// Update the capabilities of the running language server.
pub fn update_capabilities(&self, update: impl FnOnce(&mut ServerCapabilities)) {
update(self.capabilities.write().deref_mut());
}
/// Get the individual configuration settings for the running language server.
/// Does not include globally applied settings (which are stored in ProjectSettings::GlobalLspSettings).
pub fn configuration(&self) -> &Value {
&self.configuration.settings
}
/// Get the id of the running language server.
/// Get the ID of the running language server.
pub fn server_id(&self) -> LanguageServerId {
self.server_id
}
@ -1305,17 +1346,18 @@ impl LanguageServer {
self.server.lock().as_ref().map(|child| child.id())
}
/// Language server's binary information.
/// Get the binary information of the running language server.
pub fn binary(&self) -> &LanguageServerBinary {
&self.binary
}
/// Sends a RPC request to the language server.
/// Send a RPC request to the language server.
///
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
pub fn request<T: request::Request>(
&self,
params: T::Params,
request_timeout: Duration,
) -> impl LspRequestFuture<T::Result> + use<T>
where
T::Result: 'static + Send,
@ -1326,12 +1368,13 @@ impl LanguageServer {
&self.outbound_tx,
&self.notification_tx,
&self.executor,
request_timeout,
params,
)
}
/// Sends a RPC request to the language server, with a custom timer, a future which when becoming
/// ready causes the request to be timed out with the future's output message.
/// Send a RPC request to the language server with a custom timer.
/// Once the attached future becomes ready, the request will time out with the provided output message.
///
/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage)
pub fn request_with_timer<T: request::Request, U: Future<Output = String>>(
@ -1355,7 +1398,7 @@ impl LanguageServer {
fn request_internal_with_timer<T, U>(
next_id: &AtomicI32,
response_handlers: &Mutex<Option<HashMap<RequestId, ResponseHandler>>>,
response_handlers: &Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
outbound_tx: &channel::Sender<String>,
notification_serializers: &channel::Sender<NotificationSerializer>,
executor: &BackgroundExecutor,
@ -1374,7 +1417,7 @@ impl LanguageServer {
method: T::METHOD,
params,
})
.unwrap();
.expect("LSP message should be serializable to JSON");
let (tx, rx) = oneshot::channel();
let handle_response = response_handlers
@ -1398,9 +1441,8 @@ impl LanguageServer {
}
Err(error) => Err(anyhow!("{}", error.message)),
};
_ = tx.send(response);
tx.send(response).ok();
})
.detach();
}),
);
});
@ -1409,6 +1451,7 @@ impl LanguageServer {
.try_send(message)
.context("failed to write to language server's stdin");
let response_handlers = Arc::clone(response_handlers);
let notification_serializers = notification_serializers.downgrade();
let started = Instant::now();
LspRequest::new(id, async move {
@ -1448,7 +1491,16 @@ impl LanguageServer {
message = timer.fuse() => {
log::error!("Cancelled LSP request task for {method:?} id {id} {message}");
ConnectionResult::Timeout
match response_handlers
.lock()
.as_mut()
.context("server shut down") {
Ok(handlers) => {
handlers.remove(&RequestId::Int(id));
ConnectionResult::Timeout
}
Err(e) => ConnectionResult::Result(Err(e)),
}
}
}
})
@ -1456,10 +1508,11 @@ impl LanguageServer {
fn request_internal<T>(
next_id: &AtomicI32,
response_handlers: &Mutex<Option<HashMap<RequestId, ResponseHandler>>>,
response_handlers: &Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
outbound_tx: &channel::Sender<String>,
notification_serializers: &channel::Sender<NotificationSerializer>,
executor: &BackgroundExecutor,
request_timeout: Duration,
params: T::Params,
) -> impl LspRequestFuture<T::Result> + use<T>
where
@ -1472,15 +1525,31 @@ impl LanguageServer {
outbound_tx,
notification_serializers,
executor,
Self::default_request_timer(executor.clone()),
Self::request_timeout_future(executor.clone(), request_timeout),
params,
)
}
pub fn default_request_timer(executor: BackgroundExecutor) -> impl Future<Output = String> {
executor
.timer(LSP_REQUEST_TIMEOUT)
.map(|_| format!("which took over {LSP_REQUEST_TIMEOUT:?}"))
/// Internal function to return a Future from a configured timeout duration.
/// If the duration is zero or `Duration::MAX`, the returned future never completes.
fn request_timeout_future(
executor: BackgroundExecutor,
request_timeout: Duration,
) -> impl Future<Output = String> {
if request_timeout == Duration::MAX || request_timeout == Duration::ZERO {
return Either::Left(future::pending::<String>());
}
Either::Right(
executor
.timer(request_timeout)
.map(move |_| format!("which took over {request_timeout:?}")),
)
}
/// Obtain a request timer for the LSP.
pub fn request_timer(&self, timeout: Duration) -> impl Future<Output = String> {
Self::request_timeout_future(self.executor.clone(), timeout)
}
/// Sends a RPC notification to the language server.
@ -1851,12 +1920,16 @@ impl FakeLanguageServer {
}
/// See [`LanguageServer::request`].
pub async fn request<T>(&self, params: T::Params) -> ConnectionResult<T::Result>
pub async fn request<T>(
&self,
params: T::Params,
timeout: Duration,
) -> ConnectionResult<T::Result>
where
T: request::Request,
T::Result: 'static + Send,
{
self.server.request::<T>(params).await
self.server.request::<T>(params, timeout).await
}
/// Attempts [`Self::try_receive_notification`], unwrapping if it has not received the specified type yet.
@ -1938,18 +2011,23 @@ impl FakeLanguageServer {
/// Simulate that the server has started work and notifies about its progress with the specified token.
pub async fn start_progress(&self, token: impl Into<String>) {
self.start_progress_with(token, Default::default()).await
self.start_progress_with(token, Default::default(), Default::default())
.await
}
pub async fn start_progress_with(
&self,
token: impl Into<String>,
progress: WorkDoneProgressBegin,
request_timeout: Duration,
) {
let token = token.into();
self.request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
token: NumberOrString::String(token.clone()),
})
self.request::<request::WorkDoneProgressCreate>(
WorkDoneProgressCreateParams {
token: NumberOrString::String(token.clone()),
},
request_timeout,
)
.await
.into_response()
.unwrap();
@ -2015,7 +2093,12 @@ mod tests {
let configuration = DidChangeConfigurationParams {
settings: Default::default(),
};
server.initialize(params, configuration.into(), cx)
server.initialize(
params,
configuration.into(),
DEFAULT_LSP_REQUEST_TIMEOUT,
cx,
)
})
.await
.unwrap();

View file

@ -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"),

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -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

View file

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

View file

@ -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;
};
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")?;
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 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
}
};
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();
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)
})
})
.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;
}
};
let cx = cx.clone();
let request_timeout = cx.update(|app|
ProjectSettings::get_global(app)
.global_lsp_settings
.get_request_timeout()
);
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;
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
}
};
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();
}

View file

@ -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 current_worktree_id = worktree.read(cx).id();
if let Some((config_path, _, _)) = prettier_config_file_changed {
log::info!(
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
);
let prettiers_to_reload =
self.prettiers_per_worktree
.get(&current_worktree_id)
.iter()
.flat_map(|prettier_paths| prettier_paths.iter())
.flatten()
.filter_map(|prettier_path| {
Some((
current_worktree_id,
Some(prettier_path.clone()),
self.prettier_instances.get(prettier_path)?.clone(),
))
})
.chain(self.default_prettier.instance().map(|default_prettier| {
(current_worktree_id, None, default_prettier.clone())
}))
.collect::<Vec<_>>();
cx.background_spawn(async move {
let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| {
async move {
if let Some(instance) = prettier_instance.prettier {
match instance.await {
Ok(prettier) => {
prettier.clear_cache().log_err().await;
},
Err(e) => {
match prettier_path {
Some(prettier_path) => log::error!(
"Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
),
None => log::error!(
"Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
),
}
},
}
}
}
}))
.await;
let Some((config_path, _, _)) = prettier_config_file_changed else {
return;
};
let current_worktree_id = worktree.read(cx).id();
log::info!(
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
);
let prettiers_to_reload = self
.prettiers_per_worktree
.get(&current_worktree_id)
.iter()
.flat_map(|prettier_paths| prettier_paths.iter())
.flatten()
.filter_map(|prettier_path| {
Some((
current_worktree_id,
Some(prettier_path.clone()),
self.prettier_instances.get(prettier_path)?.clone(),
))
})
.detach();
}
.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();
}
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));

View file

@ -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

View file

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

View file

@ -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;

View file

@ -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.

View file

@ -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.