Repository: rust-analyzer/rust-analyzer
Pull request #2716 (2019-12-31 20:40:41)

Allow assists with multiple selectable actions
This PR prepares an infra for https://github.com/rust-analyzer/rust-analyzer/issues/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.

SomeoneToIgnore: I had to use `#[allow(dead_code)]` twice for methods that I'm using in the auto import assist branch to add an assist with multiple actions.

I wanted to include those metods in this PR nevertheless since they are more related to this change and showcase the API for the assist with multiple acitons.
SomeoneToIgnore: `cargo test` passes for me locally on two different machines and the CI does not show the logs currently, so I have no idea what's wrong.
lnicola: The CI logs load for me, there are two failing heavy tests (missing module).
SomeoneToIgnore: TIL we have heavy tests, thank you.
I've found the way to download the logs and fix the issue.
kiljacken: For future reference you can enable the slow test by setting `RUN_SLOW_TESTS=1` in environment variables :)
flodiebold: If I understand it correctly, this change would break other existing clients like the emacs integration, until they're adapted. Maybe there's a way of extending the protocol in a compatible way?
SomeoneToIgnore: Good point. What are the compatibility guarantees RA promises to keep currently?
I have thought that there are none, since it's an experimental tool built from sources.

There are obviously ways to keep the compatibility, we can either create a new action or fiddle with the parameters of an existing one.

What if we fix the emacs client instead?
We should pass a list as a parameter now, I think it might be a one-liner fix somewhere around here: 
https://github.com/rust-analyzer/rust-analyzer/blob/master/editors/emacs/ra-emacs-lsp.el#L41
matklad: >What if we fix the emacs client instead?

