Skip to content

feat: question chain api #19828

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion crates/ide-assists/src/assist_context.rs
Original file line number Diff line number Diff line change
@@ -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::{
Expand Down Expand Up @@ -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>,
Expand All @@ -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(())
}

Expand Down
16 changes: 16 additions & 0 deletions crates/ide-assists/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1132,6 +1147,7 @@ pub fn test_some_range(a: int) -> bool {
},
),
command: None,
question_chain: None,
}
"#]]
.assert_debug_eq(&extract_into_function_assist);
Expand Down
7 changes: 6 additions & 1 deletion crates/ide-db/src/assists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)]
Expand Down
151 changes: 151 additions & 0 deletions crates/ide-db/src/source_change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ fn quickfix_for_redundant_assoc_item(
target: range,
source_change: Some(source_change_builder.finish()),
command: None,
question_chain: None,
}])
}

Expand Down
1 change: 1 addition & 0 deletions crates/ide-diagnostics/src/handlers/typed_hole.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading