Skip to content

feat: user choice group 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 6 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::{UserChoice, UserChoiceGroup};
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 multiple choices, user's choice will be passed to `f` as a list of indices.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I once again got a bit confused with the distinction between a multiple choice question and many consecutive questions. Surely the problem is with me, but a clarification never hurts. Thanks.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I once again got a bit confused with the distinction between a multiple choice question and many consecutive questions. Surely the problem is with me, but a clarification never hurts. Thanks.

yes that's confusing, I should come up with a better name for them, because this api intended to provide multiple consecutive questions with multiple choices, it's a bit of confusing.

/// 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,
user_choice_group: Some(UserChoiceGroup::new(
choices
.into_iter()
.map(|(title, choices)| UserChoice::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,
user_choice_group: 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,
user_choice_group: 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,
user_choice_group: 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,
user_choice_group: 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,
user_choice_group: 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,
user_choice_group: 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,
user_choice_group: 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,
user_choice_group: 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,
user_choice_group: 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,
),
user_choice_group: 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,
user_choice_group: 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,
user_choice_group: 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,
user_choice_group: 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,
),
user_choice_group: 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,
),
user_choice_group: 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,
),
user_choice_group: 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,
user_choice_group: 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::{SourceChange, UserChoiceGroup},
};

#[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 group of choices to show to the user when applying the assist.
pub user_choice_group: Option<UserChoiceGroup>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down
158 changes: 158 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,159 @@ 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 choices offered to the user(Using ShowMessageRequest), 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 UserChoiceGroup {
/// A list of choice groups. Each inner tuple's first string is title, second vector represents a set of options
/// from which the user can make one selection.
/// For example, `choice_options[0]` might be `["Question 1", ["Option A", "Option B"]]` and
/// `choices[1]` might be `["Question 2", ["Setting X", "Setting Y"]]`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be choice_options[1] instead of choices[1] ?

choice_options: Vec<UserChoice>,
/// 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 group in `choice_options`.
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 UserChoice {
pub title: String,
pub actions: Vec<String>,
}

impl UserChoice {
pub fn new(title: String, actions: Vec<String>) -> Self {
Self { title, actions }
}
}

impl std::fmt::Debug for UserChoiceGroup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UserChoiceGroup")
.field("choice_options", &self.choice_options)
.field("callback", &"<ChoiceCallback>")
.field("cur_choices", &self.cur_choices)
.finish()
}
}

impl UserChoiceGroup {
/// Creates a new `UserChoiceGroup`.
///
/// # Arguments
///
/// * `choice_options`: A vector of `UserChoice` objects representing the choices
/// * `callback`: A function that will be called with the indices of the
/// user's selections after they make their choices.
///
pub fn new(
choice_options: Vec<UserChoice>,
callback: impl FnOnce(&mut SourceChangeBuilder, &[usize]) + Send + 'static,
file: FileId,
) -> Self {
Self {
cur_choices: vec![],
choice_options,
callback: Arc::new(Mutex::new(Some(Box::new(callback)))),
file,
}
}

/// Returns (`idx`, `title`, `choices`) of the current question.
///
pub fn get_cur_question(&self) -> Option<(usize, &UserChoice)> {
if self.cur_choices.len() < self.choice_options.len() {
let idx = self.cur_choices.len();
let user_choice = &self.choice_options[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.choice_options.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.choice_options.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 choice group are made, we will queue them up and ask the user
/// one by one.
queue: VecDeque<UserChoiceGroup>,
/// Indicates if the first choice 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 `UserChoiceGroup` to the queue.
pub fn add_choice_group(&mut self, group: UserChoiceGroup) {
self.queue.push_back(group);
}

pub fn first_mut_choice_group(&mut self) -> Option<&mut UserChoiceGroup> {
self.queue.front_mut()
}

pub fn pop_choice_group(&mut self) -> Option<UserChoiceGroup> {
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,
user_choice_group: None,
}])
}

Expand Down
Loading