Start work on a Buffer API for requesting autoindent on the next parse
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
a0c8b60a1b
commit
b83b4ad7c7
5 changed files with 341 additions and 31 deletions
|
@ -23,8 +23,9 @@ pub struct AutoclosePair {
|
|||
pub struct Language {
|
||||
pub(crate) config: LanguageConfig,
|
||||
pub(crate) grammar: Grammar,
|
||||
pub(crate) highlight_query: Query,
|
||||
pub(crate) highlights_query: Query,
|
||||
pub(crate) brackets_query: Query,
|
||||
pub(crate) indents_query: Query,
|
||||
pub(crate) highlight_map: Mutex<HighlightMap>,
|
||||
}
|
||||
|
||||
|
@ -68,19 +69,25 @@ impl Language {
|
|||
Self {
|
||||
config,
|
||||
brackets_query: Query::new(grammar, "").unwrap(),
|
||||
highlight_query: Query::new(grammar, "").unwrap(),
|
||||
highlights_query: Query::new(grammar, "").unwrap(),
|
||||
indents_query: Query::new(grammar, "").unwrap(),
|
||||
grammar,
|
||||
highlight_map: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_highlights_query(mut self, highlights_query_source: &str) -> Result<Self> {
|
||||
self.highlight_query = Query::new(self.grammar, highlights_query_source)?;
|
||||
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
|
||||
self.highlights_query = Query::new(self.grammar, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_brackets_query(mut self, brackets_query_source: &str) -> Result<Self> {
|
||||
self.brackets_query = Query::new(self.grammar, brackets_query_source)?;
|
||||
pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
|
||||
self.brackets_query = Query::new(self.grammar, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_indents_query(mut self, source: &str) -> Result<Self> {
|
||||
self.indents_query = Query::new(self.grammar, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
|
@ -97,7 +104,8 @@ impl Language {
|
|||
}
|
||||
|
||||
pub fn set_theme(&self, theme: &SyntaxTheme) {
|
||||
*self.highlight_map.lock() = HighlightMap::new(self.highlight_query.capture_names(), theme);
|
||||
*self.highlight_map.lock() =
|
||||
HighlightMap::new(self.highlights_query.capture_names(), theme);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,28 +118,22 @@ mod tests {
|
|||
let grammar = tree_sitter_rust::language();
|
||||
let registry = LanguageRegistry {
|
||||
languages: vec![
|
||||
Arc::new(Language {
|
||||
config: LanguageConfig {
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".to_string(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
grammar,
|
||||
highlight_query: Query::new(grammar, "").unwrap(),
|
||||
brackets_query: Query::new(grammar, "").unwrap(),
|
||||
highlight_map: Default::default(),
|
||||
}),
|
||||
Arc::new(Language {
|
||||
config: LanguageConfig {
|
||||
)),
|
||||
Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Make".to_string(),
|
||||
path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
grammar,
|
||||
highlight_query: Query::new(grammar, "").unwrap(),
|
||||
brackets_query: Query::new(grammar, "").unwrap(),
|
||||
highlight_map: Default::default(),
|
||||
}),
|
||||
)),
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -30,10 +30,12 @@ use std::{
|
|||
any::Any,
|
||||
cell::RefCell,
|
||||
cmp,
|
||||
collections::BTreeMap,
|
||||
convert::{TryFrom, TryInto},
|
||||
ffi::OsString,
|
||||
hash::BuildHasher,
|
||||
iter::Iterator,
|
||||
mem,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
|
@ -149,6 +151,7 @@ impl Drop for QueryCursorHandle {
|
|||
QUERY_CURSORS.lock().push(cursor)
|
||||
}
|
||||
}
|
||||
const SPACES: &'static str = " ";
|
||||
|
||||
pub struct Buffer {
|
||||
fragments: SumTree<Fragment>,
|
||||
|
@ -162,6 +165,7 @@ pub struct Buffer {
|
|||
history: History,
|
||||
file: Option<Box<dyn File>>,
|
||||
language: Option<Arc<Language>>,
|
||||
autoindent_requests: Vec<AutoindentRequest>,
|
||||
sync_parse_timeout: Duration,
|
||||
syntax_tree: Mutex<Option<SyntaxTree>>,
|
||||
parsing_in_background: bool,
|
||||
|
@ -190,6 +194,13 @@ struct SyntaxTree {
|
|||
version: clock::Global,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct AutoindentRequest {
|
||||
position: Anchor,
|
||||
indent_size: u8,
|
||||
force: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Transaction {
|
||||
start: clock::Global,
|
||||
|
@ -628,6 +639,7 @@ impl Buffer {
|
|||
parsing_in_background: false,
|
||||
parse_count: 0,
|
||||
sync_parse_timeout: Duration::from_millis(1),
|
||||
autoindent_requests: Default::default(),
|
||||
language,
|
||||
saved_mtime,
|
||||
selections: HashMap::default(),
|
||||
|
@ -878,14 +890,13 @@ impl Buffer {
|
|||
}
|
||||
|
||||
if let Some(language) = self.language.clone() {
|
||||
// The parse tree is out of date, so grab the syntax tree to synchronously
|
||||
// splice all the edits that have happened since the last parse.
|
||||
let old_tree = self.syntax_tree();
|
||||
let parsed_text = self.visible_text.clone();
|
||||
let old_snapshot = self.snapshot();
|
||||
let parsed_version = self.version();
|
||||
let parse_task = cx.background().spawn({
|
||||
let language = language.clone();
|
||||
async move { Self::parse_text(&parsed_text, old_tree, &language) }
|
||||
let text = old_snapshot.visible_text.clone();
|
||||
let tree = old_snapshot.tree.clone();
|
||||
async move { Self::parse_text(&text, tree, &language) }
|
||||
});
|
||||
|
||||
match cx
|
||||
|
@ -894,13 +905,12 @@ impl Buffer {
|
|||
{
|
||||
Ok(new_tree) => {
|
||||
*self.syntax_tree.lock() = Some(SyntaxTree {
|
||||
tree: new_tree,
|
||||
tree: new_tree.clone(),
|
||||
dirty: false,
|
||||
version: parsed_version,
|
||||
});
|
||||
self.parse_count += 1;
|
||||
cx.emit(Event::Reparsed);
|
||||
cx.notify();
|
||||
self.did_finish_parsing(new_tree, old_snapshot, language, cx);
|
||||
return true;
|
||||
}
|
||||
Err(parse_task) => {
|
||||
|
@ -914,7 +924,7 @@ impl Buffer {
|
|||
});
|
||||
let parse_again = this.version > parsed_version || language_changed;
|
||||
*this.syntax_tree.lock() = Some(SyntaxTree {
|
||||
tree: new_tree,
|
||||
tree: new_tree.clone(),
|
||||
dirty: false,
|
||||
version: parsed_version,
|
||||
});
|
||||
|
@ -925,8 +935,7 @@ impl Buffer {
|
|||
return;
|
||||
}
|
||||
|
||||
cx.emit(Event::Reparsed);
|
||||
cx.notify();
|
||||
this.did_finish_parsing(new_tree, old_snapshot, language, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
@ -956,6 +965,243 @@ impl Buffer {
|
|||
})
|
||||
}
|
||||
|
||||
fn did_finish_parsing(
|
||||
&mut self,
|
||||
new_tree: Tree,
|
||||
old_snapshot: Snapshot,
|
||||
language: Arc<Language>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let mut autoindent_requests_by_row = BTreeMap::<u32, AutoindentRequest>::default();
|
||||
let mut autoindent_requests = mem::take(&mut self.autoindent_requests);
|
||||
for request in autoindent_requests.drain(..) {
|
||||
let row = request.position.to_point(&*self).row;
|
||||
autoindent_requests_by_row
|
||||
.entry(row)
|
||||
.and_modify(|req| {
|
||||
req.indent_size = req.indent_size.max(request.indent_size);
|
||||
req.force |= request.force;
|
||||
})
|
||||
.or_insert(request);
|
||||
}
|
||||
self.autoindent_requests = autoindent_requests;
|
||||
|
||||
let mut cursor = QueryCursorHandle::new();
|
||||
|
||||
self.start_transaction(None).unwrap();
|
||||
let mut row_range = None;
|
||||
for row in autoindent_requests_by_row.keys().copied() {
|
||||
match &mut row_range {
|
||||
None => row_range = Some(row..(row + 1)),
|
||||
Some(range) => {
|
||||
if range.end == row {
|
||||
range.end += 1;
|
||||
} else {
|
||||
self.perform_autoindent(
|
||||
range.clone(),
|
||||
&new_tree,
|
||||
&old_snapshot,
|
||||
&autoindent_requests_by_row,
|
||||
language.as_ref(),
|
||||
&mut cursor,
|
||||
cx,
|
||||
);
|
||||
row_range.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(range) = row_range {
|
||||
self.perform_autoindent(
|
||||
range,
|
||||
&new_tree,
|
||||
&old_snapshot,
|
||||
&autoindent_requests_by_row,
|
||||
language.as_ref(),
|
||||
&mut cursor,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
self.end_transaction(None, cx).unwrap();
|
||||
|
||||
cx.emit(Event::Reparsed);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn perform_autoindent(
|
||||
&mut self,
|
||||
row_range: Range<u32>,
|
||||
new_tree: &Tree,
|
||||
old_snapshot: &Snapshot,
|
||||
autoindent_requests: &BTreeMap<u32, AutoindentRequest>,
|
||||
language: &Language,
|
||||
cursor: &mut QueryCursor,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let max_row = self.row_count() - 1;
|
||||
let prev_non_blank_row = self.prev_non_blank_row(row_range.start);
|
||||
|
||||
// Get the "indentation ranges" that intersect this row range.
|
||||
let indent_capture_ix = language.indents_query.capture_index_for_name("indent");
|
||||
let end_capture_ix = language.indents_query.capture_index_for_name("end");
|
||||
cursor.set_point_range(
|
||||
Point::new(prev_non_blank_row.unwrap_or(row_range.start), 0).into()
|
||||
..Point::new(row_range.end, 0).into(),
|
||||
);
|
||||
let mut indentation_ranges = Vec::<Range<u32>>::new();
|
||||
for mat in cursor.matches(
|
||||
&language.indents_query,
|
||||
new_tree.root_node(),
|
||||
TextProvider(&self.visible_text),
|
||||
) {
|
||||
let mut start_row = None;
|
||||
let mut end_row = None;
|
||||
for capture in mat.captures {
|
||||
if Some(capture.index) == indent_capture_ix {
|
||||
start_row.get_or_insert(capture.node.start_position().row as u32);
|
||||
end_row.get_or_insert(max_row.min(capture.node.end_position().row as u32 + 1));
|
||||
} else if Some(capture.index) == end_capture_ix {
|
||||
end_row = Some(capture.node.start_position().row as u32);
|
||||
}
|
||||
}
|
||||
if let Some((start_row, end_row)) = start_row.zip(end_row) {
|
||||
let range = start_row..end_row;
|
||||
match indentation_ranges.binary_search_by_key(&range.start, |r| r.start) {
|
||||
Err(ix) => indentation_ranges.insert(ix, range),
|
||||
Ok(ix) => {
|
||||
let prev_range = &mut indentation_ranges[ix];
|
||||
prev_range.end = prev_range.end.max(range.end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Adjustment {
|
||||
row: u32,
|
||||
indent: bool,
|
||||
outdent: bool,
|
||||
}
|
||||
|
||||
let mut adjustments = Vec::<Adjustment>::new();
|
||||
'outer: for range in &indentation_ranges {
|
||||
let mut indent_row = range.start;
|
||||
loop {
|
||||
indent_row += 1;
|
||||
if indent_row > max_row {
|
||||
continue 'outer;
|
||||
}
|
||||
if row_range.contains(&indent_row) || !self.is_line_blank(indent_row) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut outdent_row = range.end;
|
||||
loop {
|
||||
outdent_row += 1;
|
||||
if outdent_row > max_row {
|
||||
break;
|
||||
}
|
||||
if row_range.contains(&outdent_row) || !self.is_line_blank(outdent_row) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match adjustments.binary_search_by_key(&indent_row, |a| a.row) {
|
||||
Ok(ix) => adjustments[ix].indent = true,
|
||||
Err(ix) => adjustments.insert(
|
||||
ix,
|
||||
Adjustment {
|
||||
row: indent_row,
|
||||
indent: true,
|
||||
outdent: false,
|
||||
},
|
||||
),
|
||||
}
|
||||
match adjustments.binary_search_by_key(&outdent_row, |a| a.row) {
|
||||
Ok(ix) => adjustments[ix].outdent = true,
|
||||
Err(ix) => adjustments.insert(
|
||||
ix,
|
||||
Adjustment {
|
||||
row: outdent_row,
|
||||
indent: false,
|
||||
outdent: true,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let mut adjustments = adjustments.iter().peekable();
|
||||
for row in row_range {
|
||||
if let Some(request) = autoindent_requests.get(&row) {
|
||||
while let Some(adjustment) = adjustments.peek() {
|
||||
if adjustment.row < row {
|
||||
adjustments.next();
|
||||
} else {
|
||||
if adjustment.row == row {
|
||||
match (adjustment.indent, adjustment.outdent) {
|
||||
(true, false) => self.indent_line(row, request.indent_size, cx),
|
||||
(false, true) => self.outdent_line(row, request.indent_size, cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_non_blank_row(&self, mut row: u32) -> Option<u32> {
|
||||
while row > 0 {
|
||||
row -= 1;
|
||||
if !self.is_line_blank(row) {
|
||||
return Some(row);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn next_non_blank_row(&self, mut row: u32) -> Option<u32> {
|
||||
let row_count = self.row_count();
|
||||
row += 1;
|
||||
while row < row_count {
|
||||
if !self.is_line_blank(row) {
|
||||
return Some(row);
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn indent_line(&mut self, row: u32, size: u8, cx: &mut ModelContext<Self>) {
|
||||
self.edit(
|
||||
[Point::new(row, 0)..Point::new(row, 0)],
|
||||
&SPACES[..(size as usize)],
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn outdent_line(&mut self, row: u32, size: u8, cx: &mut ModelContext<Self>) {
|
||||
let mut end_column = 0;
|
||||
for c in self.chars_at(Point::new(row, 0)) {
|
||||
if c == ' ' {
|
||||
end_column += 1;
|
||||
if end_column == size as u32 {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.edit([Point::new(row, 0)..Point::new(row, end_column)], "", cx);
|
||||
}
|
||||
|
||||
fn is_line_blank(&self, row: u32) -> bool {
|
||||
self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row)))
|
||||
.all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none())
|
||||
}
|
||||
|
||||
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
||||
if let Some(tree) = self.syntax_tree() {
|
||||
let root = tree.root_node();
|
||||
|
@ -997,6 +1243,18 @@ impl Buffer {
|
|||
.min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
|
||||
}
|
||||
|
||||
pub fn request_autoindent_for_line(&mut self, row: u32, indent_size: u8, force: bool) {
|
||||
assert!(
|
||||
self.history.transaction_depth > 0,
|
||||
"autoindent can only be requested during a transaction"
|
||||
);
|
||||
self.autoindent_requests.push(AutoindentRequest {
|
||||
position: self.anchor_before(Point::new(row, 0)),
|
||||
indent_size,
|
||||
force,
|
||||
});
|
||||
}
|
||||
|
||||
fn diff(&self, new_text: Arc<str>, cx: &AppContext) -> Task<Diff> {
|
||||
// TODO: it would be nice to not allocate here.
|
||||
let old_text = self.text();
|
||||
|
@ -2162,6 +2420,7 @@ impl Clone for Buffer {
|
|||
parsing_in_background: false,
|
||||
sync_parse_timeout: self.sync_parse_timeout,
|
||||
parse_count: self.parse_count,
|
||||
autoindent_requests: self.autoindent_requests.clone(),
|
||||
deferred_replicas: self.deferred_replicas.clone(),
|
||||
replica_id: self.replica_id,
|
||||
remote_id: self.remote_id.clone(),
|
||||
|
@ -2227,7 +2486,7 @@ impl Snapshot {
|
|||
let chunks = self.visible_text.chunks_in_range(range.clone());
|
||||
if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) {
|
||||
let captures = self.query_cursor.set_byte_range(range.clone()).captures(
|
||||
&language.highlight_query,
|
||||
&language.highlights_query,
|
||||
tree.root_node(),
|
||||
TextProvider(&self.visible_text),
|
||||
);
|
||||
|
@ -4016,6 +4275,29 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_request_autoindent(mut cx: gpui::TestAppContext) {
|
||||
let rust_lang = rust_lang();
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let text = "fn a() {}".into();
|
||||
Buffer::from_history(0, History::new(text), None, Some(rust_lang.clone()), cx)
|
||||
});
|
||||
|
||||
buffer
|
||||
.condition(&cx, |buffer, _| !buffer.is_parsing())
|
||||
.await;
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.start_transaction(None).unwrap();
|
||||
buffer.edit([8..8], "\n\n", cx);
|
||||
buffer.request_autoindent_for_line(1, 4, true);
|
||||
assert_eq!(buffer.text(), "fn a() {\n\n}");
|
||||
|
||||
buffer.end_transaction(None, cx).unwrap();
|
||||
assert_eq!(buffer.text(), "fn a() {\n \n}");
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Envelope<T: Clone> {
|
||||
message: T,
|
||||
|
@ -4104,6 +4386,8 @@ mod tests {
|
|||
},
|
||||
tree_sitter_rust::language(),
|
||||
)
|
||||
.with_indents_query(r#" (_ "{" "}" @end) @indent "#)
|
||||
.unwrap()
|
||||
.with_brackets_query(r#" ("{" @open "}" @close) "#)
|
||||
.unwrap(),
|
||||
)
|
||||
|
|
|
@ -745,6 +745,7 @@ impl Editor {
|
|||
if !self.skip_autoclose_end(text, cx) {
|
||||
self.start_transaction(cx);
|
||||
self.insert(text, cx);
|
||||
self.request_autoindent(cx);
|
||||
self.autoclose_pairs(cx);
|
||||
self.end_transaction(cx);
|
||||
}
|
||||
|
@ -792,6 +793,17 @@ impl Editor {
|
|||
self.end_transaction(cx);
|
||||
}
|
||||
|
||||
fn request_autoindent(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let selections = self.selections(cx);
|
||||
let tab_size = self.build_settings.borrow()(cx).tab_size as u8;
|
||||
self.buffer.update(cx, |buffer, _| {
|
||||
for selection in selections.iter() {
|
||||
let row = selection.head().to_point(&*buffer).row;
|
||||
buffer.request_autoindent_for_line(row, tab_size, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn autoclose_pairs(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let selections = self.selections(cx);
|
||||
let new_autoclose_pair_state = self.buffer.update(cx, |buffer, cx| {
|
||||
|
|
10
crates/zed/languages/rust/indents.scm
Normal file
10
crates/zed/languages/rust/indents.scm
Normal file
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
(where_clause)
|
||||
(field_expression)
|
||||
(call_expression)
|
||||
] @indent
|
||||
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "<" ">" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
|
@ -22,6 +22,8 @@ fn rust() -> Language {
|
|||
.unwrap()
|
||||
.with_brackets_query(load_query("rust/brackets.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_indents_query(load_query("rust/indents.scm").as_ref())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn load_query(path: &str) -> Cow<'static, str> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue