#[derive(Clone, Debug)]
pub(crate) enum Assist {
Unresolved { label: AssistLabel },
- Resolved { label: AssistLabel, action: AssistAction },
+ Resolved { label: AssistLabel, action: AssistAction, alternative_actions: Vec<AssistAction> },
}
/// `AssistCtx` allows to apply an assist or check if it could be applied.
self,
id: AssistId,
label: impl Into<String>,
- f: impl FnOnce(&mut AssistBuilder),
+ f: impl FnOnce(&mut ActionBuilder),
) -> Option<Assist> {
let label = AssistLabel { label: label.into(), id };
assert!(label.label.chars().nth(0).unwrap().is_uppercase());
let assist = if self.should_compute_edit {
let action = {
- let mut edit = AssistBuilder::default();
+ let mut edit = ActionBuilder::default();
f(&mut edit);
edit.build()
};
- Assist::Resolved { label, action }
+ Assist::Resolved { label, action, alternative_actions: Vec::default() }
+ } else {
+ Assist::Unresolved { label }
+ };
+
+ Some(assist)
+ }
+
+ #[allow(dead_code)] // will be used for auto import assist with multiple actions
+ pub(crate) fn add_assist_group(
+ self,
+ id: AssistId,
+ label: impl Into<String>,
+ f: impl FnOnce() -> (ActionBuilder, Vec<ActionBuilder>),
+ ) -> Option<Assist> {
+ let label = AssistLabel { label: label.into(), id };
+ let assist = if self.should_compute_edit {
+ let (action, alternative_actions) = f();
+ Assist::Resolved {
+ label,
+ action: action.build(),
+ alternative_actions: alternative_actions
+ .into_iter()
+ .map(ActionBuilder::build)
+ .collect(),
+ }
} else {
Assist::Unresolved { label }
};
}
#[derive(Default)]
-pub(crate) struct AssistBuilder {
+pub(crate) struct ActionBuilder {
edit: TextEditBuilder,
cursor_position: Option<TextUnit>,
target: Option<TextRange>,
+ label: Option<String>,
}
-impl AssistBuilder {
+impl ActionBuilder {
+ #[allow(dead_code)]
+ /// Adds a custom label to the action, if it needs to be different from the assist label
+ pub fn label(&mut self, label: impl Into<String>) {
+ self.label = Some(label.into())
+ }
+
/// Replaces specified `range` of text with a given string.
pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
self.edit.replace(range, replace_with.into())
edit: self.edit.finish(),
cursor_position: self.cursor_position,
target: self.target,
+ label: self.label,
}
}
}
TextRange,
};
-use crate::assist_ctx::AssistBuilder;
+use crate::assist_ctx::ActionBuilder;
use crate::{Assist, AssistCtx, AssistId};
// Assist: inline_local_variable
ctx.add_assist(
AssistId("inline_local_variable"),
"Inline variable",
- move |edit: &mut AssistBuilder| {
+ move |edit: &mut ActionBuilder| {
edit.delete(delete_range);
for (desc, should_wrap) in refs.iter().zip(wrap_in_parens) {
if should_wrap {
let (db, file_id) = TestDB::with_single_file(&before);
let frange = FileRange { file_id, range: selection.into() };
- let (_assist_id, action) = crate::assists(&db, frange)
+ let (_assist_id, action, _) = crate::assists(&db, frange)
.into_iter()
- .find(|(id, _)| id.id.0 == assist_id)
+ .find(|(id, _, _)| id.id.0 == assist_id)
.unwrap_or_else(|| {
panic!(
"\n\nAssist is not applicable: {}\nAvailable assists: {}",
assist_id,
crate::assists(&db, frange)
.into_iter()
- .map(|(id, _)| id.id.0)
+ .map(|(id, _, _)| id.id.0)
.collect::<Vec<_>>()
.join(", ")
)
#[derive(Debug, Clone)]
pub struct AssistAction {
+ pub label: Option<String>,
pub edit: TextEdit,
pub cursor_position: Option<TextUnit>,
pub target: Option<TextRange>,
///
/// Assists are returned in the "resolved" state, that is with edit fully
/// computed.
-pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)>
+pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction, Vec<AssistAction>)>
where
H: HirDatabase + 'static,
{
.iter()
.filter_map(|f| f(ctx.clone()))
.map(|a| match a {
- Assist::Resolved { label, action } => (label, action),
+ Assist::Resolved { label, action, alternative_actions } => {
+ (label, action, alternative_actions)
+ }
Assist::Unresolved { .. } => unreachable!(),
})
.collect::<Vec<_>>();
use ra_db::{FilePosition, FileRange};
-use crate::{db::RootDatabase, SourceChange, SourceFileEdit};
+use crate::{db::RootDatabase, FileId, SourceChange, SourceFileEdit};
pub use ra_assists::AssistId;
+use ra_assists::{AssistAction, AssistLabel};
#[derive(Debug)]
pub struct Assist {
pub id: AssistId,
pub change: SourceChange,
+ pub label: String,
+ pub alternative_changes: Vec<SourceChange>,
}
pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec<Assist> {
ra_assists::assists(db, frange)
.into_iter()
- .map(|(label, action)| {
+ .map(|(assist_label, action, alternative_actions)| {
let file_id = frange.file_id;
- let file_edit = SourceFileEdit { file_id, edit: action.edit };
- let id = label.id;
- let change = SourceChange::source_file_edit(label.label, file_edit).with_cursor_opt(
- action.cursor_position.map(|offset| FilePosition { offset, file_id }),
- );
- Assist { id, change }
+ Assist {
+ id: assist_label.id,
+ label: assist_label.label.clone(),
+ change: action_to_edit(action, file_id, &assist_label),
+ alternative_changes: alternative_actions
+ .into_iter()
+ .map(|action| action_to_edit(action, file_id, &assist_label))
+ .collect(),
+ }
})
.collect()
}
+
+fn action_to_edit(
+ action: AssistAction,
+ file_id: FileId,
+ assist_label: &AssistLabel,
+) -> SourceChange {
+ let file_edit = SourceFileEdit { file_id, edit: action.edit };
+ SourceChange::source_file_edit(
+ action.label.unwrap_or_else(|| assist_label.label.clone()),
+ file_edit,
+ )
+ .with_cursor_opt(action.cursor_position.map(|offset| FilePosition { offset, file_id }))
+}
let line_index = world.analysis().file_line_index(file_id)?;
let range = params.range.conv_with(&line_index);
- let assists = world.analysis().assists(FileRange { file_id, range })?.into_iter();
let diagnostics = world.analysis().diagnostics(file_id)?;
let mut res = CodeActionResponse::default();
res.push(action.into());
}
- for assist in assists {
- let title = assist.change.label.clone();
+ for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() {
+ let title = assist.label.clone();
let edit = assist.change.try_conv_with(&world)?;
+ let alternative_edits = assist
+ .alternative_changes
+ .into_iter()
+ .map(|change| change.try_conv_with(&world))
+ .collect::<Result<Vec<_>>>()?;
let command = Command {
title,
command: "rust-analyzer.applySourceChange".to_string(),
- arguments: Some(vec![to_value(edit).unwrap()]),
+ arguments: Some(vec![to_value(edit).unwrap(), to_value(alternative_edits).unwrap()]),
};
let action = CodeAction {
title: command.title.clone(),
}
function applySourceChange(ctx: Ctx): Cmd {
- return async (change: sourceChange.SourceChange) => {
- sourceChange.applySourceChange(ctx, change);
+ return async (change: sourceChange.SourceChange, alternativeChanges: sourceChange.SourceChange[] | undefined) => {
+ sourceChange.applySourceChange(ctx, change, alternativeChanges);
};
}
cursorPosition?: lc.TextDocumentPositionParams;
}
-export async function applySourceChange(ctx: Ctx, change: SourceChange) {
+async function applySelectedSourceChange(ctx: Ctx, change: SourceChange) {
const client = ctx.client;
if (!client) return;
);
}
}
+
+export async function applySourceChange(ctx: Ctx, change: SourceChange, alternativeChanges: SourceChange[] | undefined) {
+ if (alternativeChanges !== undefined && alternativeChanges.length > 0) {
+ const selectedChange = await vscode.window.showQuickPick([change, ...alternativeChanges]);
+ if (!selectedChange) return;
+ await applySelectedSourceChange(ctx, selectedChange);
+ } else {
+ await applySelectedSourceChange(ctx, change);
+ }
+}