Detailed changes
@@ -11,6 +11,13 @@ pub use tree_sitter::{Parser, Tree};
pub struct LanguageConfig {
pub name: String,
pub path_suffixes: Vec<String>,
+ pub autoclose_pairs: Vec<AutoclosePair>,
+}
+
+#[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()
}
@@ -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<T: ToOffset>(&self, position: T) -> impl Iterator<Item = u8> + '_ {
+ let offset = position.to_offset(self);
+ self.visible_text.bytes_at(offset)
+ }
+
+ pub fn contains_str_at<T>(&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<Item = Edit> {
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(),
)
@@ -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<Item = u8> + '_ {
+ 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())
}
@@ -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<Self>) {
+ 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>) {
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<Range<DisplayPoint>> {
self.selections_in_range(
@@ -275,6 +275,7 @@ impl WorkspaceParams {
buffer::LanguageConfig {
name: "Rust".to_string(),
path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
},
tree_sitter_rust::language(),
)));
@@ -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 = " */" },
]