diff --git a/crates/ide-assists/src/assist_context.rs b/crates/ide-assists/src/assist_context.rs
index 9eb9452a2b83..c838777b2e25 100644
--- a/crates/ide-assists/src/assist_context.rs
+++ b/crates/ide-assists/src/assist_context.rs
@@ -1,6 +1,7 @@
 //! See [`AssistContext`].
 
 use hir::{EditionedFileId, FileRange, Semantics};
+use ide_db::source_change::{MultiChoiceQuestion, QuestionChain};
 use ide_db::{FileId, RootDatabase, label::Label};
 use syntax::Edition;
 use syntax::{
@@ -202,6 +203,45 @@ impl Assists {
         self.add_impl(Some(group), id, label.into(), target, &mut |it| f.take().unwrap()(it))
     }
 
+    /// Give user many consecutive questions, each with multiple choices, user's choice will be passed to `f` as a list of indices.
+    /// The indices are the indices of the choices in the original list.
+    /// TODO(discord9): remove allow(unused) once auto import all use this function
+    #[allow(unused)]
+    pub(crate) fn add_choices(
+        &mut self,
+        group: &Option<GroupLabel>,
+        id: AssistId,
+        label: impl Into<String>,
+        target: TextRange,
+        choices: Vec<(String, Vec<String>)>,
+        f: impl FnOnce(&mut SourceChangeBuilder, &[usize]) + Send + 'static,
+    ) -> Option<()> {
+        if !self.is_allowed(&id) {
+            return None;
+        }
+        let label = Label::new(label.into());
+        let group = group.clone();
+
+        self.buf.push(Assist {
+            id,
+            label,
+            group,
+            target,
+            source_change: None,
+            command: None,
+            question_chain: Some(QuestionChain::new(
+                choices
+                    .into_iter()
+                    .map(|(title, choices)| MultiChoiceQuestion::new(title, choices))
+                    .collect(),
+                f,
+                self.file,
+            )),
+        });
+
+        Some(())
+    }
+
     fn add_impl(
         &mut self,
         group: Option<&GroupLabel>,
@@ -226,7 +266,15 @@ impl Assists {
 
         let label = Label::new(label);
         let group = group.cloned();
-        self.buf.push(Assist { id, label, group, target, source_change, command });
+        self.buf.push(Assist {
+            id,
+            label,
+            group,
+            target,
+            source_change,
+            command,
+            question_chain: None,
+        });
         Some(())
     }
 
diff --git a/crates/ide-assists/src/tests.rs b/crates/ide-assists/src/tests.rs
index 5e6889792db6..0c93178d0e76 100644
--- a/crates/ide-assists/src/tests.rs
+++ b/crates/ide-assists/src/tests.rs
@@ -548,6 +548,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_variable_assist);
@@ -569,6 +570,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_constant_assist);
@@ -590,6 +592,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_static_assist);
@@ -611,6 +614,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_function_assist);
@@ -647,6 +651,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_variable_assist);
@@ -668,6 +673,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_constant_assist);
@@ -689,6 +695,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_static_assist);
@@ -710,6 +717,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_function_assist);
@@ -792,6 +800,7 @@ pub fn test_some_range(a: int) -> bool {
                 command: Some(
                     Rename,
                 ),
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_variable_assist);
@@ -813,6 +822,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_constant_assist);
@@ -834,6 +844,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_static_assist);
@@ -855,6 +866,7 @@ pub fn test_some_range(a: int) -> bool {
                 target: 59..60,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_function_assist);
@@ -933,6 +945,7 @@ pub fn test_some_range(a: int) -> bool {
                 command: Some(
                     Rename,
                 ),
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_variable_assist);
@@ -1004,6 +1017,7 @@ pub fn test_some_range(a: int) -> bool {
                 command: Some(
                     Rename,
                 ),
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_constant_assist);
@@ -1075,6 +1089,7 @@ pub fn test_some_range(a: int) -> bool {
                 command: Some(
                     Rename,
                 ),
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_static_assist);
@@ -1132,6 +1147,7 @@ pub fn test_some_range(a: int) -> bool {
                     },
                 ),
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&extract_into_function_assist);
diff --git a/crates/ide-db/src/assists.rs b/crates/ide-db/src/assists.rs
index 384eb57c0fd5..024b6703f332 100644
--- a/crates/ide-db/src/assists.rs
+++ b/crates/ide-db/src/assists.rs
@@ -9,7 +9,10 @@ use std::str::FromStr;
 
 use syntax::TextRange;
 
-use crate::{label::Label, source_change::SourceChange};
+use crate::{
+    label::Label,
+    source_change::{QuestionChain, SourceChange},
+};
 
 #[derive(Debug, Clone)]
 pub struct Assist {
@@ -31,6 +34,8 @@ pub struct Assist {
     pub source_change: Option<SourceChange>,
     /// The command to execute after the assist is applied.
     pub command: Option<Command>,
+    /// The questions to show to the user when applying the assist.
+    pub question_chain: Option<QuestionChain>,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
diff --git a/crates/ide-db/src/source_change.rs b/crates/ide-db/src/source_change.rs
index b1b58d6568cb..fac67917a12e 100644
--- a/crates/ide-db/src/source_change.rs
+++ b/crates/ide-db/src/source_change.rs
@@ -3,6 +3,8 @@
 //!
 //! It can be viewed as a dual for `Change`.
 
+use std::collections::VecDeque;
+use std::sync::{Arc, Mutex};
 use std::{collections::hash_map::Entry, fmt, iter, mem};
 
 use crate::text_edit::{TextEdit, TextEditBuilder};
@@ -557,3 +559,152 @@ impl PlaceSnippet {
         }
     }
 }
+
+/// a function that takes a `SourceChangeBuilder` and a slice of indices
+/// which represent the indices of the choices made by the user
+/// which is the choice being made, each one from corresponding choice list in `Assists::add_choices`
+pub type ChoiceCallback = dyn FnOnce(&mut SourceChangeBuilder, &[usize]) + Send + 'static;
+
+/// Represents a group of consecutive questions offered to the user(Using LSP's ShowMessageRequest)
+/// each with multiple choices,
+/// along with a callback to be executed based on the user's selection.
+///
+/// This is typically used in scenarios like "assists" or "quick fixes" where
+/// the user needs to pick from several options to proceed with a source code change.
+#[derive(Clone)]
+pub struct QuestionChain {
+    /// A list of questions. Each `MultiChoiceQuestion` represents a question with multiple choices.
+    questions: Vec<MultiChoiceQuestion>,
+    /// The callback function to be invoked with the user's selections.
+    /// The `&[usize]` argument to the callback will contain the indices
+    /// of the choices made by the user, corresponding to each question in `question_chain`.
+    callback: Arc<Mutex<Option<Box<ChoiceCallback>>>>,
+    /// The current choices made by the user, represented as a vector of indices.
+    cur_choices: Vec<usize>,
+    /// The file ID associated with the choices. Used for construct SourceChangeBuilder.
+    /// This is typically the file where the changes will be applied.
+    file: FileId,
+}
+
+#[derive(Debug, Clone)]
+pub struct MultiChoiceQuestion {
+    /// Title of the question to be presented to the user.
+    pub title: String,
+    /// A list of actions or choices available for the user to select from.
+    pub actions: Vec<String>,
+}
+
+impl MultiChoiceQuestion {
+    pub fn new(title: String, actions: Vec<String>) -> Self {
+        Self { title, actions }
+    }
+}
+
+impl std::fmt::Debug for QuestionChain {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("QuestionChain")
+            .field("questions", &self.questions)
+            .field("callback", &"<ChoiceCallback>")
+            .field("cur_choices", &self.cur_choices)
+            .finish()
+    }
+}
+
+impl QuestionChain {
+    /// Creates a new `ConsecutiveQuestions`.
+    pub fn new(
+        questions: Vec<MultiChoiceQuestion>,
+        callback: impl FnOnce(&mut SourceChangeBuilder, &[usize]) + Send + 'static,
+        file: FileId,
+    ) -> Self {
+        Self {
+            cur_choices: vec![],
+            questions,
+            callback: Arc::new(Mutex::new(Some(Box::new(callback)))),
+            file,
+        }
+    }
+
+    /// Returns (`idx`, `MultipleChoiceQuestion`) of the current question.
+    ///
+    pub fn get_cur_question(&self) -> Option<(usize, &MultiChoiceQuestion)> {
+        if self.cur_choices.len() < self.questions.len() {
+            let idx = self.cur_choices.len();
+            let user_choice = &self.questions[idx];
+            Some((idx, user_choice))
+        } else {
+            None
+        }
+    }
+
+    /// Whether the user has finished making their choices.
+    pub fn is_done_asking(&self) -> bool {
+        self.cur_choices.len() == self.questions.len()
+    }
+
+    /// Make the idx-th choice in the group.
+    /// `choice` is the index of the choice in the group(0-based).
+    /// This function will be called when the user makes a choice.
+    pub fn make_choice(&mut self, question_idx: usize, choice: usize) -> Result<(), String> {
+        if question_idx < self.questions.len() && question_idx == self.cur_choices.len() {
+            self.cur_choices.push(choice);
+        } else {
+            return Err("Invalid index for choice group".to_owned());
+        }
+
+        Ok(())
+    }
+
+    /// Finalizes the choices made by the user and invokes the callback.
+    /// This function should be called when the user has finished making their choices.
+    pub fn finish(self, builder: &mut SourceChangeBuilder) {
+        let mut callback = self.callback.lock().unwrap();
+        let callback = callback.take().expect("Callback already");
+        callback(builder, &self.cur_choices);
+    }
+
+    pub fn file_id(&self) -> FileId {
+        self.file
+    }
+}
+
+/// A handler for managing user choices in a queue.
+#[derive(Debug, Default)]
+pub struct UserChoiceHandler {
+    /// If multiple consecutive questions group are made, we will queue them up and ask the user
+    /// one by one.
+    queue: VecDeque<QuestionChain>,
+    /// Indicates if the first consecutive questions group in the queue is being processed. Prevent send requests repeatedly.
+    is_awaiting: bool,
+}
+
+impl UserChoiceHandler {
+    /// Creates a new `UserChoiceHandler`.
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Adds a new `ConsecutiveQuestions` to the queue.
+    pub fn add_question_chain(&mut self, questions: QuestionChain) {
+        self.queue.push_back(questions);
+    }
+
+    pub fn first_mut_question_chain(&mut self) -> Option<&mut QuestionChain> {
+        self.queue.front_mut()
+    }
+
+    pub fn pop_question_chain(&mut self) -> Option<QuestionChain> {
+        self.set_awaiting(false);
+        self.queue.pop_front()
+    }
+
+    /// Whether awaiting for sent request's response.
+    pub fn is_awaiting(&self) -> bool {
+        self.is_awaiting
+    }
+
+    /// Sets the awaiting state.
+    pub fn set_awaiting(&mut self, awaiting: bool) {
+        self.is_awaiting = awaiting;
+    }
+}
diff --git a/crates/ide-diagnostics/src/handlers/trait_impl_redundant_assoc_item.rs b/crates/ide-diagnostics/src/handlers/trait_impl_redundant_assoc_item.rs
index 4327b12dce70..b736ef05265c 100644
--- a/crates/ide-diagnostics/src/handlers/trait_impl_redundant_assoc_item.rs
+++ b/crates/ide-diagnostics/src/handlers/trait_impl_redundant_assoc_item.rs
@@ -106,6 +106,7 @@ fn quickfix_for_redundant_assoc_item(
         target: range,
         source_change: Some(source_change_builder.finish()),
         command: None,
+        question_chain: None,
     }])
 }
 
diff --git a/crates/ide-diagnostics/src/handlers/typed_hole.rs b/crates/ide-diagnostics/src/handlers/typed_hole.rs
index 1915a88dd002..eed5bd86d510 100644
--- a/crates/ide-diagnostics/src/handlers/typed_hole.rs
+++ b/crates/ide-diagnostics/src/handlers/typed_hole.rs
@@ -94,6 +94,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::TypedHole) -> Option<Vec<Assist>
                 TextEdit::replace(original_range.range, code),
             )),
             command: None,
+            question_chain: None,
         })
         .collect();
 
diff --git a/crates/ide-diagnostics/src/handlers/unresolved_field.rs b/crates/ide-diagnostics/src/handlers/unresolved_field.rs
index 0649c97f8205..5387b76f93b6 100644
--- a/crates/ide-diagnostics/src/handlers/unresolved_field.rs
+++ b/crates/ide-diagnostics/src/handlers/unresolved_field.rs
@@ -127,6 +127,7 @@ fn add_variant_to_union(
         target: error_range.range,
         source_change: Some(src_change_builder.finish()),
         command: None,
+        question_chain: None,
     })
 }
 
@@ -176,6 +177,7 @@ fn add_field_to_struct_fix(
                 target: error_range.range,
                 source_change: Some(src_change_builder.finish()),
                 command: None,
+                question_chain: None,
             })
         }
         None => {
@@ -213,6 +215,7 @@ fn add_field_to_struct_fix(
                 target: error_range.range,
                 source_change: Some(src_change_builder.finish()),
                 command: None,
+                question_chain: None,
             })
         }
         Some(FieldList::TupleFieldList(_tuple)) => {
@@ -275,6 +278,7 @@ fn method_fix(
             TextEdit::insert(range.end(), "()".to_owned()),
         )),
         command: None,
+        question_chain: None,
     })
 }
 #[cfg(test)]
diff --git a/crates/ide-diagnostics/src/handlers/unresolved_method.rs b/crates/ide-diagnostics/src/handlers/unresolved_method.rs
index 00c2a8c4c468..8db936536ac6 100644
--- a/crates/ide-diagnostics/src/handlers/unresolved_method.rs
+++ b/crates/ide-diagnostics/src/handlers/unresolved_method.rs
@@ -104,6 +104,7 @@ fn field_fix(
             (file_id.file_id(ctx.sema.db), TextEdit::insert(range.end(), ")".to_owned())),
         ])),
         command: None,
+        question_chain: None,
     })
 }
 
@@ -185,6 +186,7 @@ fn assoc_func_fix(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall) -
                 TextEdit::replace(range, assoc_func_call_expr_string),
             )),
             command: None,
+            question_chain: None,
         })
     } else {
         None
diff --git a/crates/ide-diagnostics/src/handlers/unused_variables.rs b/crates/ide-diagnostics/src/handlers/unused_variables.rs
index e6bbff05f7e8..0fe3599f51ff 100644
--- a/crates/ide-diagnostics/src/handlers/unused_variables.rs
+++ b/crates/ide-diagnostics/src/handlers/unused_variables.rs
@@ -80,6 +80,7 @@ fn fixes(
             TextEdit::replace(name_range, format!("_{}", var_name.display(db, edition))),
         )),
         command: None,
+        question_chain: None,
     }])
 }
 
diff --git a/crates/ide-diagnostics/src/lib.rs b/crates/ide-diagnostics/src/lib.rs
index 2af14ca949bf..0030bd8a36ec 100644
--- a/crates/ide-diagnostics/src/lib.rs
+++ b/crates/ide-diagnostics/src/lib.rs
@@ -983,6 +983,7 @@ fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
         target,
         source_change: None,
         command: None,
+        question_chain: None,
     }
 }
 
diff --git a/crates/ide/src/ssr.rs b/crates/ide/src/ssr.rs
index 7df4499a0c2f..a5a4301c25fe 100644
--- a/crates/ide/src/ssr.rs
+++ b/crates/ide/src/ssr.rs
@@ -46,6 +46,7 @@ pub(crate) fn ssr_assists(
             target: comment_range,
             source_change,
             command: None,
+            question_chain: None,
         };
 
         ssr_assists.push(assist);
@@ -154,6 +155,7 @@ mod tests {
                     },
                 ),
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&apply_in_file_assist);
@@ -212,6 +214,7 @@ mod tests {
                     },
                 ),
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&apply_in_workspace_assist);
@@ -253,6 +256,7 @@ mod tests {
                 target: 10..21,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&apply_in_file_assist);
@@ -274,6 +278,7 @@ mod tests {
                 target: 10..21,
                 source_change: None,
                 command: None,
+                question_chain: None,
             }
         "#]]
         .assert_debug_eq(&apply_in_workspace_assist);
diff --git a/crates/rust-analyzer/src/global_state.rs b/crates/rust-analyzer/src/global_state.rs
index 3b3b9c879754..f641713e847e 100644
--- a/crates/rust-analyzer/src/global_state.rs
+++ b/crates/rust-analyzer/src/global_state.rs
@@ -8,7 +8,10 @@ use std::{ops::Not as _, time::Instant};
 use crossbeam_channel::{Receiver, Sender, unbounded};
 use hir::ChangeWithProcMacros;
 use ide::{Analysis, AnalysisHost, Cancellable, FileId, SourceRootId};
-use ide_db::base_db::{Crate, ProcMacroPaths, SourceDatabase};
+use ide_db::{
+    base_db::{Crate, ProcMacroPaths, SourceDatabase},
+    source_change::UserChoiceHandler,
+};
 use itertools::Itertools;
 use load_cargo::SourceRootConfig;
 use lsp_types::{SemanticTokens, Url};
@@ -171,6 +174,9 @@ pub(crate) struct GlobalState {
     /// this queue should run only *after* [`GlobalState::process_changes`] has
     /// been called.
     pub(crate) deferred_task_queue: TaskQueue,
+
+    /// For handling user choice group using `ShowMessageRequest`.
+    pub(crate) user_choice_handler: Arc<Mutex<UserChoiceHandler>>,
 }
 
 /// An immutable snapshot of the world's state at a point in time.
@@ -187,6 +193,8 @@ pub(crate) struct GlobalStateSnapshot {
     // FIXME: Can we derive this from somewhere else?
     pub(crate) proc_macros_loaded: bool,
     pub(crate) flycheck: Arc<[FlycheckHandle]>,
+    /// For handling user choice group using `ShowMessageRequest`.
+    pub(crate) user_choice_handler: Arc<Mutex<UserChoiceHandler>>,
 }
 
 impl std::panic::UnwindSafe for GlobalStateSnapshot {}
@@ -282,6 +290,8 @@ impl GlobalState {
             discover_workspace_queue: OpQueue::default(),
 
             deferred_task_queue: task_queue,
+
+            user_choice_handler: Arc::new(Mutex::new(UserChoiceHandler::default())),
         };
         // Apply any required database inputs from the config.
         this.update_configuration(config);
@@ -531,6 +541,7 @@ impl GlobalState {
             proc_macros_loaded: !self.config.expand_proc_macros()
                 || self.fetch_proc_macros_queue.last_op_result().copied().unwrap_or(false),
             flycheck: self.flycheck.clone(),
+            user_choice_handler: self.user_choice_handler.clone(),
         }
     }
 
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index 69983a676261..6c8f2866ff0a 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -1443,7 +1443,10 @@ pub(crate) fn handle_code_action(
         resolve,
         frange,
     )?;
