From 05d7e9c4e716a14e1b07911709b1b26b7f4c4633 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Oct 2021 16:34:57 +0200 Subject: [PATCH] Start on autoclosing pairs Co-Authored-By: Nathan Sobo --- crates/buffer/src/language.rs | 11 +++ crates/buffer/src/lib.rs | 20 +++- crates/buffer/src/rope.rs | 4 + crates/editor/src/lib.rs | 136 ++++++++++++++++++++++++++ crates/workspace/src/lib.rs | 1 + crates/zed/languages/rust/config.toml | 5 +- 6 files changed, 174 insertions(+), 3 deletions(-) diff --git a/crates/buffer/src/language.rs b/crates/buffer/src/language.rs index 5e3cf19745..1a9a29aac5 100644 --- a/crates/buffer/src/language.rs +++ b/crates/buffer/src/language.rs @@ -11,6 +11,13 @@ pub use tree_sitter::{Parser, Tree}; pub struct LanguageConfig { pub name: String, pub path_suffixes: Vec, + pub autoclose_pairs: Vec, +} + +#[derive(Clone, Deserialize)] +pub struct AutoclosePair { + pub start: String, + pub end: String, } pub struct Language { @@ -81,6 +88,10 @@ impl Language { self.config.name.as_str() } + pub fn autoclose_pairs(&self) -> &[AutoclosePair] { + &self.config.autoclose_pairs + } + pub fn highlight_map(&self) -> HighlightMap { self.highlight_map.lock().clone() } diff --git a/crates/buffer/src/lib.rs b/crates/buffer/src/lib.rs index 92ac0c70e0..a2f34a5d78 100644 --- a/crates/buffer/src/lib.rs +++ b/crates/buffer/src/lib.rs @@ -14,7 +14,7 @@ use clock::ReplicaId; use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task}; pub use highlight_map::{HighlightId, HighlightMap}; use language::Tree; -pub use language::{Language, LanguageConfig, LanguageRegistry}; +pub use language::{AutoclosePair, Language, LanguageConfig, LanguageRegistry}; use lazy_static::lazy_static; use operation_queue::OperationQueue; use parking_lot::Mutex; @@ -1110,6 +1110,23 @@ impl Buffer { self.visible_text.chars_at(offset) } + pub fn bytes_at(&self, position: T) -> impl Iterator + '_ { + let offset = position.to_offset(self); + self.visible_text.bytes_at(offset) + } + + pub fn contains_str_at(&self, position: T, needle: &str) -> bool + where + T: ToOffset, + { + let position = position.to_offset(self); + position == self.clip_offset(position, Bias::Left) + && self + .bytes_at(position) + .take(needle.len()) + .eq(needle.bytes()) + } + pub fn edits_since<'a>(&'a self, since: clock::Global) -> impl 'a + Iterator { let since_2 = since.clone(); let cursor = if since == self.version { @@ -4083,6 +4100,7 @@ mod tests { LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], + ..Default::default() }, tree_sitter_rust::language(), ) diff --git a/crates/buffer/src/rope.rs b/crates/buffer/src/rope.rs index 8467a98947..a5f4b905ba 100644 --- a/crates/buffer/src/rope.rs +++ b/crates/buffer/src/rope.rs @@ -115,6 +115,10 @@ impl Rope { self.chunks_in_range(start..self.len()).flat_map(str::chars) } + pub fn bytes_at(&self, start: usize) -> impl Iterator + '_ { + self.chunks_in_range(start..self.len()).flat_map(str::bytes) + } + pub fn chunks<'a>(&'a self) -> Chunks<'a> { self.chunks_in_range(0..self.len()) } diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 66a234e60f..25038468eb 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -772,9 +772,51 @@ impl Editor { }); self.update_selections(new_selections, true, cx); + self.autoclose_pairs(cx); self.end_transaction(cx); } + fn autoclose_pairs(&mut self, cx: &mut ViewContext) { + let selections = self.selections(cx); + 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| { + buffer.contains_str_at( + first_selection_start.saturating_sub(pair.start.len()), + &pair.start, + ) + }); + pair.and_then(|pair| { + let should_autoclose = selections[1..].iter().all(|selection| { + let selection_start = selection.start.to_offset(&*buffer); + buffer.contains_str_at( + selection_start.saturating_sub(pair.start.len()), + &pair.start, + ) + }); + + if should_autoclose { + Some(pair.clone()) + } else { + None + } + }) + }); + + if let Some(pair) = autoclose_pair { + let mut selection_ranges = SmallVec::<[_; 32]>::new(); + for selection in selections.as_ref() { + let start = selection.start.to_offset(&*buffer); + let end = selection.end.to_offset(&*buffer); + selection_ranges.push(start..end); + } + + buffer.edit(selection_ranges, &pair.end, cx); + } + }); + } + pub fn clear(&mut self, cx: &mut ViewContext) { self.start_transaction(cx); self.select_all(&SelectAll, cx); @@ -4209,6 +4251,100 @@ mod tests { ); } + #[gpui::test] + async fn test_autoclose_pairs(mut cx: gpui::TestAppContext) { + let settings = cx.read(EditorSettings::test); + let language = Arc::new(Language::new( + LanguageConfig { + autoclose_pairs: vec![ + AutoclosePair { + start: "{".to_string(), + end: "}".to_string(), + }, + AutoclosePair { + start: "/*".to_string(), + end: " */".to_string(), + }, + ], + ..Default::default() + }, + tree_sitter_rust::language(), + )); + + let text = r#" + a + + / + + "# + .unindent(); + + 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, 0)..DisplayPoint::new(0, 1), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ], + cx, + ) + .unwrap(); + view.insert(&Insert("{".to_string()), cx); + assert_eq!( + view.text(cx), + " + {} + {} + / + + " + .unindent() + ); + + view.undo(&Undo, cx); + view.insert(&Insert("/".to_string()), cx); + view.insert(&Insert("*".to_string()), cx); + assert_eq!( + view.text(cx), + " + /* */ + /* */ + / + + " + .unindent() + ); + + view.undo(&Undo, cx); + view.select_display_ranges( + &[ + DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ], + cx, + ) + .unwrap(); + view.insert(&Insert("*".to_string()), cx); + assert_eq!( + view.text(cx), + " + a + + /* + * + " + .unindent() + ); + }); + } + impl Editor { fn selection_ranges(&self, cx: &mut MutableAppContext) -> Vec> { self.selections_in_range( diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index d7a660974e..6a0b7c0031 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -275,6 +275,7 @@ impl WorkspaceParams { buffer::LanguageConfig { name: "Rust".to_string(), path_suffixes: vec!["rs".to_string()], + ..Default::default() }, tree_sitter_rust::language(), ))); diff --git a/crates/zed/languages/rust/config.toml b/crates/zed/languages/rust/config.toml index 4cde06f6f8..ece9b57ca2 100644 --- a/crates/zed/languages/rust/config.toml +++ b/crates/zed/languages/rust/config.toml @@ -1,8 +1,9 @@ name = "Rust" path_suffixes = ["rs"] -bracket_pairs = [ +autoclose_pairs = [ { start = "{", end = "}" }, { start = "[", end = "]" }, { start = "(", end = ")" }, - { start = "<", end = ">" }, + { start = "\"", end = "\"" }, + { start = "/*", end = " */" }, ]