Start on autoclosing pairs
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
3cb7ba0f57
commit
05d7e9c4e7
6 changed files with 174 additions and 3 deletions
|
@ -11,6 +11,13 @@ pub use tree_sitter::{Parser, Tree};
|
||||||
pub struct LanguageConfig {
|
pub struct LanguageConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path_suffixes: Vec<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 {
|
pub struct Language {
|
||||||
|
@ -81,6 +88,10 @@ impl Language {
|
||||||
self.config.name.as_str()
|
self.config.name.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn autoclose_pairs(&self) -> &[AutoclosePair] {
|
||||||
|
&self.config.autoclose_pairs
|
||||||
|
}
|
||||||
|
|
||||||
pub fn highlight_map(&self) -> HighlightMap {
|
pub fn highlight_map(&self) -> HighlightMap {
|
||||||
self.highlight_map.lock().clone()
|
self.highlight_map.lock().clone()
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use clock::ReplicaId;
|
||||||
use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
|
use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
|
||||||
pub use highlight_map::{HighlightId, HighlightMap};
|
pub use highlight_map::{HighlightId, HighlightMap};
|
||||||
use language::Tree;
|
use language::Tree;
|
||||||
pub use language::{Language, LanguageConfig, LanguageRegistry};
|
pub use language::{AutoclosePair, Language, LanguageConfig, LanguageRegistry};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use operation_queue::OperationQueue;
|
use operation_queue::OperationQueue;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -1110,6 +1110,23 @@ impl Buffer {
|
||||||
self.visible_text.chars_at(offset)
|
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> {
|
pub fn edits_since<'a>(&'a self, since: clock::Global) -> impl 'a + Iterator<Item = Edit> {
|
||||||
let since_2 = since.clone();
|
let since_2 = since.clone();
|
||||||
let cursor = if since == self.version {
|
let cursor = if since == self.version {
|
||||||
|
@ -4083,6 +4100,7 @@ mod tests {
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
name: "Rust".to_string(),
|
name: "Rust".to_string(),
|
||||||
path_suffixes: vec!["rs".to_string()],
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
tree_sitter_rust::language(),
|
tree_sitter_rust::language(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -115,6 +115,10 @@ impl Rope {
|
||||||
self.chunks_in_range(start..self.len()).flat_map(str::chars)
|
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> {
|
pub fn chunks<'a>(&'a self) -> Chunks<'a> {
|
||||||
self.chunks_in_range(0..self.len())
|
self.chunks_in_range(0..self.len())
|
||||||
}
|
}
|
||||||
|
|
|
@ -772,9 +772,51 @@ impl Editor {
|
||||||
});
|
});
|
||||||
|
|
||||||
self.update_selections(new_selections, true, cx);
|
self.update_selections(new_selections, true, cx);
|
||||||
|
self.autoclose_pairs(cx);
|
||||||
self.end_transaction(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>) {
|
pub fn clear(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.start_transaction(cx);
|
self.start_transaction(cx);
|
||||||
self.select_all(&SelectAll, 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 {
|
impl Editor {
|
||||||
fn selection_ranges(&self, cx: &mut MutableAppContext) -> Vec<Range<DisplayPoint>> {
|
fn selection_ranges(&self, cx: &mut MutableAppContext) -> Vec<Range<DisplayPoint>> {
|
||||||
self.selections_in_range(
|
self.selections_in_range(
|
||||||
|
|
|
@ -275,6 +275,7 @@ impl WorkspaceParams {
|
||||||
buffer::LanguageConfig {
|
buffer::LanguageConfig {
|
||||||
name: "Rust".to_string(),
|
name: "Rust".to_string(),
|
||||||
path_suffixes: vec!["rs".to_string()],
|
path_suffixes: vec!["rs".to_string()],
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
tree_sitter_rust::language(),
|
tree_sitter_rust::language(),
|
||||||
)));
|
)));
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
name = "Rust"
|
name = "Rust"
|
||||||
path_suffixes = ["rs"]
|
path_suffixes = ["rs"]
|
||||||
bracket_pairs = [
|
autoclose_pairs = [
|
||||||
{ start = "{", end = "}" },
|
{ start = "{", end = "}" },
|
||||||
{ start = "[", end = "]" },
|
{ start = "[", end = "]" },
|
||||||
{ start = "(", end = ")" },
|
{ start = "(", end = ")" },
|
||||||
{ start = "<", end = ">" },
|
{ start = "\"", end = "\"" },
|
||||||
|
{ start = "/*", end = " */" },
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue