mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-27 21:43:37 +00:00
Another attempt to add multiple edits
This commit is contained in:
parent
01422cc31d
commit
73dc8b6f06
8 changed files with 97 additions and 28 deletions
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(", ")
|
||||||
)
|
)
|
||||||
|
|
|
@ -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<_>>();
|
||||||
|
|
|
@ -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 }))
|
||||||
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue