Skip to content

Commit aa2e13b

Browse files
Merge #2716
2716: Allow assists with multiple selectable actions r=SomeoneToIgnore a=SomeoneToIgnore This PR prepares an infra for #2180 task by adding a possibility to specify multiple actions in one assist as multiple edit parameters to the `applySourceChange` command. When this is done, the command opens a selection dialog, allowing the user to pick the edit to be applied. I have no working example to test in this PR, but here's a demo of an auto import feature (a separate PR coming later for that one) using this functionality: ![out](https://user-images.githubusercontent.com/2690773/71633614-f8ea4d80-2c1d-11ea-9b15-0e13611a7aa4.gif) The PR is not that massive as it may seem: all the assist files' changes are very generic and similar. Co-authored-by: Kirill Bulatov <[email protected]>
2 parents 01422cc + 79b7740 commit aa2e13b

File tree

11 files changed

+150
-43
lines changed

11 files changed

+150
-43
lines changed

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ra_assists/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ doctest = false
1111
format-buf = "1.0.0"
1212
join_to_string = "0.1.3"
1313
rustc-hash = "1.0"
14-
itertools = "0.8.0"
14+
either = "1.5"
1515

1616
ra_syntax = { path = "../ra_syntax" }
1717
ra_text_edit = { path = "../ra_text_edit" }

crates/ra_assists/src/assist_ctx.rs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! This module defines `AssistCtx` -- the API surface that is exposed to assists.
2+
use either::Either;
23
use hir::{db::HirDatabase, InFile, SourceAnalyzer};
34
use ra_db::FileRange;
45
use ra_fmt::{leading_indent, reindent};
@@ -9,12 +10,12 @@ use ra_syntax::{
910
};
1011
use ra_text_edit::TextEditBuilder;
1112

12-
use crate::{AssistAction, AssistId, AssistLabel};
13+
use crate::{AssistAction, AssistId, AssistLabel, ResolvedAssist};
1314

1415
#[derive(Clone, Debug)]
1516
pub(crate) enum Assist {
1617
Unresolved { label: AssistLabel },
17-
Resolved { label: AssistLabel, action: AssistAction },
18+
Resolved { assist: ResolvedAssist },
1819
}
1920

