Start maintaining a syntax tree on buffers
This commit is contained in:
committed by
Antonio Scandurra
parent
4e74a8726c
commit
46e6b2cff4
@@ -9,19 +9,21 @@ pub use rope::{ChunksIter, Rope, TextSummary};
|
||||
use seahash::SeaHasher;
|
||||
pub use selection::*;
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use tree_sitter::{InputEdit, Parser};
|
||||
|
||||
use crate::{
|
||||
editor::Bias,
|
||||
language::Language,
|
||||
language::{Language, Tree},
|
||||
operation_queue::{self, OperationQueue},
|
||||
sum_tree::{self, FilterCursor, SeekBias, SumTree},
|
||||
time::{self, ReplicaId},
|
||||
time::{self, Global, ReplicaId},
|
||||
worktree::FileHandle,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{AppContext, Entity, ModelContext, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cmp,
|
||||
hash::BuildHasher,
|
||||
iter::{self, Iterator},
|
||||
@@ -57,6 +59,10 @@ type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
#[cfg(not(test))]
|
||||
type HashSet<T> = std::collections::HashSet<T>;
|
||||
|
||||
thread_local! {
|
||||
pub static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
fragments: SumTree<Fragment>,
|
||||
visible_text: Rope,
|
||||
@@ -70,6 +76,8 @@ pub struct Buffer {
|
||||
history: History,
|
||||
file: Option<FileHandle>,
|
||||
language: Option<Arc<Language>>,
|
||||
tree: Option<Tree>,
|
||||
is_parsing: bool,
|
||||
selections: HashMap<SelectionSetId, Arc<[Selection]>>,
|
||||
pub selections_last_update: SelectionsVersion,
|
||||
deferred_ops: OperationQueue<Operation>,
|
||||
@@ -465,7 +473,7 @@ impl Buffer {
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
let mut result = Self {
|
||||
visible_text,
|
||||
deleted_text: Rope::new(),
|
||||
fragments,
|
||||
@@ -476,6 +484,8 @@ impl Buffer {
|
||||
undo_map: Default::default(),
|
||||
history,
|
||||
file,
|
||||
tree: None,
|
||||
is_parsing: false,
|
||||
language,
|
||||
saved_mtime,
|
||||
selections: HashMap::default(),
|
||||
@@ -485,7 +495,9 @@ impl Buffer {
|
||||
replica_id,
|
||||
local_clock: time::Local::new(replica_id),
|
||||
lamport_clock: time::Lamport::new(replica_id),
|
||||
}
|
||||
};
|
||||
result.reparse(ctx);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Rope {
|
||||
@@ -534,6 +546,109 @@ impl Buffer {
|
||||
ctx.emit(Event::Saved);
|
||||
}
|
||||
|
||||
fn reparse(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
// Avoid spawning a new parsing task if the buffer is already being reparsed
|
||||
// due to an earlier edit.
|
||||
if self.is_parsing {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(language) = self.language.clone() {
|
||||
self.is_parsing = true;
|
||||
let mut old_text = self.visible_text.clone();
|
||||
let mut old_tree = self.tree.clone();
|
||||
let mut old_version = self.version();
|
||||
let mut had_changes = true;
|
||||
ctx.spawn(|handle, mut ctx| async move {
|
||||
while had_changes {
|
||||
// Parse the current text in a background thread.
|
||||
let (mut tree, text) = ctx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let language = language.clone();
|
||||
async move {
|
||||
let tree = Self::parse_text(&old_text, old_tree, &language);
|
||||
(tree, old_text)
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
// When the parsing completes, check if any new changes have occurred since
|
||||
// this parse began. If so, edit the new tree to reflect these new changes.
|
||||
let (has_changes, new_text, new_tree, new_version) =
|
||||
handle.update(&mut ctx, move |this, ctx| {
|
||||
let mut delta = 0_isize;
|
||||
let mut has_further_changes = false;
|
||||
for Edit {
|
||||
old_range,
|
||||
new_range,
|
||||
} in this.edits_since(old_version)
|
||||
{
|
||||
let old_len = old_range.end - old_range.start;
|
||||
let new_len = new_range.end - new_range.start;
|
||||
let old_start = (old_range.start as isize + delta) as usize;
|
||||
tree.edit(&InputEdit {
|
||||
start_byte: old_start,
|
||||
old_end_byte: old_start + old_len,
|
||||
new_end_byte: old_start + new_len,
|
||||
start_position: text.to_point(old_start).into(),
|
||||
old_end_position: text.to_point(old_start + old_len).into(),
|
||||
new_end_position: this
|
||||
.point_for_offset(old_start + new_len)
|
||||
.unwrap()
|
||||
.into(),
|
||||
});
|
||||
delta += new_len as isize - old_len as isize;
|
||||
has_further_changes = true;
|
||||
}
|
||||
|
||||
this.tree = Some(tree);
|
||||
ctx.emit(Event::Reparsed);
|
||||
|
||||
// If there were new changes, then continue the loop, spawning a new
|
||||
// parsing task. Otherwise, record the fact that parsing is complete.
|
||||
if has_further_changes {
|
||||
(
|
||||
true,
|
||||
this.visible_text.clone(),
|
||||
this.tree.clone(),
|
||||
this.version(),
|
||||
)
|
||||
} else {
|
||||
this.is_parsing = false;
|
||||
(false, Rope::new(), None, Global::new())
|
||||
}
|
||||
});
|
||||
|
||||
had_changes = has_changes;
|
||||
old_text = new_text;
|
||||
old_tree = new_tree;
|
||||
old_version = new_version;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_text(text: &Rope, old_tree: Option<Tree>, language: &Language) -> Tree {
|
||||
PARSER.with(|parser| {
|
||||
let mut parser = parser.borrow_mut();
|
||||
parser
|
||||
.set_language(language.grammar)
|
||||
.expect("incompatible grammar");
|
||||
let mut chunks = text.chunks_in_range(0..text.len());
|
||||
parser
|
||||
.parse_with(
|
||||
&mut move |offset, _| {
|
||||
chunks.seek(offset);
|
||||
chunks.next().map(str::as_bytes).unwrap_or(&[])
|
||||
},
|
||||
old_tree.as_ref(),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
fn diff(&self, new_text: Arc<str>, ctx: &AppContext) -> Task<Diff> {
|
||||
// TODO: it would be nice to not allocate here.
|
||||
let old_text = self.text();
|
||||
@@ -725,6 +840,7 @@ impl Buffer {
|
||||
|
||||
if self.edits_since(since).next().is_some() {
|
||||
self.did_edit(was_dirty, ctx);
|
||||
self.reparse(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -746,17 +862,32 @@ impl Buffer {
|
||||
self.start_transaction_at(None, Instant::now())?;
|
||||
|
||||
let new_text = new_text.into();
|
||||
let old_ranges = old_ranges
|
||||
.into_iter()
|
||||
.map(|range| range.start.to_offset(self)..range.end.to_offset(self))
|
||||
.collect::<Vec<Range<usize>>>();
|
||||
|
||||
if let Some(tree) = self.tree.as_mut() {
|
||||
let new_extent = TextSummary::from(new_text.as_str()).lines;
|
||||
for old_range in old_ranges.iter().rev() {
|
||||
let start_position = self.visible_text.to_point(old_range.start);
|
||||
tree.edit(&InputEdit {
|
||||
start_byte: old_range.start,
|
||||
old_end_byte: old_range.end,
|
||||
new_end_byte: old_range.start + new_text.len(),
|
||||
start_position: start_position.into(),
|
||||
old_end_position: self.visible_text.to_point(old_range.end).into(),
|
||||
new_end_position: (start_position + new_extent).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let new_text = if new_text.len() > 0 {
|
||||
Some(new_text)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let old_ranges = old_ranges
|
||||
.into_iter()
|
||||
.map(|range| range.start.to_offset(self)..range.end.to_offset(self))
|
||||
.collect::<Vec<Range<usize>>>();
|
||||
|
||||
let has_new_text = new_text.is_some();
|
||||
let ops = self.splice_fragments(
|
||||
old_ranges
|
||||
@@ -1890,6 +2021,8 @@ impl Clone for Buffer {
|
||||
deferred_ops: self.deferred_ops.clone(),
|
||||
file: self.file.clone(),
|
||||
language: self.language.clone(),
|
||||
tree: self.tree.clone(),
|
||||
is_parsing: false,
|
||||
deferred_replicas: self.deferred_replicas.clone(),
|
||||
replica_id: self.replica_id,
|
||||
local_clock: self.local_clock.clone(),
|
||||
@@ -1957,6 +2090,7 @@ pub enum Event {
|
||||
Saved,
|
||||
FileHandleChanged,
|
||||
Reloaded,
|
||||
Reparsed,
|
||||
}
|
||||
|
||||
impl Entity for Buffer {
|
||||
|
||||
@@ -98,3 +98,21 @@ impl Ord for Point {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<tree_sitter::Point> for Point {
|
||||
fn into(self) -> tree_sitter::Point {
|
||||
tree_sitter::Point {
|
||||
row: self.row as usize,
|
||||
column: self.column as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tree_sitter::Point> for Point {
|
||||
fn from(point: tree_sitter::Point) -> Self {
|
||||
Self {
|
||||
row: point.row as u32,
|
||||
column: point.column as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,11 +284,13 @@ impl<'a> ChunksIter<'a> {
|
||||
self.range.start.max(*self.chunks.start())
|
||||
}
|
||||
|
||||
pub fn advance_to(&mut self, offset: usize) {
|
||||
pub fn seek(&mut self, offset: usize) {
|
||||
if offset >= self.chunks.end() {
|
||||
self.chunks.seek_forward(&offset, SeekBias::Right, &());
|
||||
self.range.start = offset;
|
||||
} else {
|
||||
self.chunks.seek(&offset, SeekBias::Right, &());
|
||||
}
|
||||
self.range.start = offset;
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> Option<&'a str> {
|
||||
|
||||
@@ -2246,6 +2246,7 @@ impl BufferView {
|
||||
buffer::Event::Saved => ctx.emit(Event::Saved),
|
||||
buffer::Event::FileHandleChanged => ctx.emit(Event::FileHandleChanged),
|
||||
buffer::Event::Reloaded => ctx.emit(Event::FileHandleChanged),
|
||||
buffer::Event::Reparsed => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,7 +685,7 @@ impl<'a> Iterator for Chunks<'a> {
|
||||
// advance the transform and buffer cursors to the end of the fold.
|
||||
if let Some(display_text) = transform.display_text {
|
||||
self.buffer_offset += transform.summary.buffer.bytes;
|
||||
self.buffer_chunks.advance_to(self.buffer_offset);
|
||||
self.buffer_chunks.seek(self.buffer_offset);
|
||||
|
||||
while self.buffer_offset >= self.transform_cursor.end().buffer.bytes
|
||||
&& self.transform_cursor.item().is_some()
|
||||
|
||||
@@ -9,9 +9,9 @@ pub use tree_sitter::{Parser, Tree};
|
||||
pub struct LanguageDir;
|
||||
|
||||
pub struct Language {
|
||||
name: String,
|
||||
grammar: Grammar,
|
||||
highlight_query: Query,
|
||||
pub name: String,
|
||||
pub grammar: Grammar,
|
||||
pub highlight_query: Query,
|
||||
path_suffixes: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user