Start work on handling snippet completions

Max Brunsfeld created

Change summary

Cargo.lock                        |  1 
crates/editor/Cargo.toml          |  2 
crates/editor/src/editor.rs       | 72 +++++++++++++++++++++++++++++++-
crates/editor/src/multi_buffer.rs |  4 
crates/language/src/buffer.rs     |  4 -
5 files changed, 75 insertions(+), 8 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1562,6 +1562,7 @@ dependencies = [
  "serde",
  "smallvec",
  "smol",
+ "snippet",
  "sum_tree",
  "text",
  "theme",

crates/editor/Cargo.toml 🔗

@@ -22,7 +22,9 @@ collections = { path = "../collections" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
+lsp = { path = "../lsp" }
 project = { path = "../project" }
+snippet = { path = "../snippet" }
 sum_tree = { path = "../sum_tree" }
 theme = { path = "../theme" }
 util = { path = "../util" }

crates/editor/src/editor.rs 🔗

@@ -42,6 +42,7 @@ use postage::watch;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use smol::Timer;
+use snippet::Snippet;
 use std::{
     any::TypeId,
     cmp::{self, Ordering, Reverse},
@@ -1656,10 +1657,22 @@ impl Editor {
             .matches
             .get(completion_state.selected_item)?;
         let completion = completion_state.completions.get(mat.candidate_id)?;
+
+        if completion.lsp_completion.insert_text_format == Some(lsp::InsertTextFormat::SNIPPET) {
+            self.insert_snippet(completion.old_range.clone(), &completion.new_text, cx)
+                .log_err();
+        } else {
+            self.buffer.update(cx, |buffer, cx| {
+                buffer.edit_with_autoindent(
+                    [completion.old_range.clone()],
+                    &completion.new_text,
+                    cx,
+                );
+            });
+        }
+
         self.buffer.update(cx, |buffer, cx| {
-            let mut completion = completion.clone();
-            // completion.
-            buffer.apply_completion(completion, cx)
+            buffer.apply_additional_edits_for_completion(completion.clone(), cx)
         })
     }
 
@@ -1722,6 +1735,42 @@ impl Editor {
         })
     }
 
+    pub fn insert_snippet<S>(
+        &mut self,
+        range: Range<S>,
+        text: &str,
+        cx: &mut ViewContext<Self>,
+    ) -> Result<()>
+    where
+        S: Clone + ToOffset,
+    {
+        let snippet = Snippet::parse(text)?;
+        let tabstops = self.buffer.update(cx, |buffer, cx| {
+            buffer.edit_with_autoindent([range.clone()], snippet.text, cx);
+            let snapshot = buffer.read(cx);
+            let start = range.start.to_offset(&snapshot);
+            snippet
+                .tabstops
+                .iter()
+                .map(|ranges| {
+                    ranges
+                        .into_iter()
+                        .map(|range| {
+                            snapshot.anchor_before(start + range.start)
+                                ..snapshot.anchor_after(start + range.end)
+                        })
+                        .collect::<Vec<_>>()
+                })
+                .collect::<Vec<_>>()
+        });
+
+        if let Some(tabstop) = tabstops.first() {
+            self.select_ranges(tabstop.iter().cloned(), Some(Autoscroll::Fit), cx);
+        }
+
+        Ok(())
+    }
+
     pub fn clear(&mut self, cx: &mut ViewContext<Self>) {
         self.start_transaction(cx);
         self.select_all(&SelectAll, cx);
@@ -6581,6 +6630,23 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_snippets(mut cx: gpui::TestAppContext) {
+        let settings = cx.read(EditorSettings::test);
+
+        let text = "a. b";
+        let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
+        let (_, editor) = cx.add_window(|cx| build_editor(buffer, settings, cx));
+
+        editor.update(&mut cx, |editor, cx| {
+            editor
+                .insert_snippet(2..2, "f(${1:one}, ${2:two})$0", cx)
+                .unwrap();
+            assert_eq!(editor.text(cx), "a.f(one, two) b");
+            assert_eq!(editor.selected_ranges::<usize>(cx), &[4..7]);
+        });
+    }
+
     #[gpui::test]
     async fn test_completion(mut cx: gpui::TestAppContext) {
         let settings = cx.read(EditorSettings::test);

crates/editor/src/multi_buffer.rs 🔗

@@ -929,7 +929,7 @@ impl MultiBuffer {
         }
     }
 
-    pub fn apply_completion(
+    pub fn apply_additional_edits_for_completion(
         &self,
         completion: Completion<Anchor>,
         cx: &mut ModelContext<Self>,
@@ -941,7 +941,7 @@ impl MultiBuffer {
             .buffer
             .clone();
         buffer.update(cx, |buffer, cx| {
-            buffer.apply_completion(
+            buffer.apply_additional_edits_for_completion(
                 Completion {
                     old_range: completion.old_range.start.text_anchor
                         ..completion.old_range.end.text_anchor,

crates/language/src/buffer.rs 🔗

@@ -1777,13 +1777,11 @@ impl Buffer {
         }
     }
 
-    pub fn apply_completion(
+    pub fn apply_additional_edits_for_completion(
         &mut self,
         completion: Completion<Anchor>,
         cx: &mut ModelContext<Self>,
     ) -> Option<Task<Result<()>>> {
-        self.edit_with_autoindent([completion.old_range], completion.new_text.clone(), cx);
-
         self.file.as_ref()?.as_local()?;
         let server = self.language_server.as_ref()?.server.clone();
         Some(cx.spawn(|this, mut cx| async move {