Repository: rust-analyzer/rust-analyzer
Pull request #416 (2019-01-03 16:00:04)

assist-builder


Patch:
diff --git a/crates/ra_analysis/src/imp.rs b/crates/ra_analysis/src/imp.rs
index 136e7f7dce..771dad4755 100644
--- a/crates/ra_analysis/src/imp.rs
+++ b/crates/ra_analysis/src/imp.rs
@@ -333,19 +333,9 @@ impl db::RootDatabase {
 
     pub(crate) fn assists(&self, frange: FileRange) -> Vec<SourceChange> {
         let file = self.source_file(frange.file_id);
-        let offset = frange.range.start();
-        let actions = vec![
-            assists::flip_comma(&file, offset).map(|f| f()),
-            assists::add_derive(&file, offset).map(|f| f()),
-            assists::add_impl(&file, offset).map(|f| f()),
-            assists::change_visibility(&file, offset).map(|f| f()),
-            assists::introduce_variable(&file, frange.range).map(|f| f()),
-        ];
-        actions
+        assists::assists(&file, frange.range)
             .into_iter()
-            .filter_map(|local_edit| {
-                Some(SourceChange::from_local_edit(frange.file_id, local_edit?))
-            })
+            .map(|local_edit| SourceChange::from_local_edit(frange.file_id, local_edit))
             .collect()
     }
 
@@ -440,7 +430,7 @@ impl db::RootDatabase {
             .map(|(file_id, text_range)| SourceFileEdit {
                 file_id: *file_id,
                 edit: {
-                    let mut builder = ra_text_edit::TextEditBuilder::new();
+                    let mut builder = ra_text_edit::TextEditBuilder::default();
                     builder.replace(*text_range, new_name.into());
                     builder.finish()
                 },
diff --git a/crates/ra_editor/src/assists.rs b/crates/ra_editor/src/assists.rs
index b6e6dd628e..cc40ee4c8f 100644
--- a/crates/ra_editor/src/assists.rs
+++ b/crates/ra_editor/src/assists.rs
@@ -9,8 +9,13 @@ mod add_impl;
 mod introduce_variable;
 mod change_visibility;
 
-use ra_text_edit::TextEdit;
-use ra_syntax::{Direction, SyntaxNodeRef, TextUnit};
+use ra_text_edit::{TextEdit, TextEditBuilder};
+use ra_syntax::{
+    Direction, SyntaxNodeRef, TextUnit, TextRange,SourceFileNode, AstNode,
+    algo::{find_leaf_at_offset, find_covering_node, LeafAtOffset},
+};
+
+use crate::find_node_at_offset;
 
 pub use self::{
     flip_comma::flip_comma,
@@ -20,6 +25,21 @@ pub use self::{
     change_visibility::change_visibility,
 };
 
+/// Return all the assists applicable at the given position.
+pub fn assists(file: &SourceFileNode, range: TextRange) -> Vec<LocalEdit> {
+    let ctx = AssistCtx::new(file, range);
+    [
+        flip_comma,
+        add_derive,
+        add_impl,
+        introduce_variable,
+        change_visibility,
+    ]
+    .iter()
+    .filter_map(|&assist| ctx.clone().apply(assist))
+    .collect()
+}
+
 #[derive(Debug)]
 pub struct LocalEdit {
     pub label: String,
@@ -32,3 +52,134 @@ fn non_trivia_sibling(node: SyntaxNodeRef, direction: Direction) -> Option<Synta
         .skip(1)
         .find(|node| !node.kind().is_trivia())
 }
+
+/// `AssistCtx` allows to apply an assist or check if it could be applied.
+///
+/// Assists use a somewhat overengeneered approach, given the current needs. The
+/// assists workflow consists of two phases. In the first phase, a user asks for
+/// the list of available assists. In the second phase, the user picks a
+/// particular assist and it gets applied.
+///
+/// There are two peculiarities here:
+///
+/// * first, we ideally avoid computing more things then neccessary to answer
+///   "is assist applicable" in the first phase.
+/// * second, when we are appling assist, we don't have a gurantee that there
+///   weren't any changes between the point when user asked for assists and when
+///   they applied a particular assist. So, when applying assist, we need to do
+///   all the checks from scratch.
+///
+/// To avoid repeating the same code twice for both "check" and "apply"
+/// functions, we use an approach remeniscent of that of Django's function based
+/// views dealing with forms. Each assist receives a runtime parameter,
+/// `should_compute_edit`. It first check if an edit is applicable (potentially
+/// computing info required to compute the actual edit). If it is applicable,
+/// and `should_compute_edit` is `true`, it then computes the actual edit.
+///
+/// So, to implement the original assists workflow, we can first apply each edit
+/// with `should_compute_edit = false`, and then applying the selected edit
+/// again, with `should_compute_edit = true` this time.
+///
+/// Note, however, that we don't actually use such two-phase logic at the
+/// moment, because the LSP API is pretty awkward in this place, and it's much
+/// easier to just compute the edit eagarly :-)
+#[derive(Debug, Clone)]
+pub struct AssistCtx<'a> {
+    source_file: &'a SourceFileNode,
+    range: TextRange,
+    should_compute_edit: bool,
+}
+
+#[derive(Debug)]
+pub enum Assist {
+    Applicable,
+    Edit(LocalEdit),
+}
+
+#[derive(Default)]
+struct AssistBuilder {
+    edit: TextEditBuilder,
+    cursor_position: Option<TextUnit>,
+}
+
+impl<'a> AssistCtx<'a> {
+    pub fn new(source_file: &'a SourceFileNode, range: TextRange) -> AssistCtx {
+        AssistCtx {
+            source_file,
+            range,
+            should_compute_edit: false,
+        }
+    }
+
+    pub fn apply(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> Option<LocalEdit> {
+        self.should_compute_edit = true;
+        match assist(self) {
+            None => None,
+            Some(Assist::Edit(e)) => Some(e),
+            Some(Assist::Applicable) => unreachable!(),
+        }
+    }
+
+    pub fn check(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> bool {
+        self.should_compute_edit = false;
+        match assist(self) {
+            None => false,
+            Some(Assist::Edit(_)) => unreachable!(),
+            Some(Assist::Applicable) => true,
+        }
+    }
+
+    fn build(self, label: impl Into<String>, f: impl FnOnce(&mut AssistBuilder)) -> Option<Assist> {
+        if !self.should_compute_edit {
+            return Some(Assist::Applicable);
+        }
+        let mut edit = AssistBuilder::default();
+        f(&mut edit);
+        Some(Assist::Edit(LocalEdit {
+            label: label.into(),
+            edit: edit.edit.finish(),
+            cursor_position: edit.cursor_position,
+        }))
+    }
+
+    pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<SyntaxNodeRef<'a>> {
+        find_leaf_at_offset(self.source_file.syntax(), self.range.start())
+    }
+    pub(crate) fn node_at_offset<N: AstNode<'a>>(&self) -> Option<N> {
+        find_node_at_offset(self.source_file.syntax(), self.range.start())
+    }
+    pub(crate) fn covering_node(&self) -> SyntaxNodeRef<'a> {
+        find_covering_node(self.source_file.syntax(), self.range)
+    }
+}
+
+impl AssistBuilder {
+    fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
+        self.edit.replace(range, replace_with.into())
+    }
+    #[allow(unused)]
+    fn delete(&mut self, range: TextRange) {
+        self.edit.delete(range)
+    }
+    fn insert(&mut self, offset: TextUnit, text: impl Into<String>) {
+        self.edit.insert(offset, text.into())
+    }
+    fn set_cursor(&mut self, offset: TextUnit) {
+        self.cursor_position = Some(offset)
+    }
+}
+
+#[cfg(test)]
+fn check_assist(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) {
+    crate::test_utils::check_action(before, after, |file, off| {
+        let range = TextRange::offset_len(off, 0.into());
+        AssistCtx::new(file, range).apply(assist)
+    })
+}
+
+#[cfg(test)]
+fn check_assist_range(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) {
+    crate::test_utils::check_action_range(before, after, |file, range| {
+        AssistCtx::new(file, range).apply(assist)
+    })
+}
diff --git a/crates/ra_editor/src/assists/add_derive.rs b/crates/ra_editor/src/assists/add_derive.rs
index 33d9d2c313..1e2cd4f300 100644
--- a/crates/ra_editor/src/assists/add_derive.rs
+++ b/crates/ra_editor/src/assists/add_derive.rs
@@ -1,85 +1,73 @@
-use ra_text_edit::TextEditBuilder;
 use ra_syntax::{
     ast::{self, AstNode, AttrsOwner},
-    SourceFileNode,
     SyntaxKind::{WHITESPACE, COMMENT},
     TextUnit,
 };
 
-use crate::{
-    find_node_at_offset,
-    assists::LocalEdit,
-};
+use crate::assists::{AssistCtx, Assist};
 
-pub fn add_derive<'a>(
-    file: &'a SourceFileNode,
-    offset: TextUnit,
-) -> Option<impl FnOnce() -> LocalEdit + 'a> {
-    let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?;
+pub fn add_derive(ctx: AssistCtx) -> Option<Assist> {
+    let nominal = ctx.node_at_offset::<ast::NominalDef>()?;
     let node_start = derive_insertion_offset(nominal)?;
-    return Some(move || {
+    ctx.build("add `#[derive]`", |edit| {
         let derive_attr = nominal
             .attrs()
             .filter_map(|x| x.as_call())
             .filter(|(name, _arg)| name == "derive")
             .map(|(_name, arg)| arg)
             .next();
-        let mut edit = TextEditBuilder::new();
         let offset = match derive_attr {
             None => {
-                edit.insert(node_start, "#[derive()]\n".to_string());
+                edit.insert(node_start, "#[derive()]\n");
                 node_start + TextUnit::of_str("#[derive(")
             }
             Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'),
         };
-        LocalEdit {
-            label: "add `#[derive]`".to_string(),
-            edit: edit.finish(),
-            cursor_position: Some(offset),
-        }
-    });
+        edit.set_cursor(offset)
+    })
+}
 
-    // Insert `derive` after doc comments.
-    fn derive_insertion_offset(nominal: ast::NominalDef) -> Option<TextUnit> {
-        let non_ws_child = nominal
-            .syntax()
-            .children()
-            .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?;
-        Some(non_ws_child.range().start())
-    }
+// Insert `derive` after doc comments.
+fn derive_insertion_offset(nominal: ast::NominalDef) -> Option<TextUnit> {
+    let non_ws_child = nominal
+        .syntax()
+        .children()
+        .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?;
+    Some(non_ws_child.range().start())
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_utils::check_action;
+    use crate::assists::check_assist;
 
     #[test]
     fn add_derive_new() {
-        check_action(
+        check_assist(
+            add_derive,
             "struct Foo { a: i32, <|>}",
             "#[derive(<|>)]\nstruct Foo { a: i32, }",
-            |file, off| add_derive(file, off).map(|f| f()),
         );
-        check_action(
+        check_assist(
+            add_derive,
             "struct Foo { <|> a: i32, }",
             "#[derive(<|>)]\nstruct Foo {  a: i32, }",
-            |file, off| add_derive(file, off).map(|f| f()),
         );
     }
 
     #[test]
     fn add_derive_existing() {
-        check_action(
+        check_assist(
+            add_derive,
             "#[derive(Clone)]\nstruct Foo { a: i32<|>, }",
             "#[derive(Clone<|>)]\nstruct Foo { a: i32, }",
-            |file, off| add_derive(file, off).map(|f| f()),
         );
     }
 
     #[test]
     fn add_derive_new_with_doc_comment() {
-        check_action(
+        check_assist(
+            add_derive,
             "
 /// `Foo` is a pretty important struct.
 /// It does stuff.
@@ -91,7 +79,6 @@ struct Foo { a: i32<|>, }
 #[derive(<|>)]
 struct Foo { a: i32, }
             ",
-            |file, off| add_derive(file, off).map(|f| f()),
         );
     }
 }
diff --git a/crates/ra_editor/src/assists/add_impl.rs b/crates/ra_editor/src/assists/add_impl.rs
index 50e00688e5..9353e2717b 100644
--- a/crates/ra_editor/src/assists/add_impl.rs
+++ b/crates/ra_editor/src/assists/add_impl.rs
@@ -1,23 +1,16 @@
 use join_to_string::join;
-use ra_text_edit::TextEditBuilder;
 use ra_syntax::{
     ast::{self, AstNode, NameOwner, TypeParamsOwner},
-    SourceFileNode,
     TextUnit,
 };
 
-use crate::{find_node_at_offset, assists::LocalEdit};
+use crate::assists::{AssistCtx, Assist};
 
-pub fn add_impl<'a>(
-    file: &'a SourceFileNode,
-    offset: TextUnit,
-) -> Option<impl FnOnce() -> LocalEdit + 'a> {
-    let nominal = find_node_at_offset::<ast::NominalDef>(file.syntax(), offset)?;
+pub fn add_impl(ctx: AssistCtx) -> Option<Assist> {
+    let nominal = ctx.node_at_offset::<ast::NominalDef>()?;
     let name = nominal.name()?;
-
-    Some(move || {
+    ctx.build("add impl", |edit| {
         let type_params = nominal.type_param_list();
-        let mut edit = TextEditBuilder::new();
         let start_offset = nominal.syntax().range().end();
         let mut buf = String::new();
         buf.push_str("\n\nimpl");
@@ -40,38 +33,33 @@ pub fn add_impl<'a>(
                 .to_buf(&mut buf);
         }
         buf.push_str(" {\n");
-        let offset = start_offset + TextUnit::of_str(&buf);
+        edit.set_cursor(start_offset + TextUnit::of_str(&buf));
         buf.push_str("\n}");
         edit.insert(start_offset, buf);
-        LocalEdit {
-            label: "add impl".to_string(),
-            edit: edit.finish(),
-            cursor_position: Some(offset),
-        }
     })
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_utils::check_action;
+    use crate::assists::check_assist;
 
     #[test]
     fn test_add_impl() {
-        check_action(
+        check_assist(
+            add_impl,
             "struct Foo {<|>}\n",
             "struct Foo {}\n\nimpl Foo {\n<|>\n}\n",
-            |file, off| add_impl(file, off).map(|f| f()),
         );
-        check_action(
+        check_assist(
+            add_impl,
             "struct Foo<T: Clone> {<|>}",
             "struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n<|>\n}",
-            |file, off| add_impl(file, off).map(|f| f()),
         );
-        check_action(
+        check_assist(
+            add_impl,
             "struct Foo<'a, T: Foo<'a>> {<|>}",
             "struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}",
-            |file, off| add_impl(file, off).map(|f| f()),
         );
     }
 
diff --git a/crates/ra_editor/src/assists/change_visibility.rs b/crates/ra_editor/src/assists/change_visibility.rs
index 98c218f329..e2cd9ffd16 100644
--- a/crates/ra_editor/src/assists/change_visibility.rs
+++ b/crates/ra_editor/src/assists/change_visibility.rs
@@ -1,90 +1,69 @@
-use ra_text_edit::TextEditBuilder;
 use ra_syntax::{
-    SourceFileNode,
-    algo::find_leaf_at_offset,
     SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF},
-    TextUnit,
 };
 
-use crate::assists::LocalEdit;
+use crate::assists::{AssistCtx, Assist};
 
-pub fn change_visibility<'a>(
-    file: &'a SourceFileNode,
-    offset: TextUnit,
-) -> Option<impl FnOnce() -> LocalEdit + 'a> {
-    let syntax = file.syntax();
-
-    let keyword = find_leaf_at_offset(syntax, offset).find(|leaf| match leaf.kind() {
+pub fn change_visibility(ctx: AssistCtx) -> Option<Assist> {
+    let keyword = ctx.leaf_at_offset().find(|leaf| match leaf.kind() {
         FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true,
         _ => false,
     })?;
     let parent = keyword.parent()?;
     let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF];
-    let node_start = parent.range().start();
-    Some(move || {
-        let mut edit = TextEditBuilder::new();
-
-        if !def_kws.iter().any(|&def_kw| def_kw == parent.kind())
-            || parent.children().any(|child| child.kind() == VISIBILITY)
-        {
-            return LocalEdit {
-                label: "make pub crate".to_string(),
-                edit: edit.finish(),
-                cursor_position: Some(offset),
-            };
-        }
+    // Parent is not a definition, can't add visibility
+    if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) {
+        return None;
+    }
+    // Already have visibility, do nothing
+    if parent.children().any(|child| child.kind() == VISIBILITY) {
+        return None;
+    }
 
-        edit.insert(node_start, "pub(crate) ".to_string());
-        LocalEdit {
-            label: "make pub crate".to_string(),
-            edit: edit.finish(),
-            cursor_position: Some(node_start),
-        }
+    let node_start = parent.range().start();
+    ctx.build("make pub crate", |edit| {
+        edit.insert(node_start, "pub(crate) ");
+        edit.set_cursor(node_start);
     })
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_utils::check_action;
+    use crate::assists::check_assist;
 
     #[test]
     fn test_change_visibility() {
-        check_action(
+        check_assist(
+            change_visibility,
             "<|>fn foo() {}",
             "<|>pub(crate) fn foo() {}",
-            |file, off| change_visibility(file, off).map(|f| f()),
         );
-        check_action(
+        check_assist(
+            change_visibility,
             "f<|>n foo() {}",
             "<|>pub(crate) fn foo() {}",
-            |file, off| change_visibility(file, off).map(|f| f()),
         );
-        check_action(
+        check_assist(
+            change_visibility,
             "<|>struct Foo {}",
             "<|>pub(crate) struct Foo {}",
-            |file, off| change_visibility(file, off).map(|f| f()),
         );
-        check_action("<|>mod foo {}", "<|>pub(crate) mod foo {}", |file, off| {
-            change_visibility(file, off).map(|f| f())
-        });
-        check_action(
+        check_assist(
+            change_visibility,
+            "<|>mod foo {}",
+            "<|>pub(crate) mod foo {}",
+        );
+        check_assist(
+            change_visibility,
             "<|>trait Foo {}",
             "<|>pub(crate) trait Foo {}",
-            |file, off| change_visibility(file, off).map(|f| f()),
-        );
-        check_action("m<|>od {}", "<|>pub(crate) mod {}", |file, off| {
-            change_visibility(file, off).map(|f| f())
-        });
-        check_action(
-            "pub(crate) f<|>n foo() {}",
-            "pub(crate) f<|>n foo() {}",
-            |file, off| change_visibility(file, off).map(|f| f()),
         );
-        check_action(
+        check_assist(change_visibility, "m<|>od {}", "<|>pub(crate) mod {}");
+        check_assist(
+            change_visibility,
             "unsafe f<|>n foo() {}",
             "<|>pub(crate) unsafe fn foo() {}",
-            |file, off| change_visibility(file, off).map(|f| f()),
         );
     }
 }
diff --git a/crates/ra_editor/src/assists/flip_comma.rs b/crates/ra_editor/src/assists/flip_comma.rs
index d8727db0d6..a343413cc5 100644
--- a/crates/ra_editor/src/assists/flip_comma.rs
+++ b/crates/ra_editor/src/assists/flip_comma.rs
@@ -1,45 +1,31 @@
-use ra_text_edit::TextEditBuilder;
 use ra_syntax::{
-    algo::find_leaf_at_offset,
-    Direction, SourceFileNode,
+    Direction,
     SyntaxKind::COMMA,
-    TextUnit,
 };
 
-use crate::assists::{LocalEdit, non_trivia_sibling};
+use crate::assists::{non_trivia_sibling, AssistCtx, Assist};
 
-pub fn flip_comma<'a>(
-    file: &'a SourceFileNode,
-    offset: TextUnit,
-) -> Option<impl FnOnce() -> LocalEdit + 'a> {
-    let syntax = file.syntax();
-
-    let comma = find_leaf_at_offset(syntax, offset).find(|leaf| leaf.kind() == COMMA)?;
+pub fn flip_comma(ctx: AssistCtx) -> Option<Assist> {
+    let comma = ctx.leaf_at_offset().find(|leaf| leaf.kind() == COMMA)?;
     let prev = non_trivia_sibling(comma, Direction::Prev)?;
     let next = non_trivia_sibling(comma, Direction::Next)?;
-    Some(move || {
-        let mut edit = TextEditBuilder::new();
-        edit.replace(prev.range(), next.text().to_string());
-        edit.replace(next.range(), prev.text().to_string());
-        LocalEdit {
-            label: "flip comma".to_string(),
-            edit: edit.finish(),
-            cursor_position: None,
-        }
+    ctx.build("flip comma", |edit| {
+        edit.replace(prev.range(), next.text());
+        edit.replace(next.range(), prev.text());
     })
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_utils::check_action;
+    use crate::assists::check_assist;
 
     #[test]
-    fn test_swap_comma() {
-        check_action(
+    fn flip_comma_works_for_function_parameters() {
+        check_assist(
+            flip_comma,
             "fn foo(x: i32,<|> y: Result<(), ()>) {}",
             "fn foo(y: Result<(), ()>,<|> x: i32) {}",
-            |file, off| flip_comma(file, off).map(|f| f()),
         )
     }
 }
diff --git a/crates/ra_editor/src/assists/introduce_variable.rs b/crates/ra_editor/src/assists/introduce_variable.rs
index 17ab521fac..782861023e 100644
--- a/crates/ra_editor/src/assists/introduce_variable.rs
+++ b/crates/ra_editor/src/assists/introduce_variable.rs
@@ -1,19 +1,13 @@
-use ra_text_edit::TextEditBuilder;
 use ra_syntax::{
-    algo::{find_covering_node},
     ast::{self, AstNode},
-    SourceFileNode,
-    SyntaxKind::{WHITESPACE},
-    SyntaxNodeRef, TextRange, TextUnit,
+    SyntaxKind::WHITESPACE,
+    SyntaxNodeRef, TextUnit,
 };
 
-use crate::assists::LocalEdit;
+use crate::assists::{AssistCtx, Assist};
 
-pub fn introduce_variable<'a>(
-    file: &'a SourceFileNode,
-    range: TextRange,
-) -> Option<impl FnOnce() -> LocalEdit + 'a> {
-    let node = find_covering_node(file.syntax(), range);
+pub fn introduce_variable<'a>(ctx: AssistCtx) -> Option<Assist> {
+    let node = ctx.covering_node();
     let expr = node.ancestors().filter_map(ast::Expr::cast).next()?;
 
     let anchor_stmt = anchor_stmt(expr)?;
@@ -21,9 +15,8 @@ pub fn introduce_variable<'a>(
     if indent.kind() != WHITESPACE {
         return None;
     }
-    return Some(move || {
+    ctx.build("introduce variable", move |edit| {
         let mut buf = String::new();
-        let mut edit = TextEditBuilder::new();
 
         buf.push_str("let var_name = ");
         expr.syntax().text().push_to(&mut buf);
@@ -40,43 +33,39 @@ pub fn introduce_variable<'a>(
             edit.replace(expr.syntax().range(), "var_name".to_string());
             edit.insert(anchor_stmt.range().start(), buf);
         }
-        let cursor_position = anchor_stmt.range().start() + TextUnit::of_str("let ");
-        LocalEdit {
-            label: "introduce variable".to_string(),
-            edit: edit.finish(),
-            cursor_position: Some(cursor_position),
-        }
-    });
+        edit.set_cursor(anchor_stmt.range().start() + TextUnit::of_str("let "));
+    })
+}
 
-    /// Statement or last in the block expression, which will follow
-    /// the freshly introduced var.
-    fn anchor_stmt(expr: ast::Expr) -> Option<SyntaxNodeRef> {
-        expr.syntax().ancestors().find(|&node| {
-            if ast::Stmt::cast(node).is_some() {
+/// Statement or last in the block expression, which will follow
+/// the freshly introduced var.
+fn anchor_stmt(expr: ast::Expr) -> Option<SyntaxNodeRef> {
+    expr.syntax().ancestors().find(|&node| {
+        if ast::Stmt::cast(node).is_some() {
+            return true;
+        }
+        if let Some(expr) = node
+            .parent()
+            .and_then(ast::Block::cast)
+            .and_then(|it| it.expr())
+        {
+            if expr.syntax() == node {
                 return true;
             }
-            if let Some(expr) = node
-                .parent()
-                .and_then(ast::Block::cast)
-                .and_then(|it| it.expr())
-            {
-                if expr.syntax() == node {
-                    return true;
-                }
-            }
-            false
-        })
-    }
+        }
+        false
+    })
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test_utils::check_action_range;
+    use crate::assists::check_assist_range;
 
     #[test]
     fn test_introduce_var_simple() {
-        check_action_range(
+        check_assist_range(
+            introduce_variable,
             "
 fn foo() {
     foo(<|>1 + 1<|>);
@@ -86,13 +75,13 @@ fn foo() {
     let <|>var_name = 1 + 1;
     foo(var_name);
 }",
-            |file, range| introduce_variable(file, range).map(|f| f()),
         );
     }
 
     #[test]
     fn test_introduce_var_expr_stmt() {
-        check_action_range(
+        check_assist_range(
+            introduce_variable,
             "
 fn foo() {
     <|>1 + 1<|>;
@@ -101,13 +90,13 @@ fn foo() {
 fn foo() {
     let <|>var_name = 1 + 1;
 }",
-            |file, range| introduce_variable(file, range).map(|f| f()),
         );
     }
 
     #[test]
     fn test_introduce_var_part_of_expr_stmt() {
-        check_action_range(
+        check_assist_range(
+            introduce_variable,
             "
 fn foo() {
     <|>1<|> + 1;
@@ -117,13 +106,13 @@ fn foo() {
     let <|>var_name = 1;
     var_name + 1;
 }",
-            |file, range| introduce_variable(file, range).map(|f| f()),
         );
     }
 
     #[test]
     fn test_introduce_var_last_expr() {
-        check_action_range(
+        check_assist_range(
+            introduce_variable,
             "
 fn foo() {
     bar(<|>1 + 1<|>)
@@ -133,13 +122,13 @@ fn foo() {
     let <|>var_name = 1 + 1;
     bar(var_name)
 }",
-            |file, range| introduce_variable(file, range).map(|f| f()),
         );
     }
 
     #[test]
     fn test_introduce_var_last_full_expr() {
-        check_action_range(
+        check_assist_range(
+            introduce_variable,
             "
 fn foo() {
     <|>bar(1 + 1)<|>
@@ -149,7 +138,6 @@ fn foo() {
     let <|>var_name = bar(1 + 1);
     var_name
 }",
-            |file, range| introduce_variable(file, range).map(|f| f()),
         );
     }
 
diff --git a/crates/ra_editor/src/diagnostics.rs b/crates/ra_editor/src/diagnostics.rs
index 1b336cfe2e..199b0e5029 100644
--- a/crates/ra_editor/src/diagnostics.rs
+++ b/crates/ra_editor/src/diagnostics.rs
@@ -57,7 +57,7 @@ fn check_unnecessary_braces_in_use_statement(
             text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(single_use_tree)
                 .unwrap_or_else(|| {
                     let to_replace = single_use_tree.syntax().text().to_string();
-                    let mut edit_builder = TextEditBuilder::new();
+                    let mut edit_builder = TextEditBuilder::default();
                     edit_builder.delete(range);
                     edit_builder.insert(range.start(), to_replace);
                     edit_builder.finish()
@@ -93,7 +93,7 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
         let start = use_tree_list_node.prev_sibling()?.range().start();
         let end = use_tree_list_node.range().end();
         let range = TextRange::from_to(start, end);
-        let mut edit_builder = TextEditBuilder::new();
+        let mut edit_builder = TextEditBuilder::default();
         edit_builder.delete(range);
         return Some(edit_builder.finish());
     }
@@ -111,7 +111,7 @@ fn check_struct_shorthand_initialization(
             let field_name = name_ref.syntax().text().to_string();
             let field_expr = expr.syntax().text().to_string();
             if field_name == field_expr {
-                let mut edit_builder = TextEditBuilder::new();
+                let mut edit_builder = TextEditBuilder::default();
                 edit_builder.delete(named_field.syntax().range());
                 edit_builder.insert(named_field.syntax().range().start(), field_name);
                 let edit = edit_builder.finish();
diff --git a/crates/ra_editor/src/typing.rs b/crates/ra_editor/src/typing.rs
index 21d068a7bd..dd3d0f260d 100644
--- a/crates/ra_editor/src/typing.rs
+++ b/crates/ra_editor/src/typing.rs
@@ -21,7 +21,7 @@ pub fn join_lines(file: &SourceFileNode, range: TextRange) -> LocalEdit {
             None => {
                 return LocalEdit {
                     label: "join lines".to_string(),
-                    edit: TextEditBuilder::new().finish(),
+                    edit: TextEditBuilder::default().finish(),
                     cursor_position: None,
                 };
             }
@@ -33,7 +33,7 @@ pub fn join_lines(file: &SourceFileNode, range: TextRange) -> LocalEdit {
     };
 
     let node = find_covering_node(file.syntax(), range);
-    let mut edit = TextEditBuilder::new();
+    let mut edit = TextEditBuilder::default();
     for node in node.descendants() {
         let text = match node.leaf_text() {
             Some(text) => text,
@@ -76,7 +76,7 @@ pub fn on_enter(file: &SourceFileNode, offset: TextUnit) -> Option<LocalEdit> {
     let indent = node_indent(file, comment.syntax())?;
     let inserted = format!("\n{}{} ", indent, prefix);
     let cursor_position = offset + TextUnit::of_str(&inserted);
-    let mut edit = TextEditBuilder::new();
+    let mut edit = TextEditBuilder::default();
     edit.insert(offset, inserted);
     Some(LocalEdit {
         label: "on enter".to_string(),
@@ -127,7 +127,7 @@ pub fn on_eq_typed(file: &SourceFileNode, offset: TextUnit) -> Option<LocalEdit>
         return None;
     }
     let offset = let_stmt.syntax().range().end();
-    let mut edit = TextEditBuilder::new();
+    let mut edit = TextEditBuilder::default();
     edit.insert(offset, ";".to_string());
     Some(LocalEdit {
         label: "add semicolon".to_string(),
diff --git a/crates/ra_lsp_server/src/conv.rs b/crates/ra_lsp_server/src/conv.rs
index 7230fb101b..1107ffc8b9 100644
--- a/crates/ra_lsp_server/src/conv.rs
+++ b/crates/ra_lsp_server/src/conv.rs
@@ -1,16 +1,11 @@
 use languageserver_types::{
-    self, CreateFile, DocumentChangeOperation, DocumentChanges, InsertTextFormat, Location,
-    Position, Range, RenameFile, ResourceOp, SymbolKind, TextDocumentEdit, TextDocumentIdentifier,
-    TextDocumentItem, TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier,
-    WorkspaceEdit,
+    self, Location, Position, Range, SymbolKind, TextDocumentEdit, TextDocumentIdentifier,
+    TextDocumentItem, TextDocumentPositionParams, Url, VersionedTextDocumentIdentifier, InsertTextFormat,
 };
-use ra_analysis::{
-    CompletionItem, CompletionItemKind, FileId, FilePosition, FileRange, FileSystemEdit,
-    InsertText, NavigationTarget, SourceChange, SourceFileEdit,
-};
-use ra_editor::{translate_offset_with_edit, LineCol, LineIndex};
-use ra_syntax::{SyntaxKind, TextRange, TextUnit};
+use ra_analysis::{FileId, FileSystemEdit, SourceChange, SourceFileEdit, FilePosition,FileRange,  CompletionItem, CompletionItemKind, InsertText, NavigationTarget};
+use ra_editor::{LineCol, LineIndex, translate_offset_with_edit};
 use ra_text_edit::{AtomTextEdit, TextEdit};
+use ra_syntax::{SyntaxKind, TextRange, TextUnit};
 
 use crate::{req, server_world::ServerWorld, Result};
 
@@ -54,7 +49,7 @@ impl Conv for CompletionItemKind {
     type Output = ::languageserver_types::CompletionItemKind;
 
     fn conv(self) -> <Self as Conv>::Output {
-        use languageserver_types::CompletionItemKind::*;
+        use ::languageserver_types::CompletionItemKind::*;
         match self {
             CompletionItemKind::Keyword => Keyword,
             CompletionItemKind::Snippet => Snippet,
@@ -271,20 +266,12 @@ impl TryConvWith for SourceChange {
                 })
             }
         };
-        let mut document_changes: Vec<DocumentChangeOperation> = Vec::new();
-        for resource_op in self.file_system_edits.try_conv_with(world)? {
-            document_changes.push(DocumentChangeOperation::Op(resource_op));
-        }
-        for text_document_edit in self.source_file_edits.try_conv_with(world)? {
-            document_changes.push(DocumentChangeOperation::Edit(text_document_edit));
-        }
-        let workspace_edit = WorkspaceEdit {
-            changes: None,
-            document_changes: Some(DocumentChanges::Operations(document_changes)),
-        };
+        let source_file_edits = self.source_file_edits.try_conv_with(world)?;
+        let file_system_edits = self.file_system_edits.try_conv_with(world)?;
         Ok(req::SourceChange {
             label: self.label,
-            workspace_edit,
+            source_file_edits,
+            file_system_edits,
             cursor_position,
         })
     }
@@ -314,25 +301,21 @@ impl TryConvWith for SourceFileEdit {
 
 impl TryConvWith for FileSystemEdit {
     type Ctx = ServerWorld;
-    type Output = ResourceOp;
-    fn try_conv_with(self, world: &ServerWorld) -> Result<ResourceOp> {
+    type Output = req::FileSystemEdit;
+    fn try_conv_with(self, world: &ServerWorld) -> Result<req::FileSystemEdit> {
         let res = match self {
             FileSystemEdit::CreateFile { source_root, path } => {
-                let uri = world.path_to_uri(source_root, &path)?.to_string();
-                ResourceOp::Create(CreateFile { uri, options: None })
+                let uri = world.path_to_uri(source_root, &path)?;
+                req::FileSystemEdit::CreateFile { uri }
             }
             FileSystemEdit::MoveFile {
                 src,
                 dst_source_root,
                 dst_path,
             } => {
-                let old_uri = world.file_id_to_uri(src)?.to_string();
-                let new_uri = world.path_to_uri(dst_source_root, &dst_path)?.to_string();
-                ResourceOp::Rename(RenameFile {
-                    old_uri,
-                    new_uri,
-                    options: None,
-                })
+                let src = world.file_id_to_uri(src)?;
+                let dst = world.path_to_uri(dst_source_root, &dst_path)?;
+                req::FileSystemEdit::MoveFile { src, dst }
             }
         };
         Ok(res)
diff --git a/crates/ra_lsp_server/src/req.rs b/crates/ra_lsp_server/src/req.rs
index b41e903280..747ab8a8c9 100644
--- a/crates/ra_lsp_server/src/req.rs
+++ b/crates/ra_lsp_server/src/req.rs
@@ -1,6 +1,6 @@
+use serde::{Serialize, Deserialize};
 use languageserver_types::{Location, Position, Range, TextDocumentIdentifier, Url};
 use rustc_hash::FxHashMap;
-use serde::{Deserialize, Serialize};
 use url_serde;
 
 pub use languageserver_types::{
@@ -8,7 +8,7 @@ pub use languageserver_types::{
     CompletionResponse, DocumentOnTypeFormattingParams, DocumentSymbolParams,
     DocumentSymbolResponse, ExecuteCommandParams, Hover, InitializeResult,
     PublishDiagnosticsParams, ReferenceParams, SignatureHelp, TextDocumentEdit,
-    TextDocumentPositionParams, TextEdit, WorkspaceEdit, WorkspaceSymbolParams,
+    TextDocumentPositionParams, TextEdit, WorkspaceSymbolParams,
 };
 
 pub enum SyntaxTree {}
@@ -151,10 +151,26 @@ pub struct Runnable {
 #[serde(rename_all = "camelCase")]
 pub struct SourceChange {
     pub label: String,
-    pub workspace_edit: WorkspaceEdit,
+    pub source_file_edits: Vec<TextDocumentEdit>,
+    pub file_system_edits: Vec<FileSystemEdit>,
     pub cursor_position: Option<TextDocumentPositionParams>,
 }
 
+#[derive(Serialize, Debug)]
+#[serde(tag = "type", rename_all = "camelCase")]
+pub enum FileSystemEdit {
+    CreateFile {
+        #[serde(with = "url_serde")]
+        uri: Url,
+    },
+    MoveFile {
+        #[serde(with = "url_serde")]
+        src: Url,
+        #[serde(with = "url_serde")]
+        dst: Url,
+    },
+}
+
 pub enum InternalFeedback {}
 
 impl Notification for InternalFeedback {
diff --git a/crates/ra_syntax/src/yellow/syntax_text.rs b/crates/ra_syntax/src/yellow/syntax_text.rs
index 46bde9a086..783dca2147 100644
--- a/crates/ra_syntax/src/yellow/syntax_text.rs
+++ b/crates/ra_syntax/src/yellow/syntax_text.rs
@@ -119,3 +119,9 @@ impl SyntaxTextSlice for ops::Range<TextUnit> {
         TextRange::from_to(self.start, self.end).restrict(range)
     }
 }
+
+impl From<SyntaxText<'_>> for String {
+    fn from(text: SyntaxText) -> String {
+        text.to_string()
+    }
+}
diff --git a/crates/ra_text_edit/src/text_edit.rs b/crates/ra_text_edit/src/text_edit.rs
index 0881f3e1ce..a288a990db 100644
--- a/crates/ra_text_edit/src/text_edit.rs
+++ b/crates/ra_text_edit/src/text_edit.rs
@@ -7,15 +7,12 @@ pub struct TextEdit {
     atoms: Vec<AtomTextEdit>,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Default)]
 pub struct TextEditBuilder {
     atoms: Vec<AtomTextEdit>,
 }
 
 impl TextEditBuilder {
-    pub fn new() -> TextEditBuilder {
-        TextEditBuilder { atoms: Vec::new() }
-    }
     pub fn replace(&mut self, range: TextRange, replace_with: String) {
         self.atoms.push(AtomTextEdit::replace(range, replace_with))
     }
diff --git a/editors/code/src/commands/apply_source_change.ts b/editors/code/src/commands/apply_source_change.ts
index 10dbf72c07..cf921e3ac0 100644
--- a/editors/code/src/commands/apply_source_change.ts
+++ b/editors/code/src/commands/apply_source_change.ts
@@ -3,23 +3,46 @@ import * as lc from 'vscode-languageclient';
 
 import { Server } from '../server';
 
+interface FileSystemEdit {
+    type: string;
+    uri?: string;
+    src?: string;
+    dst?: string;
+}
+
 export interface SourceChange {
     label: string;
-    workspaceEdit: lc.WorkspaceEdit;
+    sourceFileEdits: lc.TextDocumentEdit[];
+    fileSystemEdits: FileSystemEdit[];
     cursorPosition?: lc.TextDocumentPositionParams;
 }
 
 export async function handle(change: SourceChange) {
-    const wsEdit = Server.client.protocol2CodeConverter.asWorkspaceEdit(change.workspaceEdit);
+    const wsEdit = new vscode.WorkspaceEdit();
+    for (const sourceEdit of change.sourceFileEdits) {
+        const uri = Server.client.protocol2CodeConverter.asUri(
+            sourceEdit.textDocument.uri
+        );
+        const edits = Server.client.protocol2CodeConverter.asTextEdits(
+            sourceEdit.edits
+        );
+        wsEdit.set(uri, edits);
+    }
     let created;
     let moved;
-    if (change.workspaceEdit.documentChanges) {
-        for (const docChange of change.workspaceEdit.documentChanges) {
-            if (lc.CreateFile.is(docChange)) {
-                created = docChange.uri;
-            } else if (lc.RenameFile.is(docChange)) {
-                moved = docChange.newUri;
-            }
+    for (const fsEdit of change.fileSystemEdits) {
+        switch (fsEdit.type) {
+            case 'createFile':
+                const uri = vscode.Uri.parse(fsEdit.uri!);
+                wsEdit.createFile(uri);
+                created = uri;
+                break;
+            case 'moveFile':
+                const src = vscode.Uri.parse(fsEdit.src!);
+                const dst = vscode.Uri.parse(fsEdit.dst!);
+                wsEdit.renameFile(src, dst);
+                moved = dst;
+                break;
         }
     }
     const toOpen = created || moved;
@@ -42,6 +65,6 @@ export async function handle(change: SourceChange) {
         if (!editor.selection.isEmpty) {
             return;
         }
-        editor.selection = new vscode.Selection(position, position);
+        editor!.selection = new vscode.Selection(position, position);
     }
 }


Test patch:
diff --git a/crates/ra_analysis/tests/tests.rs b/crates/ra_analysis/tests/tests.rs
index bcf29d29ca..c2ef87b549 100644
--- a/crates/ra_analysis/tests/tests.rs
+++ b/crates/ra_analysis/tests/tests.rs
@@ -531,7 +531,7 @@ fn test_rename_for_mut_param() {
 fn test_rename(text: &str, new_name: &str, expected: &str) {
     let (analysis, position) = single_file_with_position(text);
     let edits = analysis.rename(position, new_name).unwrap();
-    let mut text_edit_bulder = ra_text_edit::TextEditBuilder::new();
+    let mut text_edit_bulder = ra_text_edit::TextEditBuilder::default();
     let mut file_id: Option<FileId> = None;
     for edit in edits {
         file_id = Some(edit.file_id);
diff --git a/crates/ra_lsp_server/tests/heavy_tests/main.rs b/crates/ra_lsp_server/tests/heavy_tests/main.rs
index 4cae44eab2..b0e1e65b62 100644
--- a/crates/ra_lsp_server/tests/heavy_tests/main.rs
+++ b/crates/ra_lsp_server/tests/heavy_tests/main.rs
@@ -1,12 +1,8 @@
 mod support;
 
-use languageserver_types::{
-    CodeActionContext, DocumentFormattingParams, FormattingOptions, Position, Range,
-};
-use ra_lsp_server::req::{
-    CodeActionParams, CodeActionRequest, Formatting, Runnables, RunnablesParams,
-};
 use serde_json::json;
+use ra_lsp_server::req::{Runnables, RunnablesParams, CodeActionRequest, CodeActionParams, Formatting};
+use languageserver_types::{Position, Range, CodeActionContext, DocumentFormattingParams, FormattingOptions};
 
 use crate::support::project;
 
@@ -207,15 +203,14 @@ fn main() {}
               "arguments": [
                 {
                   "cursorPosition": null,
-                  "workspaceEdit": {
-                    "documentChanges": [
-                      {
-                        "kind": "create",
+                  "fileSystemEdits": [
+                    {
+                        "type": "createFile",
                         "uri": "file:///[..]/src/bar.rs"
-                      }
-                    ]
-                  },
-                  "label": "create module"
+                    }
+                  ],
+                  "label": "create module",
+                  "sourceFileEdits": []
                 }
               ],
               "command": "ra-lsp.applySourceChange",