I believe vim client(s) also support this extension. Really, we should "just" upstream our extensions for the assists into the protocol (I think the relevant disucssion is https://github.com/microsoft/language-server-protocol/issues/724)
kjeremy: Why doesn't returning multiple items in the reply handle this? See: https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#textDocument_codeAction
SomeoneToIgnore: @kjeremy , do you mean returning multiple actions instead of one action with multiple edits?

I keep the auto import action case in my head and from its perspective my thoughts to go this paths are the following:

* it's the same action semantically, `auto import` with multiple options, not many separate actions
* the list of options for importing might be big for some cases and scrolling through it is not that pleasant (afaik, there are no ways to filter out the list of actions or jump to any of those via any keystrokes)

So, the current way is more like a convenient grouping, allowing the user to quickly select the needed action and then to think on the options provided by it.

Your proposal is great in a sense that it's easier to implement, another thing that concerns me there (besides the bullets above) is the `AssistId`s I need to come up with for those import actions.
Here's how it looks like:

![zzz](https://user-images.githubusercontent.com/2690773/71645641-c60a8d00-2ce3-11ea-9d70-7c173cf064d9.png)

Does anybody have a preference on which approach to go with?
matklad: I feel that we'll need assist groups eventually, so I'd love to add the required plumbing! But just returning, say, the first three candidates also seems fine!
kjeremy: @SomeoneToIgnore yes I think that's what I mean grouped by `CodeActionKind` (I think we can add our own too).
SomeoneToIgnore: I'd like to continue with grouping after all the time I've put into it, but have no idea how to achieve this with the way @kjeremy proposes.

I've added the custom code action name to the [action creation](https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/ra_lsp_server/src/main_loop/handlers.rs#L696) and to the [server capabilities](https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/ra_lsp_server/src/caps.rs#L40) and to the [client capabilities](https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/ra_lsp_server/src/main.rs#L79). but still get the non-grouped actions listed.

Did I miss anything?
SomeoneToIgnore: I've implemented the https://github.com/rust-analyzer/rust-analyzer/pull/2716#discussion_r362348929 idea of mine, no more breaking changes in the protocol, double checked that on master with client changes only.

The https://github.com/rust-analyzer/rust-analyzer/pull/2716#discussion_r362313985 is not addressed yet, let's pick the final approach first.
matklad: r=me with itertools replaced with either

bors delegate+
bors[bot]: :v: SomeoneToIgnore can now approve this pull request. To approve and merge a pull request, simply reply with `bors r+`. More detailed instructions are available [here](https://bors.tech/documentation/getting-started/#reviewing-pull-requests).

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index a0444e97ab..45c4de2b6f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -869,8 +869,8 @@ version = "0.1.0"
 name = "ra_assists"
 version = "0.1.0"
 dependencies = [
+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "format-buf 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "ra_db 0.1.0",
  "ra_fmt 0.1.0",
@@ -1066,6 +1066,7 @@ name = "ra_lsp_server"
 version = "0.1.0"
 dependencies = [
  "crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "jod-thread 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/crates/ra_assists/Cargo.toml b/crates/ra_assists/Cargo.toml
index 50be8d9bc2..0d2109e4ed 100644
--- a/crates/ra_assists/Cargo.toml
+++ b/crates/ra_assists/Cargo.toml
@@ -11,7 +11,7 @@ doctest = false
 format-buf = "1.0.0"
 join_to_string = "0.1.3"
 rustc-hash = "1.0"
-itertools = "0.8.0"
+either = "1.5"
 
 ra_syntax = { path = "../ra_syntax" }
 ra_text_edit = { path = "../ra_text_edit" }
diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs
index 1a65b5dc07..9d533fa0c1 100644
--- a/crates/ra_assists/src/assist_ctx.rs
+++ b/crates/ra_assists/src/assist_ctx.rs
@@ -1,4 +1,5 @@
 //! This module defines `AssistCtx` -- the API surface that is exposed to assists.
+use either::Either;
 use hir::{db::HirDatabase, InFile, SourceAnalyzer};
 use ra_db::FileRange;
 use ra_fmt::{leading_indent, reindent};
@@ -9,12 +10,12 @@ use ra_syntax::{
 };
 use ra_text_edit::TextEditBuilder;
 
-use crate::{AssistAction, AssistId, AssistLabel};
+use crate::{AssistAction, AssistId, AssistLabel, ResolvedAssist};
 
 #[derive(Clone, Debug)]
 pub(crate) enum Assist {
     Unresolved { label: AssistLabel },
-    Resolved { label: AssistLabel, action: AssistAction },
+    Resolved { assist: ResolvedAssist },
 }
 
 /// `AssistCtx` allows to apply an assist or check if it could be applied.
@@ -81,18 +82,45 @@ impl<'a, DB: HirDatabase> AssistCtx<'a, DB> {
         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 { assist: ResolvedAssist { label, action_data: Either::Left(action) } }
+        } 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() -> Vec<ActionBuilder>,
+    ) -> Option<Assist> {
+        let label = AssistLabel { label: label.into(), id };
+        let assist = if self.should_compute_edit {
+            let actions = f();
+            assert!(!actions.is_empty(), "Assist cannot have no");
+
+            Assist::Resolved {
+                assist: ResolvedAssist {
+                    label,
+                    action_data: Either::Right(
+                        actions.into_iter().map(ActionBuilder::build).collect(),
+                    ),
+                },
+            }
         } else {
             Assist::Unresolved { label }
         };
@@ -128,13 +156,20 @@ impl<'a, DB: HirDatabase> AssistCtx<'a, DB> {
 }
 
 #[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(crate) 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())
@@ -193,6 +228,7 @@ impl AssistBuilder {
             edit: self.edit.finish(),
             cursor_position: self.cursor_position,
             target: self.target,
+            label: self.label,
         }
     }
 }
diff --git a/crates/ra_assists/src/assists/inline_local_variable.rs b/crates/ra_assists/src/assists/inline_local_variable.rs
index 164aee90cd..45e0f983fe 100644
--- a/crates/ra_assists/src/assists/inline_local_variable.rs
+++ b/crates/ra_assists/src/assists/inline_local_variable.rs
@@ -4,7 +4,7 @@ use ra_syntax::{
     TextRange,
 };
 
-use crate::assist_ctx::AssistBuilder;
+use crate::assist_ctx::ActionBuilder;
 use crate::{Assist, AssistCtx, AssistId};
 
 // Assist: inline_local_variable
@@ -94,7 +94,7 @@ pub(crate) fn inline_local_varialbe(ctx: AssistCtx<impl HirDatabase>) -> Option<
     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 {
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs
index 150b34ac71..d45b589667 100644
--- a/crates/ra_assists/src/lib.rs
+++ b/crates/ra_assists/src/lib.rs
@@ -13,6 +13,7 @@ mod doc_tests;
 mod test_db;
 pub mod ast_transform;
 
+use either::Either;
 use hir::db::HirDatabase;
 use ra_db::FileRange;
 use ra_syntax::{TextRange, TextUnit};
@@ -35,11 +36,27 @@ pub struct AssistLabel {
 
 #[derive(Debug, Clone)]
 pub struct AssistAction {
+    pub label: Option<String>,
     pub edit: TextEdit,
     pub cursor_position: Option<TextUnit>,
     pub target: Option<TextRange>,
 }
 
+#[derive(Debug, Clone)]
+pub struct ResolvedAssist {
+    pub label: AssistLabel,
+    pub action_data: Either<AssistAction, Vec<AssistAction>>,
+}
+
+impl ResolvedAssist {
+    pub fn get_first_action(&self) -> AssistAction {
+        match &self.action_data {
+            Either::Left(action) => action.clone(),
+            Either::Right(actions) => actions[0].clone(),
+        }
+    }
+}
+
 /// Return all the assists applicable at the given position.
 ///
 /// Assists are returned in the "unresolved" state, that is only labels are
@@ -64,7 +81,7 @@ where
 ///
 /// 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<ResolvedAssist>
 where
     H: HirDatabase + 'static,
 {
@@ -75,11 +92,11 @@ where
             .iter()
             .filter_map(|f| f(ctx.clone()))
             .map(|a| match a {
-                Assist::Resolved { label, action } => (label, action),
+                Assist::Resolved { assist } => assist,
                 Assist::Unresolved { .. } => unreachable!(),
             })
             .collect::<Vec<_>>();
-        a.sort_by(|a, b| match (a.1.target, b.1.target) {
+        a.sort_by(|a, b| match (a.get_first_action().target, b.get_first_action().target) {
             (Some(a), Some(b)) => a.len().cmp(&b.len()),
             (Some(_), None) => Ordering::Less,
             (None, Some(_)) => Ordering::Greater,
@@ -174,7 +191,7 @@ mod helpers {
             AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
         let action = match assist {
             Assist::Unresolved { .. } => unreachable!(),
-            Assist::Resolved { action, .. } => action,
+            Assist::Resolved { assist } => assist.get_first_action(),
         };
 
         let actual = action.edit.apply(&before);
@@ -201,7 +218,7 @@ mod helpers {
             AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
         let action = match assist {
             Assist::Unresolved { .. } => unreachable!(),
-            Assist::Resolved { action, .. } => action,
+            Assist::Resolved { assist } => assist.get_first_action(),
         };
 
         let mut actual = action.edit.apply(&before);
@@ -224,7 +241,7 @@ mod helpers {
             AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
         let action = match assist {
             Assist::Unresolved { .. } => unreachable!(),
-            Assist::Resolved { action, .. } => action,
+            Assist::Resolved { assist } => assist.get_first_action(),
         };
 
         let range = action.target.expect("expected target on action");
@@ -243,7 +260,7 @@ mod helpers {
             AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
         let action = match assist {
             Assist::Unresolved { .. } => unreachable!(),
-            Assist::Resolved { action, .. } => action,
+            Assist::Resolved { assist } => assist.get_first_action(),
         };
 
         let range = action.target.expect("expected target on action");
@@ -293,10 +310,10 @@ mod tests {
         let mut assists = assists.iter();
 
         assert_eq!(
-            assists.next().expect("expected assist").0.label,
+            assists.next().expect("expected assist").label.label,
             "Change visibility to pub(crate)"
         );
-        assert_eq!(assists.next().expect("expected assist").0.label, "Add `#[derive]`");
+        assert_eq!(assists.next().expect("expected assist").label.label, "Add `#[derive]`");
     }
 
     #[test]
@@ -315,7 +332,7 @@ mod tests {
         let assists = super::assists(&db, frange);
         let mut assists = assists.iter();
 
-        assert_eq!(assists.next().expect("expected assist").0.label, "Extract into variable");
-        assert_eq!(assists.next().expect("expected assist").0.label, "Replace with match");
+        assert_eq!(assists.next().expect("expected assist").label.label, "Extract into variable");
+        assert_eq!(assists.next().expect("expected assist").label.label, "Replace with match");
     }
 }
diff --git a/crates/ra_ide/src/assists.rs b/crates/ra_ide/src/assists.rs
index e00589733b..a936900da3 100644
--- a/crates/ra_ide/src/assists.rs
+++ b/crates/ra_ide/src/assists.rs
@@ -2,27 +2,53 @@
 
 use ra_db::{FilePosition, FileRange};
 
-use crate::{db::RootDatabase, SourceChange, SourceFileEdit};
+use crate::{db::RootDatabase, FileId, SourceChange, SourceFileEdit};
 
+use either::Either;
 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 change_data: Either<SourceChange, Vec<SourceChange>>,
 }
 
 pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec<Assist> {
     ra_assists::assists(db, frange)
         .into_iter()
-        .map(|(label, action)| {
+        .map(|assist| {
             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 }
+            let assist_label = &assist.label;
+            Assist {
+                id: assist_label.id,
+                label: assist_label.label.clone(),
+                change_data: match assist.action_data {
+                    Either::Left(action) => {
+                        Either::Left(action_to_edit(action, file_id, assist_label))
+                    }
+                    Either::Right(actions) => Either::Right(
+                        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 }))
+}
diff --git a/crates/ra_lsp_server/Cargo.toml b/crates/ra_lsp_server/Cargo.toml
index c08e67b8e6..579158780b 100644
--- a/crates/ra_lsp_server/Cargo.toml
+++ b/crates/ra_lsp_server/Cargo.toml
@@ -28,6 +28,7 @@ ra_prof = { path = "../ra_prof" }
 ra_vfs_glob = { path = "../ra_vfs_glob" }
 env_logger = { version = "0.7.1", default-features = false, features = ["humantime"] }
 ra_cargo_watch = { path = "../ra_cargo_watch" }
+either = "1.5"
 
 [dev-dependencies]
 tempfile = "3"
diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs
index f2db575eab..9e99648808 100644
--- a/crates/ra_lsp_server/src/main_loop/handlers.rs
+++ b/crates/ra_lsp_server/src/main_loop/handlers.rs
@@ -3,6 +3,7 @@
 
 use std::{fmt::Write as _, io::Write as _};
 
+use either::Either;
 use lsp_server::ErrorCode;
 use lsp_types::{
     CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
@@ -644,7 +645,6 @@ pub fn handle_code_action(
     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();
 
@@ -697,15 +697,27 @@ pub fn handle_code_action(
         res.push(action.into());
     }
 
-    for assist in assists {
-        let title = assist.change.label.clone();
-        let edit = assist.change.try_conv_with(&world)?;
+    for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() {
+        let title = assist.label.clone();
 
-        let command = Command {
-            title,
-            command: "rust-analyzer.applySourceChange".to_string(),
-            arguments: Some(vec![to_value(edit).unwrap()]),
+        let command = match assist.change_data {
+            Either::Left(change) => Command {
+                title,
+                command: "rust-analyzer.applySourceChange".to_string(),
+                arguments: Some(vec![to_value(change.try_conv_with(&world)?)?]),
+            },
+            Either::Right(changes) => Command {
+                title,
+                command: "rust-analyzer.selectAndApplySourceChange".to_string(),
+                arguments: Some(vec![to_value(
+                    changes
+                        .into_iter()
+                        .map(|change| change.try_conv_with(&world))
+                        .collect::<Result<Vec<_>>>()?,
+                )?]),
+            },
         };
+
         let action = CodeAction {
             title: command.title.clone(),
             kind: match assist.id {
diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts
index 9a1697dcbe..dc075aa828 100644
--- a/editors/code/src/commands/index.ts
+++ b/editors/code/src/commands/index.ts
@@ -39,6 +39,18 @@ function applySourceChange(ctx: Ctx): Cmd {
     };
 }
 
+function selectAndApplySourceChange(ctx: Ctx): Cmd {
+    return async (changes: sourceChange.SourceChange[]) => {
+        if (changes.length === 1) {
+            await sourceChange.applySourceChange(ctx, changes[0]);
+        } else if (changes.length > 0) {
+            const selectedChange = await vscode.window.showQuickPick(changes);
+            if (!selectedChange) return;
+            await sourceChange.applySourceChange(ctx, selectedChange);
+        }
+    };
+}
+
 function reload(ctx: Ctx): Cmd {
     return async () => {
         vscode.window.showInformationMessage('Reloading rust-analyzer...');
@@ -59,5 +71,6 @@ export {
     runSingle,
     showReferences,
     applySourceChange,
+    selectAndApplySourceChange,
     reload
 };
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 430ad31b4c..0494ccf63f 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -26,6 +26,7 @@ export async function activate(context: vscode.ExtensionContext) {
     ctx.registerCommand('runSingle', commands.runSingle);
     ctx.registerCommand('showReferences', commands.showReferences);
     ctx.registerCommand('applySourceChange', commands.applySourceChange);
+    ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);
 
     if (ctx.config.enableEnhancedTyping) {
         ctx.overrideCommand('type', commands.onEnter);


Test patch:
diff --git a/crates/ra_assists/src/doc_tests.rs b/crates/ra_assists/src/doc_tests.rs
index a8f8446cb6..5dc1ee2337 100644
--- a/crates/ra_assists/src/doc_tests.rs
+++ b/crates/ra_assists/src/doc_tests.rs
@@ -15,21 +15,21 @@ fn check(assist_id: &str, before: &str, after: &str) {
     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 = crate::assists(&db, frange)
         .into_iter()
-        .find(|(id, _)| id.id.0 == assist_id)
+        .find(|assist| assist.label.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(|assist| assist.label.id.0)
                     .collect::<Vec<_>>()
                     .join(", ")
             )
         });
 
-    let actual = action.edit.apply(&before);
+    let actual = assist.get_first_action().edit.apply(&before);
     assert_eq_text!(after, &actual);
 }