@@ -330,6 +330,10 @@ impl CachedLspAdapter {
.cloned()
.unwrap_or_else(|| language_name.lsp_id())
}
+
+ pub fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) {
+ self.adapter.process_prompt_response(context, cx)
+ }
}
/// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
@@ -355,6 +359,17 @@ pub trait LspAdapterDelegate: Send + Sync {
async fn try_exec(&self, binary: LanguageServerBinary) -> Result<()>;
}
+/// Context provided to LSP adapters when a user responds to a ShowMessageRequest prompt.
+/// This allows adapters to intercept preference selections (like "Always" or "Never")
+/// and potentially persist them to Zed's settings.
+#[derive(Debug, Clone)]
+pub struct PromptResponseContext {
+ /// The original message shown to the user
+ pub message: String,
+ /// The action (button) the user selected
+ pub selected_action: lsp::MessageActionItem,
+}
+
#[async_trait(?Send)]
pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller {
fn name(&self) -> LanguageServerName;
@@ -511,6 +526,11 @@ pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller {
fn is_extension(&self) -> bool {
false
}
+
+ /// Called when a user responds to a ShowMessageRequest from this language server.
+ /// This allows adapters to intercept preference selections (like "Always" or "Never")
+ /// for settings that should be persisted to Zed's settings file.
+ fn process_prompt_response(&self, _context: &PromptResponseContext, _cx: &mut AsyncApp) {}
}
pub trait LspInstaller {
@@ -2,13 +2,17 @@ use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
use gpui::AsyncApp;
-use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain};
+use language::{
+ LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, PromptResponseContext, Toolchain,
+};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri};
use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use regex::Regex;
use semver::Version;
use serde_json::Value;
+use serde_json::json;
+use settings::update_settings_file;
use std::{
ffi::OsString,
path::{Path, PathBuf},
@@ -16,6 +20,11 @@ use std::{
};
use util::{ResultExt, maybe, merge_json_value_into};
+const ACTION_ALWAYS: &str = "Always";
+const ACTION_NEVER: &str = "Never";
+const UPDATE_IMPORTS_MESSAGE_PATTERN: &str = "Update imports for";
+const VTSLS_SERVER_NAME: &str = "vtsls";
+
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
@@ -302,6 +311,52 @@ impl LspAdapter for VtslsLspAdapter {
(LanguageName::new_static("TSX"), "typescriptreact".into()),
])
}
+
+ fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) {
+ let selected_title = context.selected_action.title.as_str();
+ let is_preference_response =
+ selected_title == ACTION_ALWAYS || selected_title == ACTION_NEVER;
+ if !is_preference_response {
+ return;
+ }
+
+ if context.message.contains(UPDATE_IMPORTS_MESSAGE_PATTERN) {
+ let setting_value = match selected_title {
+ ACTION_ALWAYS => "always",
+ ACTION_NEVER => "never",
+ _ => return,
+ };
+
+ let settings = json!({
+ "typescript": {
+ "updateImportsOnFileMove": {
+ "enabled": setting_value
+ }
+ },
+ "javascript": {
+ "updateImportsOnFileMove": {
+ "enabled": setting_value
+ }
+ }
+ });
+
+ let _ = cx.update(|cx| {
+ update_settings_file(self.fs.clone(), cx, move |content, _| {
+ let lsp_settings = content
+ .project
+ .lsp
+ .entry(VTSLS_SERVER_NAME.into())
+ .or_default();
+
+ if let Some(existing) = &mut lsp_settings.settings {
+ merge_json_value_into(settings, existing);
+ } else {
+ lsp_settings.settings = Some(settings);
+ }
+ });
+ });
+ }
+ }
}
async fn get_cached_ts_server_binary(
@@ -1056,12 +1056,15 @@ impl LocalLspStore {
.on_request::<lsp::request::ShowMessageRequest, _, _>({
let this = lsp_store.clone();
let name = name.to_string();
+ let adapter = adapter.clone();
move |params, cx| {
let this = this.clone();
let name = name.to_string();
+ let adapter = adapter.clone();
let mut cx = cx.clone();
async move {
let actions = params.actions.unwrap_or_default();
+ let message = params.message.clone();
let (tx, rx) = smol::channel::bounded(1);
let request = LanguageServerPromptRequest {
level: match params.typ {
@@ -1082,6 +1085,14 @@ impl LocalLspStore {
.is_ok();
if did_update {
let response = rx.recv().await.ok();
+ if let Some(ref selected_action) = response {
+ let context = language::PromptResponseContext {
+ message,
+ selected_action: selected_action.clone(),
+ };
+ adapter.process_prompt_response(&context, &mut cx)
+ }
+
Ok(response)
} else {
Ok(None)