-    for (index, assist) in assists.into_iter().enumerate() {
+    for (index, mut assist) in assists.into_iter().enumerate() {
+        if let Some(user_choice_group) = assist.question_chain.take() {
+            snap.user_choice_handler.lock().add_question_chain(user_choice_group);
+        }
         let resolve_data = if code_action_resolve_cap {
             Some((index, params.clone(), snap.file_version(file_id)))
         } else {
diff --git a/crates/rust-analyzer/src/lsp/utils.rs b/crates/rust-analyzer/src/lsp/utils.rs
index 673eaa5952f0..feabb2e8fc9f 100644
--- a/crates/rust-analyzer/src/lsp/utils.rs
+++ b/crates/rust-analyzer/src/lsp/utils.rs
@@ -1,15 +1,21 @@
 //! Utilities for LSP-related boilerplate code.
-use std::{mem, ops::Range};
+use std::{mem, ops::Range, panic};
 
-use lsp_server::Notification;
-use lsp_types::request::Request;
+use ide_db::{base_db::DbPanicContext, source_change::SourceChangeBuilder};
+use lsp_server::{Notification, RequestId, Response, ResponseError};
+use lsp_types::{
+    ShowMessageRequestParams,
+    request::{Request, ShowMessageRequest},
+};
+use stdx::thread::ThreadIntent;
 use triomphe::Arc;
 
 use crate::{
     global_state::GlobalState,
     line_index::{LineEndings, LineIndex, PositionEncoding},
-    lsp::{LspError, from_proto},
+    lsp::{LspError, from_proto, to_proto},
     lsp_ext,
+    main_loop::Task,
 };
 
 pub(crate) fn invalid_params_error(message: String) -> LspError {
@@ -95,6 +101,165 @@ impl GlobalState {
         }
     }
 
+    /// Ask for user choice by sending ShowMessageRequest
+    pub(crate) fn ask_for_choice(&mut self) {
+        let params = {
+            let mut handler = self.user_choice_handler.lock();
+            if handler.is_awaiting() {
+                // already sent a request, do nothing
+                return;
+            }
+            let mut is_done_asking = false;
+            let params = if let Some(choice_group) = handler.first_mut_question_chain() {
+                if let Some((_idx, choice)) = choice_group.get_cur_question() {
+                    Some(ShowMessageRequestParams {
+                        typ: lsp_types::MessageType::INFO,
+                        message: choice.title.clone(),
+                        actions: Some(
+                            choice
+                                .actions
+                                .clone()
+                                .into_iter()
+                                .map(|action| lsp_types::MessageActionItem {
+                                    title: action,
+                                    properties: Default::default(),
+                                })
+                                .collect(),
+                        ),
+                    })
+                } else {
+                    is_done_asking = choice_group.is_done_asking();
+                    None
+                }
+            } else {
+                None
+            };
+
+            if is_done_asking {
+                let Some(choice_group) = handler.pop_question_chain() else {
+                    return;
+                };
+                let snap = self.snapshot();
+                // spawn a new task to handle the finished choice, in case of panic
+                self.task_pool.handle.spawn(ThreadIntent::Worker, move || {
+                    let result = panic::catch_unwind(move || {
+                        let _pctx = DbPanicContext::enter("ask_for_choice".to_owned());
+                        let mut source_change_builder =
+                            SourceChangeBuilder::new(choice_group.file_id());
+                        choice_group.finish(&mut source_change_builder);
+                        let source_change = source_change_builder.finish();
+                        to_proto::workspace_edit(&snap, source_change)
+                    });
+
+                    // it's either this or die horribly
+                    let empty_req_id = RequestId::from("".to_owned());
+                    match result {
+                        Ok(Ok(result)) => Task::Response(Response::new_ok(empty_req_id, result)),
+                        Ok(Err(_cancelled)) => Task::Response(Response {
+                            id: empty_req_id,
+                            result: None,
+                            error: Some(ResponseError {
+                                code: lsp_server::ErrorCode::ContentModified as i32,
+                                message: "content modified".to_owned(),
+                                data: None,
+                            }),
+                        }),
+                        Err(panic) => {
+                            let panic_message = panic
+                                .downcast_ref::<String>()
+                                .map(String::as_str)
+                                .or_else(|| panic.downcast_ref::<&str>().copied());
+
+                            let mut message = "request handler panicked".to_owned();
+                            if let Some(panic_message) = panic_message {
+                                message.push_str(": ");
+                                message.push_str(panic_message)
+                            } else if let Ok(_cancelled) =
+                                panic.downcast::<ide_db::base_db::salsa::Cancelled>()
+                            {
+                                tracing::error!(
+                                    "Cancellation propagated out of salsa! This is a bug"
+                                );
+                            }
+                            Task::Response(Response::new_err(
+                                empty_req_id,
+                                lsp_server::ErrorCode::InternalError as i32,
+                                message,
+                            ))
+                        }
+                    }
+                });
+            }
+
+            if params.is_some() {
+                handler.set_awaiting(true);
+            }
+            params
+        };
+
+        // send ShowMessageRequest to the client, and handle the response
+        if let Some(params) = params {
+            self.send_request::<ShowMessageRequest>(params, |state, response| {
+                let lsp_server::Response { error: None, result: Some(result), .. } = response
+                else {
+                    return;
+                };
+                let choice = match crate::from_json::<
+                    <lsp_types::request::ShowMessageRequest as lsp_types::request::Request>::Result,
+                >(
+                    lsp_types::request::ShowMessageRequest::METHOD, &result
+                ) {
+                    Ok(Some(item)) => Some(item.title.clone()),
+                    Err(err) => {
+                        tracing::error!("Failed to deserialize ShowMessageRequest result: {err}");
+                        None
+                    }
+                    // user made no choice
+                    Ok(None) => None,
+                };
+                let mut do_pop = false;
+                let mut handler = state.user_choice_handler.lock();
+                match (handler.first_mut_question_chain(), choice) {
+                    (Some(choice_group), Some(choice)) => {
+                        let Some((question_idx, user_choices)) = choice_group.get_cur_question()
+                        else {
+                            tracing::error!("No question found for user choice");
+                            return;
+                        };
+                        let choice_idx = user_choices
+                            .actions
+                            .iter()
+                            .position(|it| *it == choice)
+                            .unwrap_or(user_choices.actions.len());
+                        if let Err(err) = choice_group.make_choice(question_idx, choice_idx) {
+                            tracing::error!("Failed to make choice: {err}");
+                        }
+                    }
+                    (None, Some(choice)) => {
+                        tracing::error!("No ongoing choice group found for user choice: {choice}");
+                    }
+                    (Some(_), None) => {
+                        // user made no choice, pop&drop current choice group
+                        do_pop = true;
+                    }
+                    _ => (),
+                }
+
+                if do_pop {
+                    let group = handler.pop_question_chain();
+                    tracing::error!(
+                        "User made no choice, dropping current choice group: {group:?}"
+                    );
+                }
+                handler.set_awaiting(false);
+                drop(handler);
+
+                // recursively call handle_choice to handle the next question
+                state.ask_for_choice();
+            });
+        }
+    }
+
     /// rust-analyzer is resilient -- if it fails, this doesn't usually affect
     /// the user experience. Part of that is that we deliberately hide panics
     /// from the user.
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index bd213ffa57a1..05a0e8e5b8c5 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -515,6 +515,7 @@ impl GlobalState {
         }
 
         self.update_status_or_notify();
+        self.ask_for_choice();
 
         let loop_duration = loop_start.elapsed();
         if loop_duration > Duration::from_millis(100) && was_quiescent {