mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
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:
parent
52cddaae37
commit
db53a65ab6
26 changed files with 1364 additions and 741 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -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,
|
||||
.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 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,
|
||||
¬ification_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>(¬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"),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(¤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:#}"
|
||||
),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.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(¤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(),
|
||||
))
|
||||
})
|
||||
.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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue