diff --git a/.gitignore b/.gitignore index f6ea6b5d9..0e27dd464 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target helix-term/rustfmt.toml +helix-syntax/languages/ diff --git a/Cargo.lock b/Cargo.lock index 2919b53ec..c9351ac63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,7 @@ name = "helix-core" version = "0.1.0" dependencies = [ "anyhow", + "helix-syntax", "ropey", "smallvec", "tendril", diff --git a/Cargo.toml b/Cargo.toml index adebb50d2..db3530c94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,5 @@ members = [ ] # Build helix-syntax in release mode to make the code path faster in development. -[profile.dev.package."helix-syntax"] -opt-level = 3 +# [profile.dev.package."helix-syntax"] +# opt-level = 3 diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 23eccdc23..0d3bd4a44 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -7,6 +7,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +helix-syntax = { path = "../helix-syntax" } + ropey = "1.2.0" anyhow = "1.0.31" smallvec = "1.4.0" diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 069dc116b..8c58d7342 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,6 +1,7 @@ #![allow(unused)] pub mod commands; pub mod graphemes; +mod position; mod selection; pub mod state; pub mod syntax; @@ -9,8 +10,10 @@ mod transaction; pub use ropey::{Rope, RopeSlice}; pub use tendril::StrTendril as Tendril; +pub use position::Position; pub use selection::Range as SelectionRange; pub use selection::Selection; +pub use syntax::Syntax; pub use state::State; diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs new file mode 100644 index 000000000..8bc480948 --- /dev/null +++ b/helix-core/src/position.rs @@ -0,0 +1,57 @@ +/// Represents a single point in a text buffer. Zero indexed. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Position { + pub row: usize, + pub col: usize, +} + +impl Position { + pub const fn new(row: usize, col: usize) -> Self { + Self { row, col } + } + + pub const fn is_zero(self) -> bool { + self.row == 0 && self.col == 0 + } + + // TODO: generalize + pub fn traverse(self, text: &crate::Tendril) -> Self { + let Self { mut row, mut col } = self; + // TODO: there should be a better way here + for ch in text.chars() { + if ch == '\n' { + row += 1; + col = 0; + } else { + col += 1; + } + } + Self { row, col } + } +} + +impl From<(usize, usize)> for Position { + fn from(tuple: (usize, usize)) -> Self { + Position { + row: tuple.0, + col: tuple.1, + } + } +} + +impl Into for Position { + fn into(self) -> tree_sitter::Point { + tree_sitter::Point::new(self.row, self.col) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_ordering() { + // (0, 5) is less than (1, 0) + assert!(Position::new(0, 5) < Position::new(1, 0)); + } +} diff --git a/helix-core/src/state.rs b/helix-core/src/state.rs index e41ee565a..150874b15 100644 --- a/helix-core/src/state.rs +++ b/helix-core/src/state.rs @@ -1,5 +1,5 @@ use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes}; -use crate::{Rope, RopeSlice, Selection, SelectionRange}; +use crate::{Position, Rope, RopeSlice, Selection, SelectionRange, Syntax}; use anyhow::Error; use std::path::PathBuf; @@ -17,6 +17,9 @@ pub struct State { pub(crate) doc: Rope, pub(crate) selection: Selection, pub(crate) mode: Mode, + + // + pub syntax: Option, } #[derive(Copy, Clone, PartialEq, Eq)] @@ -40,6 +43,7 @@ impl State { doc, selection: Selection::single(0, 0), mode: Mode::Normal, + syntax: None, } } @@ -54,6 +58,29 @@ impl State { let mut state = Self::new(doc); state.path = Some(path); + let language = helix_syntax::get_language(&helix_syntax::LANG::Rust); + + let mut highlight_config = crate::syntax::HighlightConfiguration::new( + language, + &std::fs::read_to_string( + "../helix-syntax/languages/tree-sitter-rust/queries/highlights.scm", + ) + .unwrap(), + &std::fs::read_to_string( + "../helix-syntax/languages/tree-sitter-rust/queries/injections.scm", + ) + .unwrap(), + "", // locals.scm + ) + .unwrap(); + + // TODO: config.configure(scopes) is now delayed, is that ok? + + // TODO: get_language is called twice + let syntax = Syntax::new(helix_syntax::LANG::Rust, &state.doc, highlight_config); + + state.syntax = Some(syntax); + Ok(state) } @@ -175,29 +202,29 @@ impl State { } /// Coordinates are a 0-indexed line and column pair. -type Coords = (usize, usize); // line, col +pub type Coords = (usize, usize); // line, col /// Convert a character index to (line, column) coordinates. -pub fn coords_at_pos(text: &RopeSlice, pos: usize) -> Coords { +pub fn coords_at_pos(text: &RopeSlice, pos: usize) -> Position { let line = text.char_to_line(pos); let line_start = text.line_to_char(line); let col = text.slice(line_start..pos).len_chars(); - (line, col) + Position::new(line, col) } /// Convert (line, column) coordinates to a character index. -pub fn pos_at_coords(text: &RopeSlice, coords: Coords) -> usize { - let (line, col) = coords; - let line_start = text.line_to_char(line); +pub fn pos_at_coords(text: &RopeSlice, coords: Position) -> usize { + let Position { row, col } = coords; + let line_start = text.line_to_char(row); nth_next_grapheme_boundary(text, line_start, col) } fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) -> usize { - let (line, col) = coords_at_pos(text, pos); + let Position { row, col } = coords_at_pos(text, pos); let new_line = match dir { - Direction::Backward => line.saturating_sub(count), - Direction::Forward => std::cmp::min(line.saturating_add(count), text.len_lines() - 1), + Direction::Backward => row.saturating_sub(count), + Direction::Forward => std::cmp::min(row.saturating_add(count), text.len_lines() - 1), }; // convert to 0-indexed, subtract another 1 because len_chars() counts \n @@ -210,7 +237,7 @@ fn move_vertically(text: &RopeSlice, dir: Direction, pos: usize, count: usize) - col }; - pos_at_coords(text, (new_line, new_col)) + pos_at_coords(text, Position::new(new_line, new_col)) } #[cfg(test)] @@ -220,32 +247,32 @@ mod test { #[test] fn test_coords_at_pos() { let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); - assert_eq!(coords_at_pos(&text.slice(..), 0), (0, 0)); - assert_eq!(coords_at_pos(&text.slice(..), 5), (0, 5)); // position on \n - assert_eq!(coords_at_pos(&text.slice(..), 6), (1, 0)); // position on w - assert_eq!(coords_at_pos(&text.slice(..), 7), (1, 1)); // position on o - assert_eq!(coords_at_pos(&text.slice(..), 10), (1, 4)); // position on d + assert_eq!(coords_at_pos(&text.slice(..), 0), (0, 0).into()); + assert_eq!(coords_at_pos(&text.slice(..), 5), (0, 5).into()); // position on \n + assert_eq!(coords_at_pos(&text.slice(..), 6), (1, 0).into()); // position on w + assert_eq!(coords_at_pos(&text.slice(..), 7), (1, 1).into()); // position on o + assert_eq!(coords_at_pos(&text.slice(..), 10), (1, 4).into()); // position on d } #[test] fn test_pos_at_coords() { let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); - assert_eq!(pos_at_coords(&text.slice(..), (0, 0)), 0); - assert_eq!(pos_at_coords(&text.slice(..), (0, 5)), 5); // position on \n - assert_eq!(pos_at_coords(&text.slice(..), (1, 0)), 6); // position on w - assert_eq!(pos_at_coords(&text.slice(..), (1, 1)), 7); // position on o - assert_eq!(pos_at_coords(&text.slice(..), (1, 4)), 10); // position on d + assert_eq!(pos_at_coords(&text.slice(..), (0, 0).into()), 0); + assert_eq!(pos_at_coords(&text.slice(..), (0, 5).into()), 5); // position on \n + assert_eq!(pos_at_coords(&text.slice(..), (1, 0).into()), 6); // position on w + assert_eq!(pos_at_coords(&text.slice(..), (1, 1).into()), 7); // position on o + assert_eq!(pos_at_coords(&text.slice(..), (1, 4).into()), 10); // position on d } #[test] fn test_vertical_move() { let text = Rope::from("abcd\nefg\nwrs"); - let pos = pos_at_coords(&text.slice(..), (0, 4)); + let pos = pos_at_coords(&text.slice(..), (0, 4).into()); let slice = text.slice(..); assert_eq!( coords_at_pos(&slice, move_vertically(&slice, Direction::Forward, pos, 1)), - (1, 2) + (1, 2).into() ); } } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index b661efad3..c0b67f5d1 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -1,66 +1,397 @@ -// pub struct Syntax { -// parser: Parser, -// } +use crate::{Change, Rope, RopeSlice, Transaction}; +pub use helix_syntax::LANG; +pub use helix_syntax::{get_language, get_language_name}; + +pub struct Syntax { + grammar: Language, + parser: Parser, + cursors: Vec, + + config: HighlightConfiguration, -//impl Syntax { -// // buffer, grammar, config, grammars, sync_timeout? -// pub fn new() -> Self { -// unimplemented!() -// // make a new root layer -// // track markers of injections -// // -// // track scope_descriptor: a Vec of scopes for item in tree -// // -// // fetch grammar for parser based on language string -// // update root layer -// } - -// // fn buffer_changed -> call layer.update(range, new_text) on root layer and then all marker layers - -// // call this on transaction.apply() -> buffer_changed(changes) -// // -// // fn parse(language, old_tree, ranges) -// // -// // fn tree() -> Tree -// // -// // - -// // Highlighting -// // fn highlight_iter() -> iterates over all the scopes -// // on_tokenize -// // on_change_highlighting - -// // Commenting -// // comment_strings_for_pos -// // is_commented - -// // Indentation -// // suggested_indent_for_line_at_buffer_row -// // suggested_indent_for_buffer_row -// // indent_level_for_line - -// // TODO: Folding - -// // Syntax APIs -// // get_syntax_node_containing_range -> -// // ... -// // get_syntax_node_at_pos -// // buffer_range_for_scope_at_pos -//} + root_layer: LanguageLayer, +} + +impl Syntax { + // buffer, grammar, config, grammars, sync_timeout? + pub fn new(language: LANG, source: &Rope, config: HighlightConfiguration) -> Self { + // fetch grammar for parser based on language string + let grammar = get_language(&language); + let parser = Parser::new(); + + let root_layer = LanguageLayer::new(); + + // track markers of injections + // track scope_descriptor: a Vec of scopes for item in tree + + let mut syntax = Self { + grammar, + parser, + cursors: Vec::new(), + config, + root_layer, + }; + + // update root layer + syntax.root_layer.parse( + &mut syntax.parser, + &syntax.config, + source, + 0, + vec![Range { + start_byte: 0, + end_byte: usize::MAX, + start_point: Point::new(0, 0), + end_point: Point::new(usize::MAX, usize::MAX), + }], + ); + syntax + } + + pub fn configure(&mut self, scopes: &[String]) { + self.config.configure(scopes) + } + + pub fn update(&mut self, source: &Rope, changeset: &ChangeSet) -> Result<(), Error> { + self.root_layer + .update(&mut self.parser, &self.config, source, changeset) + + // TODO: deal with injections and update them too + } + + // fn buffer_changed -> call layer.update(range, new_text) on root layer and then all marker layers + + // call this on transaction.apply() -> buffer_changed(changes) + // + // fn parse(language, old_tree, ranges) + // + fn tree(&self) -> &Tree { + self.root_layer.tree() + } + // + // + + // Highlighting + + /// Iterate over the highlighted regions for a given slice of source code. + pub fn highlight_iter<'a>( + &'a mut self, + source: &'a [u8], + cancellation_flag: Option<&'a AtomicUsize>, + mut injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, + ) -> Result> + 'a, Error> { + // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which + // prevents them from being moved. But both of these values are really just + // pointers, so it's actually ok to move them. + + let mut cursor = QueryCursor::new(); // reuse a pool + let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) }; + let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; + let query_ref = unsafe { mem::transmute::<_, &'static mut Query>(&mut self.config.query) }; + let config_ref = + unsafe { mem::transmute::<_, &'static HighlightConfiguration>(&self.config) }; + let captures = cursor_ref + .captures(query_ref, tree_ref.root_node(), move |n: Node| { + &source[n.byte_range()] + }) + .peekable(); + + // manually craft the root layer based on the existing tree + let layer = HighlightIterLayer { + highlight_end_stack: Vec::new(), + scope_stack: vec![LocalScope { + inherits: false, + range: 0..usize::MAX, + local_defs: Vec::new(), + }], + cursor, + depth: 0, + _tree: None, + captures, + config: config_ref, + ranges: vec![Range { + start_byte: 0, + end_byte: usize::MAX, + start_point: Point::new(0, 0), + end_point: Point::new(usize::MAX, usize::MAX), + }], + }; + + let mut result = HighlightIter { + source, + byte_offset: 0, + injection_callback, + cancellation_flag, + highlighter: self, + iter_count: 0, + layers: vec![layer], + next_event: None, + last_highlight_range: None, + }; + result.sort_layers(); + Ok(result) + } + // on_tokenize + // on_change_highlighting + + // Commenting + // comment_strings_for_pos + // is_commented + + // Indentation + // suggested_indent_for_line_at_buffer_row + // suggested_indent_for_buffer_row + // indent_level_for_line + + // TODO: Folding + + // Syntax APIs + // get_syntax_node_containing_range -> + // ... + // get_syntax_node_at_pos + // buffer_range_for_scope_at_pos +} pub struct LanguageLayer { // mode -// grammar -// depth -// tree: Tree, + // grammar + // depth + tree: Option, } -// impl LanguageLayer { -// // fn highlight_iter() -> same as Mode but for this layer. Mode composits these -// // fn buffer_changed -// // fn update(range) -// // fn update_injections() -// } +use crate::state::{coords_at_pos, Coords}; +use crate::transaction::{ChangeSet, Operation}; +use crate::Tendril; + +impl LanguageLayer { + pub fn new() -> Self { + Self { tree: None } + } + + fn tree(&self) -> &Tree { + // TODO: no unwrap + self.tree.as_ref().unwrap() + } + + fn parse<'a>( + &mut self, + parser: &mut Parser, + config: &HighlightConfiguration, + source: &Rope, + mut depth: usize, + mut ranges: Vec, + ) -> Result<(), Error> { + if parser.set_included_ranges(&ranges).is_ok() { + parser + .set_language(config.language) + .map_err(|_| Error::InvalidLanguage)?; + + // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) }; + let tree = parser + .parse_with( + &mut |byte, _| { + if byte <= source.len_bytes() { + let (chunk, start_byte, _, _) = source.chunk_at_byte(byte); + chunk[byte - start_byte..].as_bytes() + } else { + // out of range + &[] + } + }, + self.tree.as_ref(), + ) + .ok_or(Error::Cancelled)?; + // unsafe { syntax.parser.set_cancellation_flag(None) }; + // let mut cursor = syntax.cursors.pop().unwrap_or_else(QueryCursor::new); + + // Process combined injections. (ERB, EJS, etc https://github.com/tree-sitter/tree-sitter/pull/526) + // if let Some(combined_injections_query) = &config.combined_injections_query { + // let mut injections_by_pattern_index = + // vec![(None, Vec::new(), false); combined_injections_query.pattern_count()]; + // let matches = + // cursor.matches(combined_injections_query, tree.root_node(), |n: Node| { + // &source[n.byte_range()] + // }); + // for mat in matches { + // let entry = &mut injections_by_pattern_index[mat.pattern_index]; + // let (language_name, content_node, include_children) = + // injection_for_match(config, combined_injections_query, &mat, source); + // if language_name.is_some() { + // entry.0 = language_name; + // } + // if let Some(content_node) = content_node { + // entry.1.push(content_node); + // } + // entry.2 = include_children; + // } + // for (lang_name, content_nodes, includes_children) in injections_by_pattern_index { + // if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) { + // if let Some(next_config) = (injection_callback)(lang_name) { + // let ranges = + // Self::intersect_ranges(&ranges, &content_nodes, includes_children); + // if !ranges.is_empty() { + // queue.push((next_config, depth + 1, ranges)); + // } + // } + // } + // } + // } + self.tree = Some(tree) + } + Ok(()) + } + + pub(crate) fn generate_edits( + text: &RopeSlice, + changeset: &ChangeSet, + ) -> Vec { + use Operation::*; + let mut old_pos = 0; + let mut new_pos = 0; + + let mut edits = Vec::new(); + + let mut iter = changeset.changes.iter().peekable(); + + // TODO; this is a lot easier with Change instead of Operation. + + fn point_at_pos(text: &RopeSlice, pos: usize) -> (usize, Point) { + let byte = text.char_to_byte(pos); + let line = text.char_to_line(pos); + let line_start_byte = text.line_to_byte(line); + let col = byte - line_start_byte; + + (byte, Point::new(line, col)) + } + + fn traverse(point: Point, text: &Tendril) -> Point { + let Point { + mut row, + mut column, + } = point; + + // TODO: there should be a better way here + for ch in text.bytes() { + if ch == b'\n' { + row += 1; + column = 0; + } else { + column += 1; + } + } + Point { row, column } + } + + while let Some(change) = iter.next() { + let len = match change { + Delete(i) | Retain(i) => *i, + Insert(_) => 0, + }; + let old_end = old_pos + len; + + match change { + Retain(_) => { + new_pos += len; + } + Delete(_) => { + let (start_byte, start_position) = point_at_pos(&text, old_pos); + let (old_end_byte, old_end_position) = point_at_pos(&text, old_end); + + // TODO: Position also needs to be byte based... + // let byte = char_to_byte(old_pos) + // let line = char_to_line(old_pos) + // let line_start_byte = line_to_byte() + // Position::new(line, line_start_byte - byte) + + // a subsequent ins means a replace, consume it + if let Some(Insert(s)) = iter.peek() { + iter.next(); + let ins = s.chars().count(); + + // replacement + edits.push(tree_sitter::InputEdit { + start_byte, // old_pos to byte + old_end_byte, // old_end to byte + new_end_byte: start_byte + s.len(), // old_pos to byte + s.len() + start_position, // old pos to coords + old_end_position, // old_end to coords + new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over) + }); + + new_pos += ins; + } else { + // deletion + edits.push(tree_sitter::InputEdit { + start_byte, // old_pos to byte + old_end_byte, // old_end to byte + new_end_byte: start_byte, // old_pos to byte + start_position, // old pos to coords + old_end_position, // old_end to coords + new_end_position: start_position, // old pos to coords + }); + }; + } + Insert(s) => { + let (start_byte, start_position) = point_at_pos(&text, old_pos); + + let ins = s.chars().count(); + + // insert + edits.push(tree_sitter::InputEdit { + start_byte, // old_pos to byte + old_end_byte: start_byte, // same + new_end_byte: start_byte + s.len(), // old_pos + s.len() + start_position, // old pos to coords + old_end_position: start_position, // same + new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over) + }); + + new_pos += ins; + } + } + old_pos = old_end; + } + edits + } + + fn update( + &mut self, + parser: &mut Parser, + config: &HighlightConfiguration, + source: &Rope, + changeset: &ChangeSet, + ) -> Result<(), Error> { + if changeset.is_empty() { + return Ok(()); + } + + let edits = Self::generate_edits(&source.slice(..), changeset); + + // Notify the tree about all the changes + for edit in edits { + self.tree.as_mut().unwrap().edit(&edit); + } + + self.parse( + parser, + config, + source, + 0, + // TODO: what to do about this range on update + vec![Range { + start_byte: 0, + end_byte: usize::MAX, + start_point: Point::new(0, 0), + end_point: Point::new(usize::MAX, usize::MAX), + }], + ) + } + + // fn highlight_iter() -> same as Mode but for this layer. Mode composits these + // fn buffer_changed + // fn update(range) + // fn update_injections() +} // -- refactored from tree-sitter-highlight to be able to retain state // TODO: add seek() to iter @@ -169,7 +500,7 @@ where { source: &'a [u8], byte_offset: usize, - highlighter: &'a mut Highlighter, + highlighter: &'a mut Syntax, injection_callback: F, cancellation_flag: Option<&'a AtomicUsize>, layers: Vec>, @@ -179,7 +510,7 @@ where } struct HighlightIterLayer<'a> { - _tree: Tree, + _tree: Option, cursor: QueryCursor, captures: iter::Peekable>, config: &'a HighlightConfiguration, @@ -207,43 +538,43 @@ impl Highlighter { &mut self.parser } - /// Iterate over the highlighted regions for a given slice of source code. - pub fn highlight<'a>( - &'a mut self, - config: &'a HighlightConfiguration, - source: &'a [u8], - cancellation_flag: Option<&'a AtomicUsize>, - mut injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, - ) -> Result> + 'a, Error> { - let layers = HighlightIterLayer::new( - source, - self, - cancellation_flag, - &mut injection_callback, - config, - 0, - vec![Range { - start_byte: 0, - end_byte: usize::MAX, - start_point: Point::new(0, 0), - end_point: Point::new(usize::MAX, usize::MAX), - }], - )?; - assert_ne!(layers.len(), 0); - let mut result = HighlightIter { - source, - byte_offset: 0, - injection_callback, - cancellation_flag, - highlighter: self, - iter_count: 0, - layers, - next_event: None, - last_highlight_range: None, - }; - result.sort_layers(); - Ok(result) - } + // /// Iterate over the highlighted regions for a given slice of source code. + // pub fn highlight<'a>( + // &'a mut self, + // config: &'a HighlightConfiguration, + // source: &'a [u8], + // cancellation_flag: Option<&'a AtomicUsize>, + // mut injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, + // ) -> Result> + 'a, Error> { + // let layers = HighlightIterLayer::new( + // source, + // self, + // cancellation_flag, + // &mut injection_callback, + // config, + // 0, + // vec![Range { + // start_byte: 0, + // end_byte: usize::MAX, + // start_point: Point::new(0, 0), + // end_point: Point::new(usize::MAX, usize::MAX), + // }], + // )?; + // assert_ne!(layers.len(), 0); + // let mut result = HighlightIter { + // source, + // byte_offset: 0, + // injection_callback, + // cancellation_flag, + // highlighter: self, + // iter_count: 0, + // layers, + // next_event: None, + // last_highlight_range: None, + // }; + // result.sort_layers(); + // Ok(result) + // } } impl HighlightConfiguration { @@ -413,7 +744,7 @@ impl<'a> HighlightIterLayer<'a> { /// added to the returned vector. fn new Option<&'a HighlightConfiguration> + 'a>( source: &'a [u8], - highlighter: &mut Highlighter, + highlighter: &mut Syntax, cancellation_flag: Option<&'a AtomicUsize>, injection_callback: &mut F, mut config: &'a HighlightConfiguration, @@ -423,6 +754,8 @@ impl<'a> HighlightIterLayer<'a> { let mut result = Vec::with_capacity(1); let mut queue = Vec::new(); loop { + // --> Tree parsing part + if highlighter.parser.set_included_ranges(&ranges).is_ok() { highlighter .parser @@ -474,6 +807,8 @@ impl<'a> HighlightIterLayer<'a> { } } + // --> Highlighting query part + // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which // prevents them from being moved. But both of these values are really just // pointers, so it's actually ok to move them. @@ -495,7 +830,7 @@ impl<'a> HighlightIterLayer<'a> { }], cursor, depth, - _tree: tree, + _tree: Some(tree), captures, config, ranges, @@ -1016,3 +1351,106 @@ fn shrink_and_clear(vec: &mut Vec, capacity: usize) { } vec.clear(); } + +#[test] +fn test_parser() { + let highlight_names: Vec = [ + "attribute", + "constant", + "function.builtin", + "function", + "keyword", + "operator", + "property", + "punctuation", + "punctuation.bracket", + "punctuation.delimiter", + "string", + "string.special", + "tag", + "type", + "type.builtin", + "variable", + "variable.builtin", + "variable.parameter", + ] + .iter() + .cloned() + .map(String::from) + .collect(); + + let language = get_language(&LANG::Rust); + let mut config = HighlightConfiguration::new( + language, + &std::fs::read_to_string( + "../helix-syntax/languages/tree-sitter-rust/queries/highlights.scm", + ) + .unwrap(), + &std::fs::read_to_string( + "../helix-syntax/languages/tree-sitter-rust/queries/injections.scm", + ) + .unwrap(), + "", // locals.scm + ) + .unwrap(); + config.configure(&highlight_names); + + let source = Rope::from_str( + " + struct Stuff {} + fn main() {} + ", + ); + let syntax = Syntax::new(LANG::Rust, &source, config); + let tree = syntax.root_layer.tree.unwrap(); + let root = tree.root_node(); + assert_eq!(root.kind(), "source_file"); + + assert_eq!( + root.to_sexp(), + concat!( + "(source_file ", + "(struct_item name: (type_identifier) body: (field_declaration_list)) ", + "(function_item name: (identifier) parameters: (parameters) body: (block)))" + ) + ); + + let struct_node = root.child(0).unwrap(); + assert_eq!(struct_node.kind(), "struct_item"); +} + +#[test] +fn test_input_edits() { + use crate::State; + use tree_sitter::InputEdit; + + let mut state = State::new("hello world!\ntest 123".into()); + let transaction = Transaction::change( + &state, + vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(), + ); + let edits = LanguageLayer::generate_edits(&state.doc.slice(..), &transaction.changes); + // transaction.apply(&mut state); + + assert_eq!( + edits, + &[ + InputEdit { + start_byte: 6, + old_end_byte: 11, + new_end_byte: 10, + start_position: Point { row: 0, column: 6 }, + old_end_position: Point { row: 0, column: 11 }, + new_end_position: Point { row: 0, column: 10 } + }, + InputEdit { + start_byte: 12, + old_end_byte: 17, + new_end_byte: 12, + start_position: Point { row: 0, column: 12 }, + old_end_position: Point { row: 1, column: 4 }, + new_end_position: Point { row: 0, column: 12 } + } + ] + ); +} diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index b4cc83c6a..ab32d5daa 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -4,7 +4,7 @@ use crate::{Rope, Selection, SelectionRange, State, Tendril}; pub type Change = (usize, usize, Option); #[derive(Debug, Clone, PartialEq, Eq)] -enum Operation { +pub(crate) enum Operation { /// Move cursor by n characters. Retain(usize), /// Delete n characters. @@ -22,7 +22,7 @@ pub enum Assoc { // ChangeSpec = Change | ChangeSet | Vec #[derive(Debug)] pub struct ChangeSet { - changes: Vec, + pub(crate) changes: Vec, /// The required document length. Will refuse to apply changes unless it matches. len: usize, } @@ -307,7 +307,7 @@ impl ChangeSet { /// a single transaction. pub struct Transaction { /// Changes made to the buffer. - changes: ChangeSet, + pub(crate) changes: ChangeSet, /// When set, explicitly updates the selection. selection: Option, // effects, annotations @@ -337,6 +337,14 @@ impl Transaction { .clone() .unwrap_or_else(|| state.selection.clone().map(&self.changes)); + // TODO: no unwrap + state + .syntax + .as_mut() + .unwrap() + .update(&state.doc, &self.changes) + .unwrap(); + true } @@ -359,9 +367,15 @@ impl Transaction { for (from, to, tendril) in changes { // Retain from last "to" to current "from" acc.push(Operation::Retain(from - last)); + let span = to - from; match tendril { - Some(text) => acc.push(Operation::Insert(text)), - None => acc.push(Operation::Delete(to - from)), + Some(text) => { + if span > 0 { + acc.push(Operation::Delete(span)); + } + acc.push(Operation::Insert(text)); + } + None => acc.push(Operation::Delete(span)), } last = to; } @@ -466,4 +480,15 @@ mod test { assert_eq!(cs.map_pos(2, Assoc::Before), 2); assert_eq!(cs.map_pos(2, Assoc::After), 2); } + + #[test] + fn transaction_change() { + let mut state = State::new("hello world!\ntest 123".into()); + let transaction = Transaction::change( + &state, + vec![(6, 11, Some("void".into())), (12, 17, None)].into_iter(), + ); + transaction.apply(&mut state); + assert_eq!(state.doc, Rope::from_str("hello void! 123")); + } } diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index b7a58e3e0..e0b96ead5 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -11,11 +11,11 @@ name = "hx" path = "src/main.rs" [dependencies] +helix-core = { path = "../helix-core" } helix-syntax = { path = "../helix-syntax" } anyhow = "1" argh = "0.1.3" -helix-core = { path = "../helix-core" } crossterm = { version = "0.17", features = ["event-stream"] } smol = "1" diff --git a/helix-term/src/editor.rs b/helix-term/src/editor.rs index 080c05aac..b3788260b 100644 --- a/helix-term/src/editor.rs +++ b/helix-term/src/editor.rs @@ -38,8 +38,6 @@ pub struct Editor { size: (u16, u16), surface: Surface, theme: Theme, - highlighter: Highlighter, - highlight_config: HighlightConfiguration, } impl Editor { @@ -49,33 +47,8 @@ impl Editor { let mut terminal = Terminal::new(backend)?; let size = terminal::size().unwrap(); let area = Rect::new(0, 0, size.0, size.1); - let theme = Theme::default(); - // let mut parser = tree_sitter::Parser::new(); - // parser.set_language(language).unwrap(); - // let tree = parser.parse(source_code, None).unwrap(); - - let language = helix_syntax::get_language(&helix_syntax::LANG::Rust); - - let highlighter = Highlighter::new(); - - let mut highlight_config = HighlightConfiguration::new( - language, - &std::fs::read_to_string( - "../helix-syntax/languages/tree-sitter-rust/queries/highlights.scm", - ) - .unwrap(), - &std::fs::read_to_string( - "../helix-syntax/languages/tree-sitter-rust/queries/injections.scm", - ) - .unwrap(), - "", // locals.scm - ) - .unwrap(); - - highlight_config.configure(theme.scopes()); - let mut editor = Editor { terminal, state: None, @@ -84,8 +57,6 @@ impl Editor { surface: Surface::empty(area), theme, // TODO; move to state - highlighter, - highlight_config, }; if let Some(file) = args.files.pop() { @@ -96,12 +67,19 @@ impl Editor { } pub fn open(&mut self, path: PathBuf) -> Result<(), Error> { - self.state = Some(State::load(path)?); + let mut state = State::load(path)?; + state + .syntax + .as_mut() + .unwrap() + .configure(self.theme.scopes()); + self.state = Some(state); Ok(()) } fn render(&mut self) { - match &self.state { + // TODO: ideally not mut but highlights require it because of cursor cache + match &mut self.state { Some(state) => { let area = Rect::new(0, 0, self.size.0, self.size.1); let mut surface = Surface::empty(area); @@ -112,12 +90,13 @@ impl Editor { // TODO: cache highlight results // TODO: only recalculate when state.doc is actually modified - let highlights = self - .highlighter - .highlight(&self.highlight_config, source_code.as_bytes(), None, |_| { - None - }) - .unwrap(); + let highlights: Vec<_> = state + .syntax + .as_mut() + .unwrap() + .highlight_iter(source_code.as_bytes(), None, |_| None) + .unwrap() + .collect(); // TODO: we collect here to avoid double borrow, fix later let mut spans = Vec::new(); @@ -235,7 +214,7 @@ impl Editor { let coords = coords_at_pos(&state.doc().slice(..), pos); execute!( stdout, - cursor::MoveTo((coords.1 + 2) as u16, coords.0 as u16) + cursor::MoveTo((coords.col + 2) as u16, coords.row as u16) ); } None => (),