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 {