Insert an extra newline between brackets

Nathan Sobo , Antonio Scandurra , and Max Brunsfeld created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

crates/buffer/src/language.rs         |  12 +
crates/buffer/src/lib.rs              |  14 ++
crates/buffer/src/rope.rs             |  86 +++++++++++--
crates/editor/src/lib.rs              | 178 ++++++++++++++++++++++++----
crates/zed/languages/rust/config.toml |  13 +
5 files changed, 244 insertions(+), 59 deletions(-)

Detailed changes

crates/buffer/src/language.rs 🔗

@@ -11,13 +11,15 @@ pub use tree_sitter::{Parser, Tree};
 pub struct LanguageConfig {
     pub name: String,
     pub path_suffixes: Vec<String>,
-    pub autoclose_pairs: Vec<AutoclosePair>,
+    pub brackets: Vec<BracketPair>,
 }
 
-#[derive(Clone, Deserialize)]
-pub struct AutoclosePair {
+#[derive(Clone, Debug, Deserialize)]
+pub struct BracketPair {
     pub start: String,
     pub end: String,
+    pub close: bool,
+    pub newline: bool,
 }
 
 pub struct Language {
@@ -95,8 +97,8 @@ impl Language {
         self.config.name.as_str()
     }
 
-    pub fn autoclose_pairs(&self) -> &[AutoclosePair] {
-        &self.config.autoclose_pairs
+    pub fn brackets(&self) -> &[BracketPair] {
+        &self.config.brackets
     }
 
     pub fn highlight_map(&self) -> HighlightMap {

crates/buffer/src/lib.rs 🔗

@@ -16,7 +16,7 @@ use clock::ReplicaId;
 use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
 pub use highlight_map::{HighlightId, HighlightMap};
 use language::Tree;
-pub use language::{AutoclosePair, Language, LanguageConfig, LanguageRegistry};
+pub use language::{BracketPair, Language, LanguageConfig, LanguageRegistry};
 use lazy_static::lazy_static;
 use operation_queue::OperationQueue;
 use parking_lot::Mutex;
@@ -1337,6 +1337,13 @@ impl Buffer {
         self.content().chars_at(position)
     }
 
+    pub fn reversed_chars_at<'a, T: 'a + ToOffset>(
+        &'a self,
+        position: T,
+    ) -> impl Iterator<Item = char> + 'a {
+        self.content().reversed_chars_at(position)
+    }
+
     pub fn chars_for_range<T: ToOffset>(&self, range: Range<T>) -> impl Iterator<Item = char> + '_ {
         self.text_for_range(range).flat_map(str::chars)
     }
@@ -2794,6 +2801,11 @@ impl<'a> Content<'a> {
         self.visible_text.chars_at(offset)
     }
 
+    pub fn reversed_chars_at<T: ToOffset>(&self, position: T) -> impl Iterator<Item = char> + 'a {
+        let offset = position.to_offset(self);
+        self.visible_text.reversed_chars_at(offset)
+    }
+
     pub fn text_for_range<T: ToOffset>(&self, range: Range<T>) -> Chunks<'a> {
         let start = range.start.to_offset(self);
         let end = range.end.to_offset(self);

crates/buffer/src/rope.rs 🔗

@@ -115,6 +115,11 @@ impl Rope {
         self.chunks_in_range(start..self.len()).flat_map(str::chars)
     }
 
+    pub fn reversed_chars_at(&self, start: usize) -> impl Iterator<Item = char> + '_ {
+        self.reversed_chunks_in_range(0..start)
+            .flat_map(|chunk| chunk.chars().rev())
+    }
+
     pub fn bytes_at(&self, start: usize) -> impl Iterator<Item = u8> + '_ {
         self.chunks_in_range(start..self.len()).flat_map(str::bytes)
     }
@@ -123,8 +128,12 @@ impl Rope {
         self.chunks_in_range(0..self.len())
     }
 