2021
/// `AssistCtx` allows to apply an assist or check if it could be applied.
@@ -81,18 +82,45 @@ impl<'a, DB: HirDatabase> AssistCtx<'a, DB> {
8182
self,
8283
id: AssistId,
8384
label: impl Into<String>,
84-
f: impl FnOnce(&mut AssistBuilder),
85+
f: impl FnOnce(&mut ActionBuilder),
8586
) -> Option<Assist> {
8687
let label = AssistLabel { label: label.into(), id };
8788
assert!(label.label.chars().nth(0).unwrap().is_uppercase());
8889

8990
let assist = if self.should_compute_edit {
9091
let action = {
91-
let mut edit = AssistBuilder::default();
92+
let mut edit = ActionBuilder::default();
9293
f(&mut edit);
9394
edit.build()
9495
};
95-
Assist::Resolved { label, action }
96+
Assist::Resolved { assist: ResolvedAssist { label, action_data: Either::Left(action) } }
97+
} else {
98+
Assist::Unresolved { label }
99+
};
100+
101+
Some(assist)
102+
}
103+
104+
#[allow(dead_code)] // will be used for auto import assist with multiple actions
105+
pub(crate) fn add_assist_group(
106+
self,
107+
id: AssistId,
108+
label: impl Into<String>,
109+
f: impl FnOnce() -> Vec<ActionBuilder>,
110+
) -> Option<Assist> {
111+
let label = AssistLabel { label: label.into(), id };
112+
let assist = if self.should_compute_edit {
113+
let actions = f();
114+
assert!(!actions.is_empty(), "Assist cannot have no");
115+
116+
Assist::Resolved {
117+
assist: ResolvedAssist {
118+
label,
119+
action_data: Either::Right(
120+
actions.into_iter().map(ActionBuilder::build).collect(),
121+
),
122+
},
123+
}
96124
} else {
97125
Assist::Unresolved { label }
98126
};
@@ -128,13 +156,20 @@ impl<'a, DB: HirDatabase> AssistCtx<'a, DB> {
128156
}
129157

130158
#[derive(Default)]
131-
pub(crate) struct AssistBuilder {
159+
pub(crate) struct ActionBuilder {
132160
edit: TextEditBuilder,
133161
cursor_position: Option<TextUnit>,
134162
target: Option<TextRange>,
163+
label: Option<String>,
135164
}
136165

137-
impl AssistBuilder {
166+
impl ActionBuilder {
167+
#[allow(dead_code)]
168+
/// Adds a custom label to the action, if it needs to be different from the assist label
169+
pub(crate) fn label(&mut self, label: impl Into<String>) {
170+
self.label = Some(label.into())
171+
}
172+
138173
/// Replaces specified `range` of text with a given string.
139174
pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
140175
self.edit.replace(range, replace_with.into())
@@ -193,6 +228,7 @@ impl AssistBuilder {
193228
edit: self.edit.finish(),
194229
cursor_position: self.cursor_position,
195230
target: self.target,
231+
label: self.label,
196232
}
197233
}
198234
}

crates/ra_assists/src/assists/inline_local_variable.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use ra_syntax::{
44
TextRange,
55
};
66

7-
use crate::assist_ctx::AssistBuilder;
7+
use crate::assist_ctx::ActionBuilder;
88
use crate::{Assist, AssistCtx, AssistId};
99

1010
// Assist: inline_local_variable
@@ -94,7 +94,7 @@ pub(crate) fn inline_local_varialbe(ctx: AssistCtx<impl HirDatabase>) -> Option<
9494
ctx.add_assist(
9595
AssistId("inline_local_variable"),
9696
"Inline variable",
97-
move |edit: &mut AssistBuilder| {
97+
move |edit: &mut ActionBuilder| {
9898
edit.delete(delete_range);
9999
for (desc, should_wrap) in refs.iter().zip(wrap_in_parens) {
100100
if should_wrap {

crates/ra_assists/src/doc_tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,21 @@ fn check(assist_id: &str, before: &str, after: &str) {
1515
let (db, file_id) = TestDB::with_single_file(&before);
1616
let frange = FileRange { file_id, range: selection.into() };
1717

18-
let (_assist_id, action) = crate::assists(&db, frange)
18+
let assist = crate::assists(&db, frange)
1919
.into_iter()
20-
.find(|(id, _)| id.id.0 == assist_id)
20+
.find(|assist| assist.label.id.0 == assist_id)
2121
.unwrap_or_else(|| {
2222
panic!(
2323
"\n\nAssist is not applicable: {}\nAvailable assists: {}",
2424
assist_id,
2525
crate::assists(&db, frange)
2626
.into_iter()
27-
.map(|(id, _)| id.id.0)
27+
.map(|assist| assist.label.id.0)
2828
.collect::<Vec<_>>()
2929
.join(", ")
3030
)
3131
});
3232

33-
let actual = action.edit.apply(&before);
33+
let actual = assist.get_first_action().edit.apply(&before);
3434
assert_eq_text!(after, &actual);
3535
}

crates/ra_assists/src/lib.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod doc_tests;
1313
mod test_db;
1414
pub mod ast_transform;
1515

16+
use either::Either;
1617
use hir::db::HirDatabase;
1718
use ra_db::FileRange;
1819
use ra_syntax::{TextRange, TextUnit};
@@ -35,11 +36,27 @@ pub struct AssistLabel {
3536

3637
#[derive(Debug, Clone)]
3738
pub struct AssistAction {
39+
pub label: Option<String>,
3840
pub edit: TextEdit,
3941
pub cursor_position: Option<TextUnit>,
4042
pub target: Option<TextRange>,
4143
}
4244

45+
#[derive(Debug, Clone)]
46+
pub struct ResolvedAssist {
47+
pub label: AssistLabel,
48+
pub action_data: Either<AssistAction, Vec<AssistAction>>,
49+
}
50+
51+
impl ResolvedAssist {
52+
pub fn get_first_action(&self) -> AssistAction {
53+
match &self.action_data {
54+
Either::Left(action) => action.clone(),
55+
Either::Right(actions) => actions[0].clone(),
56+
}
57+
}
58+
}
59+
4360
/// Return all the assists applicable at the given position.
4461
///
4562
/// Assists are returned in the "unresolved" state, that is only labels are
@@ -64,7 +81,7 @@ where
6481
///
6582
/// Assists are returned in the "resolved" state, that is with edit fully
6683
/// computed.
67-
pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)>
84+
pub fn assists<H>(db: &H, range: FileRange) -> Vec<ResolvedAssist>
6885
where
6986
H: HirDatabase + 'static,
7087
{
@@ -75,11 +92,11 @@ where
7592
.iter()
7693
.filter_map(|f| f(ctx.clone()))
7794
.map(|a| match a {
78-
Assist::Resolved { label, action } => (label, action),
95+
Assist::Resolved { assist } => assist,
7996
Assist::Unresolved { .. } => unreachable!(),
8097
})
8198
.collect::<Vec<_>>();
82-
a.sort_by(|a, b| match (a.1.target, b.1.target) {
99+
a.sort_by(|a, b| match (a.get_first_action().target, b.get_first_action().target) {
83100
(Some(a), Some(b)) => a.len().cmp(&b.len()),
84101
(Some(_), None) => Ordering::Less,
85102
(None, Some(_)) => Ordering::Greater,
@@ -174,7 +191,7 @@ mod helpers {
174191
AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
175192
let action = match assist {
176193
Assist::Unresolved { .. } => unreachable!(),
177-
Assist::Resolved { action, .. } => action,
194+
Assist::Resolved { assist } => assist.get_first_action(),
178195
};
179196

180197
let actual = action.edit.apply(&before);
@@ -201,7 +218,7 @@ mod helpers {
201218
AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
202219
let action = match assist {
203220
Assist::Unresolved { .. } => unreachable!(),
204-
Assist::Resolved { action, .. } => action,
221+
Assist::Resolved { assist } => assist.get_first_action(),
205222
};
206223

207224
let mut actual = action.edit.apply(&before);
@@ -224,7 +241,7 @@ mod helpers {
224241
AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
225242
let action = match assist {
226243
Assist::Unresolved { .. } => unreachable!(),
227-
Assist::Resolved { action, .. } => action,
244+
Assist::Resolved { assist } => assist.get_first_action(),
228245
};
229246

230247
let range = action.target.expect("expected target on action");
@@ -243,7 +260,7 @@ mod helpers {
243260
AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
244261
let action = match assist {
245262
Assist::Unresolved { .. } => unreachable!(),
246-
Assist::Resolved { action, .. } => action,
263+
Assist::Resolved { assist } => assist.get_first_action(),
247264
};
248265

249266
let range = action.target.expect("expected target on action");
@@ -293,10 +310,10 @@ mod tests {
293310
let mut assists = assists.iter();
294311

295312
assert_eq!(
296-
assists.next().expect("expected assist").0.label,
313+
assists.next().expect("expected assist").label.label,
297314
"Change visibility to pub(crate)"
298315
);
299-
assert_eq!(assists.next().expect("expected assist").0.label, "Add `#[derive]`");
316+
assert_eq!(assists.next().expect("expected assist").label.label, "Add `#[derive]`");
300317
}
301318

302319
#[test]
@@ -315,7 +332,7 @@ mod tests {
315332
let assists = super::assists(&db, frange);
316333
let mut assists = assists.iter();
317334

318-
assert_eq!(assists.next().expect("expected assist").0.label, "Extract into variable");
319-
assert_eq!(assists.next().expect("expected assist").0.label, "Replace with match");
335+
assert_eq!(assists.next().expect("expected assist").label.label, "Extract into variable");
336+
assert_eq!(assists.next().expect("expected assist").label.label, "Replace with match");
320337
}
321338
}

crates/ra_ide/src/assists.rs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,53 @@
22
33
use ra_db::{FilePosition, FileRange};
44

5-
use crate::{db::RootDatabase, SourceChange, SourceFileEdit};
5+
use crate::{db::RootDatabase, FileId, SourceChange, SourceFileEdit};
66

7+
use either::Either;
78
pub use ra_assists::AssistId;
9+
use ra_assists::{AssistAction, AssistLabel};
810

911
#[derive(Debug)]
1012
pub struct Assist {
1113
pub id: AssistId,
12-
pub change: SourceChange,
14+
pub label: String,
15+
pub change_data: Either<SourceChange, Vec<SourceChange>>,
1316
}
1417

1518
pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec<Assist> {
1619
ra_assists::assists(db, frange)
1720
.into_iter()
18-
.map(|(label, action)| {
21+
.map(|assist| {
1922
let file_id = frange.file_id;
20-
let file_edit = SourceFileEdit { file_id, edit: action.edit };
21-
let id = label.id;
22-
let change = SourceChange::source_file_edit(label.label, file_edit).with_cursor_opt(
23-
action.cursor_position.map(|offset| FilePosition { offset, file_id }),
24-
);
25-
Assist { id, change }
23+
let assist_label = &assist.label;
24+
Assist {
25+
id: assist_label.id,
26+
label: assist_label.label.clone(),
27+
change_data: match assist.action_data {
28+
Either::Left(action) => {
29+
Either::Left(action_to_edit(action, file_id, assist_label))
30+
}
31+
Either::Right(actions) => Either::Right(
32+
actions
33+
.into_iter()
34+
.map(|action| action_to_edit(action, file_id, assist_label))
35+
.collect(),
36+
),
37+
},
38+
}
2639
})
2740
.collect()
2841
}
42+
43+
fn action_to_edit(
44+
action: AssistAction,
45+
file_id: FileId,
46+
assist_label: &AssistLabel,
47+
) -> SourceChange {
48+
let file_edit = SourceFileEdit { file_id, edit: action.edit };
49+
SourceChange::source_file_edit(
50+
action.label.unwrap_or_else(|| assist_label.label.clone()),
51+
file_edit,
52+
)
53+
.with_cursor_opt(action.cursor_position.map(|offset| FilePosition { offset, file_id }))
54+
}

crates/ra_lsp_server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ ra_prof = { path = "../ra_prof" }
2828
ra_vfs_glob = { path = "../ra_vfs_glob" }
2929
env_logger = { version = "0.7.1", default-features = false, features = ["humantime"] }
3030
ra_cargo_watch = { path = "../ra_cargo_watch" }
31+
either = "1.5"
3132

3233
[dev-dependencies]
3334
tempfile = "3"

0 commit comments

Comments
 (0)