From b647c7a773fb3323ccc884f4d0d4ce25c3e8aea8 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 10 Sep 2020 13:55:18 +0900 Subject: tree-sitter based syntax highlighting draft --- helix-core/src/lib.rs | 2 +- helix-term/src/editor.rs | 290 +++++++++++++++++++++++++++++++++++++---------- helix-term/src/keymap.rs | 17 --- helix-term/src/macros.rs | 16 +++ helix-term/src/main.rs | 4 +- helix-term/test.rs | 36 ++++++ 6 files changed, 288 insertions(+), 77 deletions(-) create mode 100644 helix-term/src/macros.rs create mode 100644 helix-term/test.rs diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index c7c020d6..da00b9ae 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,6 +1,6 @@ #![allow(unused)] pub mod commands; -mod graphemes; +pub mod graphemes; mod selection; pub mod state; mod transaction; diff --git a/helix-term/src/editor.rs b/helix-term/src/editor.rs index 1b58c46d..f7abc806 100644 --- a/helix-term/src/editor.rs +++ b/helix-term/src/editor.rs @@ -11,6 +11,7 @@ use crossterm::{ use helix_core::{state::coords_at_pos, state::Mode, State}; use smol::prelude::*; +use std::collections::HashMap; use std::io::{self, stdout, Write}; use std::path::PathBuf; use std::time::Duration; @@ -29,6 +30,8 @@ pub struct Editor { state: Option, first_line: u16, size: (u16, u16), + surface: Surface, + theme: HashMap<&'static str, Style>, } impl Editor { @@ -36,12 +39,54 @@ impl Editor { let backend = CrosstermBackend::new(stdout()); let mut terminal = Terminal::new(backend)?; + let size = terminal::size().unwrap(); + let area = Rect::new(0, 0, size.0, size.1); + + use tui::style::Color; + let theme = hashmap! { + "attribute" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac + "keyword" => Style::default().fg(Color::Rgb(236, 205, 186)), // almond + "punctuation" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender + "punctuation.delimiter" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender + "operator" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac + "property" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender + "variable.parameter" => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender + // TODO distinguish type from type.builtin? + "type" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "type.builtin" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "constructor" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac + "function" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "function.macro" => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac + "comment" => Style::default().fg(Color::Rgb(105, 124, 129)), // sirocco + "variable.builtin" => Style::default().fg(Color::Rgb(159, 242, 143)), // mint + "constant" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "constant.builtin" => Style::default().fg(Color::Rgb(255, 255, 255)), // white + "string" => Style::default().fg(Color::Rgb(204, 204, 204)), // silver + "escape" => Style::default().fg(Color::Rgb(239, 186, 93)), // honey + // used for lifetimes + "label" => Style::default().fg(Color::Rgb(239, 186, 93)), // honey + + // TODO: diferentiate number builtin + // TODO: diferentiate doc comment + // TODO: variable as lilac + // TODO: mod/use statements as white + // TODO: mod stuff as chamoise + // TODO: add "(scoped_identifier) @path" for std::mem:: + // + // concat (ERROR) @syntax-error and "MISSING ;" selectors for errors + + "module" => Style::default().fg(Color::Rgb(255, 0, 0)), // white + "variable" => Style::default().fg(Color::Rgb(255, 0, 0)), // white + "function.builtin" => Style::default().fg(Color::Rgb(255, 0, 0)), // white + }; let mut editor = Editor { terminal, state: None, first_line: 0, - size: terminal::size().unwrap(), + size, + surface: Surface::empty(area), + theme, }; if let Some(file) = args.files.pop() { @@ -61,69 +106,200 @@ impl Editor { Some(state) => { let area = Rect::new(0, 0, self.size.0, self.size.1); let mut surface = Surface::empty(area); - - let lines = state - .doc - .lines_at(self.first_line as usize) - .take(self.size.1 as usize) - .map(|x| x.as_str().unwrap()); - let mut stdout = stdout(); - for (n, line) in lines.enumerate() { - execute!( - stdout, - SetForegroundColor(Color::DarkCyan), - cursor::MoveTo(0, n as u16), - Print((n + 1).to_string()) - ); - - surface.set_string(2, n as u16, line, Style::default()); - // execute!( - // stdout, - // SetForegroundColor(Color::Reset), - // cursor::MoveTo(2, n as u16), - // Print(line) - // ); - } - - // iterate over selections and render them - let select = Style::default().bg(tui::style::Color::LightBlue); - let text = state.doc.slice(..); - for range in state.selection.ranges() { - // get terminal coords for x,y for each range pos - // TODO: this won't work with multiline - let (y1, x1) = coords_at_pos(&text, range.from()); - let (y2, x2) = coords_at_pos(&text, range.to()); - let area = Rect::new( - (x1 + 2) as u16, - y1 as u16, - (x2 - x1 + 1) as u16, - (y2 - y1 + 1) as u16, - ); - surface.set_style(area, select); - - // TODO: don't highlight next char in append mode + // + use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter}; + + let highlight_names: Vec = [ + "attribute", + "constant.builtin", + "constant", + "function.builtin", + "function.macro", + "function", + "keyword", + "operator", + "property", + "punctuation", + "comment", + "escape", + "label", + // "punctuation.bracket", + "punctuation.delimiter", + "string", + "string.special", + "tag", + "type", + "type.builtin", + "constructor", + "variable", + "variable.builtin", + "variable.parameter", + "path", + ] + .iter() + .cloned() + .map(String::from) + .collect(); + + let language = helix_syntax::get_language(&helix_syntax::LANG::Rust); + // let mut parser = tree_sitter::Parser::new(); + // parser.set_language(language).unwrap(); + // let tree = parser.parse(source_code, None).unwrap(); + + let mut highlighter = Highlighter::new(); + + 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); + + // TODO: inefficient, should feed chunks.iter() to tree_sitter.parse_with(|offset, + // pos|) + let source_code = state.doc.to_string(); + + // TODO: cache highlight results + // TODO: only recalculate when state.doc is actually modified + let highlights = highlighter + .highlight(&config, source_code.as_bytes(), None, |_| None) + .unwrap(); + + let mut spans = Vec::new(); + + let offset = 2; + + let mut visual_x = 0; + let mut line = 0; + + for event in highlights { + match event.unwrap() { + HighlightEvent::HighlightStart(span) => { + // eprintln!("highlight style started: {:?}", highlight_names[span.0]); + spans.push(span); + } + HighlightEvent::HighlightEnd => { + spans.pop(); + // eprintln!("highlight style ended"); + } + HighlightEvent::Source { start, end } => { + // TODO: filter out spans out of viewport for now.. + + let start = state.doc.byte_to_char(start); + let end = state.doc.byte_to_char(end); + + let text = state.doc.slice(start..end); + + use helix_core::graphemes::{grapheme_width, RopeGraphemes}; + + use tui::style::Color; + let style = match spans.first() { + Some(span) => self + .theme + .get(highlight_names[span.0].as_str()) + .map(|style| *style) + .unwrap_or(Style::default().fg(Color::Rgb(0, 0, 255))), + + None => Style::default().fg(Color::Rgb(164, 160, 232)), // lavender + // None => Style::default().fg(Color::Rgb(219, 191, 239)), // lilac + }; + + // iterate over range char by char + for grapheme in RopeGraphemes::new(&text) { + // TODO: track current char_index + + if grapheme == "\n" { + visual_x = 0; + line += 1; + } else { + // Cow will prevent allocations if span contained in a single slice + // which should really be the majority case + let grapheme = std::borrow::Cow::from(grapheme); + let width = grapheme_width(&grapheme) as u16; + surface.set_string(offset + visual_x, line, grapheme, style); + + visual_x += width; + } + // if grapheme == "\t" + } + } + } } - let mode = match state.mode { - Mode::Insert => "INS", - Mode::Normal => "NOR", - }; - - execute!( - stdout, - SetForegroundColor(Color::Reset), - cursor::MoveTo(0, self.size.1), - Print(mode) - ); + // + + // let lines = state + // .doc + // .lines_at(self.first_line as usize) + // .take(self.size.1 as usize) + // .map(|x| x.as_str().unwrap()); + + // for (n, line) in lines.enumerate() { + // execute!( + // stdout, + // SetForegroundColor(Color::DarkCyan), + // cursor::MoveTo(0, n as u16), + // Print((n + 1).to_string()) + // ); + + // surface.set_string(2, n as u16, line, Style::default()); + // // execute!( + // // stdout, + // // SetForegroundColor(Color::Reset), + // // cursor::MoveTo(2, n as u16), + // // Print(line) + // // ); + // } + + // // iterate over selections and render them + // let select = Style::default().bg(tui::style::Color::LightBlue); + // let text = state.doc.slice(..); + // for range in state.selection.ranges() { + // // get terminal coords for x,y for each range pos + // // TODO: this won't work with multiline + // let (y1, x1) = coords_at_pos(&text, range.from()); + // let (y2, x2) = coords_at_pos(&text, range.to()); + // let area = Rect::new( + // (x1 + 2) as u16, + // y1 as u16, + // (x2 - x1 + 1) as u16, + // (y2 - y1 + 1) as u16, + // ); + // surface.set_style(area, select); + + // // TODO: don't highlight next char in append mode + // } + + // let mode = match state.mode { + // Mode::Insert => "INS", + // Mode::Normal => "NOR", + // }; + + // execute!( + // stdout, + // SetForegroundColor(Color::Reset), + // cursor::MoveTo(0, self.size.1), + // Print(mode) + // ); use tui::backend::Backend; - // TODO: double buffer and diff here - let empty = Surface::empty(area); + // // TODO: double buffer and diff here self.terminal .backend_mut() - .draw(empty.diff(&surface).into_iter()); + .draw(self.surface.diff(&surface).into_iter()); + // swap the buffer + self.surface = surface; // set cursor shape match state.mode { @@ -260,7 +436,7 @@ impl Editor { fn test_parser() { use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter}; - let source_code = include_str!("./main.rs"); + let source_code = include_str!("../test.rs"); let highlight_names: Vec = [ "attribute", diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 4b176b61..148611e3 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -79,23 +79,6 @@ use std::collections::HashMap; // } // } -macro_rules! hashmap { - (@single $($x:tt)*) => (()); - (@count $($rest:expr),*) => (<[()]>::len(&[$(hashmap!(@single $rest)),*])); - - ($($key:expr => $value:expr,)+) => { hashmap!($($key => $value),+) }; - ($($key:expr => $value:expr),*) => { - { - let _cap = hashmap!(@count $($key),*); - let mut _map = ::std::collections::HashMap::with_capacity(_cap); - $( - let _ = _map.insert($key, $value); - )* - _map - } - }; -} - type Keymap = HashMap; pub fn default() -> Keymap { diff --git a/helix-term/src/macros.rs b/helix-term/src/macros.rs new file mode 100644 index 00000000..3b22a786 --- /dev/null +++ b/helix-term/src/macros.rs @@ -0,0 +1,16 @@ +macro_rules! hashmap { + (@single $($x:tt)*) => (()); + (@count $($rest:expr),*) => (<[()]>::len(&[$(hashmap!(@single $rest)),*])); + + ($($key:expr => $value:expr,)+) => { hashmap!($($key => $value),+) }; + ($($key:expr => $value:expr),*) => { + { + let _cap = hashmap!(@count $($key),*); + let mut _map = ::std::collections::HashMap::with_capacity(_cap); + $( + let _ = _map.insert($key, $value); + )* + _map + } + }; +} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 7a9289ee..e3c16c14 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,6 +1,6 @@ #![allow(unused)] -// mod editor; -// mod component; +#[macro_use] +mod macros; mod editor; mod keymap; diff --git a/helix-term/test.rs b/helix-term/test.rs new file mode 100644 index 00000000..713c23d0 --- /dev/null +++ b/helix-term/test.rs @@ -0,0 +1,36 @@ +pub struct TextArea { + properties: Properties, + frame: Rect, +} + +impl Component for TextArea { + type Message = (); + type Properties = Properties; + + fn create(properties: Self::Properties, frame: Rect, _link: ComponentLink) -> Self { + TextArea { properties, frame } + } + + fn change<'a>(&'a mut self, properties: Self::Properties) -> ShouldRender { + let a: &'static str = "ase"; + let q = 2u8; + let q = 2 as u16; + Some(0); + true; + self.properties = properties; + ShouldRender::Yes + } + + fn resize(&mut self, frame: Rect) -> ShouldRender { + println!("hello world! \" test"); + self.frame = frame; + ShouldRender::Yes + } + + fn view(&self) -> Layout { + let mut canvas = Canvas::new(self.frame.size); + canvas.clear(self.properties.theme.text); + self.draw_text(&mut canvas); + canvas.into() + } +} -- cgit v1.2.3-70-g09d2