Another attempt to add multiple edits

This commit is contained in:
Kirill Bulatov 2020-01-02 01:39:01 +02:00
parent 01422cc31d
commit 73dc8b6f06
8 changed files with 97 additions and 28 deletions

View file

@ -14,7 +14,7 @@ use crate::{AssistAction, AssistId, AssistLabel};
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) enum Assist { pub(crate) enum Assist {
Unresolved { label: AssistLabel }, 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. /// `AssistCtx` allows to apply an assist or check if it could be applied.
@ -81,18 +81,43 @@ impl<'a, DB: HirDatabase> AssistCtx<'a, DB> {
self, self,
id: AssistId, id: AssistId,
label: impl Into<String>, label: impl Into<String>,
f: impl FnOnce(&mut AssistBuilder), f: impl FnOnce(&mut ActionBuilder),
) -> Option<Assist> { ) -> Option<Assist> {
let label = AssistLabel { label: label.into(), id }; let label = AssistLabel { label: label.into(), id };
assert!(label.label.chars().nth(0).unwrap().is_uppercase()); assert!(label.label.chars().nth(0).unwrap().is_uppercase());
let assist = if self.should_compute_edit { let assist = if self.should_compute_edit {
let action = { let action = {
let mut edit = AssistBuilder::default(); let mut edit = ActionBuilder::default();
f(&mut edit); f(&mut edit);
edit.build() 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 { } else {
Assist::Unresolved { label } Assist::Unresolved { label }
}; };
@ -128,13 +153,20 @@ impl<'a, DB: HirDatabase> AssistCtx<'a, DB> {
} }
#[derive(Default)] #[derive(Default)]
pub(crate) struct AssistBuilder { pub(crate) struct ActionBuilder {
edit: TextEditBuilder, edit: TextEditBuilder,
cursor_position: Option<TextUnit>, cursor_position: Option<TextUnit>,
target: Option<TextRange>, 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. /// Replaces specified `range` of text with a given string.
pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) { pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
self.edit.replace(range, replace_with.into()) self.edit.replace(range, replace_with.into())
@ -193,6 +225,7 @@ impl AssistBuilder {
edit: self.edit.finish(), edit: self.edit.finish(),
cursor_position: self.cursor_position, cursor_position: self.cursor_position,
target: self.target, target: self.target,
label: self.label,
} }
} }
} }

View file

@ -4,7 +4,7 @@ use ra_syntax::{
TextRange, TextRange,
}; };
use crate::assist_ctx::AssistBuilder; use crate::assist_ctx::ActionBuilder;
use crate::{Assist, AssistCtx, AssistId}; use crate::{Assist, AssistCtx, AssistId};
// Assist: inline_local_variable // Assist: inline_local_variable
@ -94,7 +94,7 @@ pub(crate) fn inline_local_varialbe(ctx: AssistCtx<impl HirDatabase>) -> Option<
ctx.add_assist( ctx.add_assist(
AssistId("inline_local_variable"), AssistId("inline_local_variable"),
"Inline variable", "Inline variable",
move |edit: &mut AssistBuilder| { move |edit: &mut ActionBuilder| {
edit.delete(delete_range); edit.delete(delete_range);
for (desc, should_wrap) in refs.iter().zip(wrap_in_parens) { for (desc, should_wrap) in refs.iter().zip(wrap_in_parens) {
if should_wrap { if should_wrap {

View file

@ -15,16 +15,16 @@ fn check(assist_id: &str, before: &str, after: &str) {
let (db, file_id) = TestDB::with_single_file(&before); let (db, file_id) = TestDB::with_single_file(&before);
let frange = FileRange { file_id, range: selection.into() }; 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() .into_iter()
.find(|(id, _)| id.id.0 == assist_id) .find(|(id, _, _)| id.id.0 == assist_id)
.unwrap_or_else(|| { .unwrap_or_else(|| {
panic!( panic!(
"\n\nAssist is not applicable: {}\nAvailable assists: {}", "\n\nAssist is not applicable: {}\nAvailable assists: {}",
assist_id, assist_id,
crate::assists(&db, frange) crate::assists(&db, frange)
.into_iter() .into_iter()
.map(|(id, _)| id.id.0) .map(|(id, _, _)| id.id.0)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", ") .join(", ")
) )

View file

@ -35,6 +35,7 @@ pub struct AssistLabel {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AssistAction { pub struct AssistAction {
pub label: Option<String>,
pub edit: TextEdit, pub edit: TextEdit,
pub cursor_position: Option<TextUnit>, pub cursor_position: Option<TextUnit>,
pub target: Option<TextRange>, pub target: Option<TextRange>,
@ -64,7 +65,7 @@ where
/// ///
/// Assists are returned in the "resolved" state, that is with edit fully /// Assists are returned in the "resolved" state, that is with edit fully
/// computed. /// 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 where
H: HirDatabase + 'static, H: HirDatabase + 'static,
{ {
@ -75,7 +76,9 @@ where
.iter() .iter()
.filter_map(|f| f(ctx.clone())) .filter_map(|f| f(ctx.clone()))
.map(|a| match a { .map(|a| match a {
Assist::Resolved { label, action } => (label, action), Assist::Resolved { label, action, alternative_actions } => {
(label, action, alternative_actions)
}
Assist::Unresolved { .. } => unreachable!(), Assist::Unresolved { .. } => unreachable!(),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -2,27 +2,46 @@
use ra_db::{FilePosition, FileRange}; use ra_db::{FilePosition, FileRange};
use crate::{db::RootDatabase, SourceChange, SourceFileEdit}; use crate::{db::RootDatabase, FileId, SourceChange, SourceFileEdit};
pub use ra_assists::AssistId; pub use ra_assists::AssistId;
use ra_assists::{AssistAction, AssistLabel};
#[derive(Debug)] #[derive(Debug)]
pub struct Assist { pub struct Assist {
pub id: AssistId, pub id: AssistId,
pub change: SourceChange, pub change: SourceChange,
pub label: String,
pub alternative_changes: Vec<SourceChange>,
} }
pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec<Assist> { pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec<Assist> {
ra_assists::assists(db, frange) ra_assists::assists(db, frange)
.into_iter() .into_iter()
.map(|(label, action)| { .map(|(assist_label, action, alternative_actions)| {
let file_id = frange.file_id; let file_id = frange.file_id;
let file_edit = SourceFileEdit { file_id, edit: action.edit }; Assist {
let id = label.id; id: assist_label.id,
let change = SourceChange::source_file_edit(label.label, file_edit).with_cursor_opt( label: assist_label.label.clone(),
action.cursor_position.map(|offset| FilePosition { offset, file_id }), change: action_to_edit(action, file_id, &assist_label),
); alternative_changes: alternative_actions
Assist { id, change } .into_iter()
.map(|action| action_to_edit(action, file_id, &assist_label))
.collect(),
}
}) })
.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 }))
}

View file

@ -644,7 +644,6 @@ pub fn handle_code_action(
let line_index = world.analysis().file_line_index(file_id)?; let line_index = world.analysis().file_line_index(file_id)?;
let range = params.range.conv_with(&line_index); 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 diagnostics = world.analysis().diagnostics(file_id)?;
let mut res = CodeActionResponse::default(); let mut res = CodeActionResponse::default();
@ -697,14 +696,19 @@ pub fn handle_code_action(
res.push(action.into()); res.push(action.into());
} }
for assist in assists { for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() {
let title = assist.change.label.clone(); let title = assist.label.clone();
let edit = assist.change.try_conv_with(&world)?; 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 { let command = Command {
title, title,
command: "rust-analyzer.applySourceChange".to_string(), 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 { let action = CodeAction {
title: command.title.clone(), title: command.title.clone(),

View file

@ -34,8 +34,8 @@ function showReferences(ctx: Ctx): Cmd {
} }
function applySourceChange(ctx: Ctx): Cmd { function applySourceChange(ctx: Ctx): Cmd {
return async (change: sourceChange.SourceChange) => { return async (change: sourceChange.SourceChange, alternativeChanges: sourceChange.SourceChange[] | undefined) => {
sourceChange.applySourceChange(ctx, change); sourceChange.applySourceChange(ctx, change, alternativeChanges);
}; };
} }

View file

@ -9,7 +9,7 @@ export interface SourceChange {
cursorPosition?: lc.TextDocumentPositionParams; cursorPosition?: lc.TextDocumentPositionParams;
} }
export async function applySourceChange(ctx: Ctx, change: SourceChange) { async function applySelectedSourceChange(ctx: Ctx, change: SourceChange) {
const client = ctx.client; const client = ctx.client;
if (!client) return; if (!client) return;
@ -55,3 +55,13 @@ export async function applySourceChange(ctx: Ctx, change: SourceChange) {
); );
} }
} }
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);
}
}