-    pub fn chunks_in_range<'a>(&'a self, range: Range<usize>) -> Chunks<'a> {
-        Chunks::new(self, range)
+    pub fn chunks_in_range(&self, range: Range<usize>) -> Chunks {
+        Chunks::new(self, range, false)
+    }
+
+    pub fn reversed_chunks_in_range(&self, range: Range<usize>) -> Chunks {
+        Chunks::new(self, range, true)
     }
 
     pub fn to_point(&self, offset: usize) -> Point {
@@ -284,38 +293,65 @@ impl<'a> Cursor<'a> {
 pub struct Chunks<'a> {
     chunks: sum_tree::Cursor<'a, Chunk, usize>,
     range: Range<usize>,
+    reversed: bool,
 }
 
 impl<'a> Chunks<'a> {
-    pub fn new(rope: &'a Rope, range: Range<usize>) -> Self {
+    pub fn new(rope: &'a Rope, range: Range<usize>, reversed: bool) -> Self {
         let mut chunks = rope.chunks.cursor();
-        chunks.seek(&range.start, Bias::Right, &());
-        Self { chunks, range }
+        if reversed {
+            chunks.seek(&range.end, Bias::Left, &());
+        } else {
+            chunks.seek(&range.start, Bias::Right, &());
+        }
+        Self {
+            chunks,
+            range,
+            reversed,
+        }
     }
 
     pub fn offset(&self) -> usize {
-        self.range.start.max(*self.chunks.start())
+        if self.reversed {
+            self.range.end.min(self.chunks.end(&()))
+        } else {
+            self.range.start.max(*self.chunks.start())
+        }
     }
 
     pub fn seek(&mut self, offset: usize) {
+        let bias = if self.reversed {
+            Bias::Left
+        } else {
+            Bias::Right
+        };
+
         if offset >= self.chunks.end(&()) {
-            self.chunks.seek_forward(&offset, Bias::Right, &());
+            self.chunks.seek_forward(&offset, bias, &());
         } else {
-            self.chunks.seek(&offset, Bias::Right, &());
+            self.chunks.seek(&offset, bias, &());
+        }
+
+        if self.reversed {
+            self.range.end = offset;
+        } else {
+            self.range.start = offset;
         }
-        self.range.start = offset;
     }
 
     pub fn peek(&self) -> Option<&'a str> {
-        if let Some(chunk) = self.chunks.item() {
-            let offset = *self.chunks.start();
-            if self.range.end > offset {
-                let start = self.range.start.saturating_sub(*self.chunks.start());
-                let end = self.range.end - self.chunks.start();
-                return Some(&chunk.0[start..chunk.0.len().min(end)]);
-            }
+        let chunk = self.chunks.item()?;
+        if self.reversed && self.range.start >= self.chunks.end(&()) {
+            return None;
         }
-        None
+        let chunk_start = *self.chunks.start();
+        if self.range.end <= chunk_start {
+            return None;
+        }
+
+        let start = self.range.start.saturating_sub(chunk_start);
+        let end = self.range.end - chunk_start;
+        Some(&chunk.0[start..chunk.0.len().min(end)])
     }
 }
 
@@ -325,7 +361,11 @@ impl<'a> Iterator for Chunks<'a> {
     fn next(&mut self) -> Option<Self::Item> {
         let result = self.peek();
         if result.is_some() {
-            self.chunks.next(&());
+            if self.reversed {
+                self.chunks.prev(&());
+            } else {
+                self.chunks.next(&());
+            }
         }
         result
     }
@@ -571,6 +611,16 @@ mod tests {
                     actual.chunks_in_range(start_ix..end_ix).collect::<String>(),
                     &expected[start_ix..end_ix]
                 );
+
+                assert_eq!(
+                    actual
+                        .reversed_chunks_in_range(start_ix..end_ix)
+                        .collect::<Vec<&str>>()
+                        .into_iter()
+                        .rev()
+                        .collect::<String>(),
+                    &expected[start_ix..end_ix]
+                );
             }
 
             let mut point = Point::new(0, 0);

crates/editor/src/lib.rs 🔗

@@ -296,7 +296,7 @@ pub struct Editor {
     pending_selection: Option<Selection>,
     next_selection_id: usize,
     add_selections_state: Option<AddSelectionsState>,
-    autoclose_stack: Vec<AutoclosePairState>,
+    autoclose_stack: Vec<BracketPairState>,
     select_larger_syntax_node_stack: Vec<Arc<[Selection]>>,
     scroll_position: Vector2F,
     scroll_top_anchor: Anchor,
@@ -324,9 +324,9 @@ struct AddSelectionsState {
     stack: Vec<usize>,
 }
 
-struct AutoclosePairState {
+struct BracketPairState {
     ranges: SmallVec<[Range<Anchor>; 32]>,
-    pair: AutoclosePair,
+    pair: BracketPair,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -767,7 +767,35 @@ impl Editor {
                     .min(start_point.column);
                 let start = selection.start.to_offset(buffer);
                 let end = selection.end.to_offset(buffer);
-                old_selections.push((selection.id, start..end, indent));
+
+                let mut insert_extra_newline = false;
+                if let Some(language) = buffer.language() {
+                    let leading_whitespace_len = buffer
+                        .reversed_chars_at(start)
+                        .take_while(|c| c.is_whitespace() && *c != '\n')
+                        .map(|c| c.len_utf8())
+                        .sum::<usize>();
+
+                    let trailing_whitespace_len = buffer
+                        .chars_at(end)
+                        .take_while(|c| c.is_whitespace() && *c != '\n')
+                        .map(|c| c.len_utf8())
+                        .sum::<usize>();
+
+                    insert_extra_newline = language.brackets().iter().any(|pair| {
+                        let pair_start = pair.start.trim_end();
+                        let pair_end = pair.end.trim_start();
+
+                        pair.newline
+                            && buffer.contains_str_at(end + trailing_whitespace_len, pair_end)
+                            && buffer.contains_str_at(
+                                (start - leading_whitespace_len).saturating_sub(pair_start.len()),
+                                pair_start,
+                            )
+                    });
+                }
+
+                old_selections.push((selection.id, start..end, indent, insert_extra_newline));
             }
         }
 
@@ -775,26 +803,33 @@ impl Editor {
         self.buffer.update(cx, |buffer, cx| {
             let mut delta = 0_isize;
             let mut pending_edit: Option<PendingEdit> = None;
-            for (_, range, indent) in &old_selections {
-                if pending_edit
-                    .as_ref()
-                    .map_or(false, |pending| pending.indent != *indent)
-                {
+            for (_, range, indent, insert_extra_newline) in &old_selections {
+                if pending_edit.as_ref().map_or(false, |pending| {
+                    pending.indent != *indent
+                        || pending.insert_extra_newline != *insert_extra_newline
+                }) {
                     let pending = pending_edit.take().unwrap();
                     let mut new_text = String::with_capacity(1 + pending.indent as usize);
                     new_text.push('\n');
                     new_text.extend(iter::repeat(' ').take(pending.indent as usize));
+                    if pending.insert_extra_newline {
+                        new_text = new_text.repeat(2);
+                    }
                     buffer.edit_with_autoindent(pending.ranges, new_text, cx);
                     delta += pending.delta;
                 }
 
                 let start = (range.start as isize + delta) as usize;
                 let end = (range.end as isize + delta) as usize;
-                let text_len = *indent as usize + 1;
+                let mut text_len = *indent as usize + 1;
+                if *insert_extra_newline {
+                    text_len *= 2;
+                }
 
                 let pending = pending_edit.get_or_insert_with(Default::default);
                 pending.delta += text_len as isize - (end - start) as isize;
                 pending.indent = *indent;
+                pending.insert_extra_newline = *insert_extra_newline;
                 pending.ranges.push(start..end);
             }
 
@@ -802,23 +837,33 @@ impl Editor {
             let mut new_text = String::with_capacity(1 + pending.indent as usize);
             new_text.push('\n');
             new_text.extend(iter::repeat(' ').take(pending.indent as usize));
+            if pending.insert_extra_newline {
+                new_text = new_text.repeat(2);
+            }
             buffer.edit_with_autoindent(pending.ranges, new_text, cx);
 
             let mut delta = 0_isize;
-            new_selections.extend(old_selections.into_iter().map(|(id, range, indent)| {
-                let start = (range.start as isize + delta) as usize;
-                let end = (range.end as isize + delta) as usize;
-                let text_len = indent as usize + 1;
-                let anchor = buffer.anchor_before(start + text_len);
-                delta += text_len as isize - (end - start) as isize;
-                Selection {
-                    id,
-                    start: anchor.clone(),
-                    end: anchor,
-                    reversed: false,
-                    goal: SelectionGoal::None,
-                }
-            }))
+            new_selections.extend(old_selections.into_iter().map(
+                |(id, range, indent, insert_extra_newline)| {
+                    let start = (range.start as isize + delta) as usize;
+                    let end = (range.end as isize + delta) as usize;
+                    let text_before_cursor_len = indent as usize + 1;
+                    let anchor = buffer.anchor_before(start + text_before_cursor_len);
+                    let text_len = if insert_extra_newline {
+                        text_before_cursor_len * 2
+                    } else {
+                        text_before_cursor_len
+                    };
+                    delta += text_len as isize - (end - start) as isize;
+                    Selection {
+                        id,
+                        start: anchor.clone(),
+                        end: anchor,
+                        reversed: false,
+                        goal: SelectionGoal::None,
+                    }
+                },
+            ))
         });
 
         self.update_selections(new_selections, true, cx);
@@ -827,6 +872,7 @@ impl Editor {
         #[derive(Default)]
         struct PendingEdit {
             indent: u32,
+            insert_extra_newline: bool,
             delta: isize,
             ranges: SmallVec<[Range<usize>; 32]>,
         }
@@ -879,7 +925,7 @@ impl Editor {
         let new_autoclose_pair_state = self.buffer.update(cx, |buffer, cx| {
             let autoclose_pair = buffer.language().and_then(|language| {
                 let first_selection_start = selections.first().unwrap().start.to_offset(&*buffer);
-                let pair = language.autoclose_pairs().iter().find(|pair| {
+                let pair = language.brackets().iter().find(|pair| {
                     buffer.contains_str_at(
                         first_selection_start.saturating_sub(pair.start.len()),
                         &pair.start,
@@ -914,7 +960,7 @@ impl Editor {
                 buffer.edit(selection_ranges, &pair.end, cx);
 
                 if pair.end.len() == 1 {
-                    Some(AutoclosePairState {
+                    Some(BracketPairState {
                         ranges: selections
                             .iter()
                             .map(|selection| {
@@ -4506,14 +4552,18 @@ mod tests {
         let settings = cx.read(EditorSettings::test);
         let language = Arc::new(Language::new(
             LanguageConfig {
-                autoclose_pairs: vec![
-                    AutoclosePair {
+                brackets: vec![
+                    BracketPair {
                         start: "{".to_string(),
                         end: "}".to_string(),
+                        close: true,
+                        newline: true,
                     },
-                    AutoclosePair {
+                    BracketPair {
                         start: "/*".to_string(),
                         end: " */".to_string(),
+                        close: true,
+                        newline: true,
                     },
                 ],
                 ..Default::default()
@@ -4612,6 +4662,76 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_extra_newline_insertion(mut cx: gpui::TestAppContext) {
+        let settings = cx.read(EditorSettings::test);
+        let language = Arc::new(Language::new(
+            LanguageConfig {
+                brackets: vec![
+                    BracketPair {
+                        start: "{".to_string(),
+                        end: "}".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                    BracketPair {
+                        start: "/* ".to_string(),
+                        end: " */".to_string(),
+                        close: true,
+                        newline: true,
+                    },
+                ],
+                ..Default::default()
+            },
+            tree_sitter_rust::language(),
+        ));
+
+        let text = concat!(
+            "{   }\n",     // Suppress rustfmt
+            "  x\n",       //
+            "  /*   */\n", //
+            "x\n",         //
+            "{{} }\n",     //
+        );
+
+        let buffer = cx.add_model(|cx| {
+            let history = History::new(text.into());
+            Buffer::from_history(0, history, None, Some(language), cx)
+        });
+        let (_, view) = cx.add_window(|cx| build_editor(buffer, settings, cx));
+        view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
+            .await;
+
+        view.update(&mut cx, |view, cx| {
+            view.select_display_ranges(
+                &[
+                    DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
+                    DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
+                    DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
+                ],
+                cx,
+            )
+            .unwrap();
+            view.newline(&Newline, cx);
+
+            assert_eq!(
+                view.buffer().read(cx).text(),
+                concat!(
+                    "{ \n",    // Suppress rustfmt
+                    "\n",      //
+                    "}\n",     //
+                    "  x\n",   //
+                    "  /* \n", //
+                    "  \n",    //
+                    "  */\n",  //
+                    "x\n",     //
+                    "{{} \n",  //
+                    "}\n",     //
+                )
+            );
+        });
+    }
+
     impl Editor {
         fn selection_ranges(&self, cx: &mut MutableAppContext) -> Vec<Range<DisplayPoint>> {
             self.selections_in_range(

crates/zed/languages/rust/config.toml 🔗

@@ -1,9 +1,10 @@
 name = "Rust"
 path_suffixes = ["rs"]
-autoclose_pairs = [
-    { start = "{", end = "}" },
-    { start = "[", end = "]" },
-    { start = "(", end = ")" },
-    { start = "\"", end = "\"" },
-    { start = "/*", end = " */" },
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = false, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false },
+    { start = "/*", end = " */", close = true, newline = false },
 ]