From 9722d01b078dbf554431a3af3767012fce341b95 Mon Sep 17 00:00:00 2001 From: Nathan Reiner Date: Thu, 25 Jul 2024 16:39:49 +0200 Subject: editor add syntax highlighting using treesitter --- Cargo.lock | 98 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/app.rs | 1 - src/widgets/luaeditor/buffer.rs | 26 ++++++---- src/widgets/luaeditor/mod.rs | 62 ++++++++++++++++------- src/widgets/luaeditor/theme.rs | 43 ++++++++++++++++ src/widgets/luaeditor/treesitter.rs | 75 ++++++++++++++++++++++++++++ src/widgets/sheetview.rs | 1 - 8 files changed, 279 insertions(+), 29 deletions(-) create mode 100644 src/widgets/luaeditor/theme.rs create mode 100644 src/widgets/luaeditor/treesitter.rs diff --git a/Cargo.lock b/Cargo.lock index a7d12ee..20ecfaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -144,6 +153,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.155" @@ -223,6 +238,8 @@ version = "0.1.0" dependencies = [ "mlua", "ratatui", + "tree-sitter-highlight", + "tree-sitter-lua", ] [[package]] @@ -323,6 +340,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rustc-hash" version = "2.0.0" @@ -452,6 +498,58 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tree-sitter" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-highlight" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaca0fe34fa96eec6aaa8e63308dbe1bafe65a6317487c287f93938959b21907" +dependencies = [ + "lazy_static", + "regex", + "thiserror", + "tree-sitter", +] + +[[package]] +name = "tree-sitter-lua" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9fe6fc87bd480e1943fc1fcb02453fb2da050e4e8ce0daa67d801544046856" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index bd83f5b..b4f83df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" [dependencies] mlua = { version = "0.9.9", features = ["luajit"] } ratatui = "0.27.0" +tree-sitter-highlight = "0.22.6" +tree-sitter-lua = "0.1.0" diff --git a/src/app.rs b/src/app.rs index 3544fdf..71e6a02 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,6 @@ use ratatui::{ widgets::Block, }; -#[derive(Debug)] pub struct App<'a> { exit: bool, view: SheetView<'a>, diff --git a/src/widgets/luaeditor/buffer.rs b/src/widgets/luaeditor/buffer.rs index 6a954ed..905950e 100644 --- a/src/widgets/luaeditor/buffer.rs +++ b/src/widgets/luaeditor/buffer.rs @@ -32,16 +32,22 @@ impl Buffer { fn end_balance(&self, level: usize) -> isize { let str = "\t".repeat(level); - let start_count = self.lines.iter().filter(|s| { - s.starts_with(&(str.clone() + "if ")) - || s.starts_with(&(str.clone() + "for ")) - || s.starts_with(&(str.clone() + "while ")) - || (s.starts_with(&str) && s.contains("function(") && s.ends_with(")")) - }).count(); - - let end_count = self.lines.iter().filter( - |s| s.starts_with(&(str.clone() + "end ")) || **s == (str.clone() + "end") - ).count(); + let start_count = self + .lines + .iter() + .filter(|s| { + s.starts_with(&(str.clone() + "if ")) + || s.starts_with(&(str.clone() + "for ")) + || s.starts_with(&(str.clone() + "while ")) + || (s.starts_with(&str) && s.contains("function(") && s.ends_with(")")) + }) + .count(); + + let end_count = self + .lines + .iter() + .filter(|s| s.starts_with(&(str.clone() + "end ")) || **s == (str.clone() + "end")) + .count(); (end_count as isize) - (start_count as isize) } diff --git a/src/widgets/luaeditor/mod.rs b/src/widgets/luaeditor/mod.rs index 723d8b5..9a73e6e 100644 --- a/src/widgets/luaeditor/mod.rs +++ b/src/widgets/luaeditor/mod.rs @@ -10,16 +10,19 @@ use ratatui::{ pub mod buffer; pub mod cursor; +pub mod theme; +pub mod treesitter; use buffer::Buffer; +use tree_sitter_highlight::HighlightConfiguration; use self::cursor::CursorMove; -#[derive(Debug)] pub struct LuaEditor<'a> { block: Option>, scroll: usize, buffer: Buffer, + highlight_config: HighlightConfiguration, } impl<'a> LuaEditor<'a> { @@ -31,6 +34,7 @@ impl<'a> LuaEditor<'a> { block: None, scroll: 0, buffer: Buffer::from_str(content.as_ref()).unwrap(), + highlight_config: treesitter::new_highlight_configuration(), } } @@ -88,18 +92,40 @@ impl Widget for &mut LuaEditor<'_> { self.block.render(area, buf); let inner_area = self.block.inner_if_some(area); - for (i, line) in self.buffer.lines().iter().enumerate().skip(self.scroll) { - let replace = &line.replace('\t', " "); - let span = replace.to_span(); - let mut span_area = inner_area.clone(); - span_area.height = 1; - span_area.y += i as u16; + let text = self.text(); + let highlights = treesitter::highlighter_split(text.as_bytes(), &self.highlight_config); + + let mut span_area = inner_area.clone(); + + for (hl, group) in highlights.iter().skip(self.scroll) { + let lines: Vec<_> = group.lines().collect(); + + for (i, line) in lines.iter().enumerate() { + let line = line.replace("\t", " "); + let span = line.to_span(); + + if !inner_area.contains(span_area.into()) { + break; + } + + theme::theme_highlight_group(*hl, span).render(span_area, buf); + + if i < lines.len() - 1 { + span_area.y += 1; + span_area.x = inner_area.x; + } else { + span_area.x += line.len() as u16; + } + } if !inner_area.contains(span_area.into()) { break; } - span.render(span_area, buf) + if group.ends_with("\n") { + span_area.y += 1; + span_area.x = inner_area.x; + } } let mut cursor_area = inner_area.clone(); @@ -119,14 +145,16 @@ impl Widget for &mut LuaEditor<'_> { } } - self.buffer - .current_line() - .chars() - .nth(self.buffer.cursor().x() as usize) - .map(|c| if c == '\t' { ' ' } else { c }) - .unwrap_or(' ') - .to_span() - .reversed() - .render(cursor_area, buf); + if inner_area.contains(cursor_area.into()) { + self.buffer + .current_line() + .chars() + .nth(self.buffer.cursor().x() as usize) + .map(|c| if c == '\t' { ' ' } else { c }) + .unwrap_or(' ') + .to_span() + .reversed() + .render(cursor_area, buf); + } } } diff --git a/src/widgets/luaeditor/theme.rs b/src/widgets/luaeditor/theme.rs new file mode 100644 index 0000000..2af8534 --- /dev/null +++ b/src/widgets/luaeditor/theme.rs @@ -0,0 +1,43 @@ +use ratatui::{style::{Color, Stylize}, text::Span}; +use tree_sitter_highlight::Highlight; + + +static HIGHLIGHT_THEME: [Color; 29] = [ + Color::White, // attribute + Color::White, // boolean + Color::DarkGray, // comment + Color::LightYellow, // conditional + Color::White, // constant + Color::White, // constant.builtin + Color::White, // constructor + Color::White, // field + Color::White, // function + Color::White, // function.builtin + Color::White, // function.call + Color::Yellow, // keyword + Color::LightYellow, // keyword.function + Color::White, // keyword.operator + Color::LightYellow, // keyword.return + Color::White, // label + Color::White, // method + Color::White, // method.call + Color::Blue, // number + Color::Gray, // operator + Color::White, // parameter + Color::White, // preproc + Color::Gray, // punctuation.bracket + Color::Gray, // punctuation.delimiter + Color::LightYellow, // repeat + Color::LightGreen, // string + Color::Yellow, // string.escape + Color::Gray, // variable + Color::White, // variable.builtin +]; + +pub fn theme_highlight_group(hl: Option, span: Span) -> Span { + if let Some(hl) = hl { + span.fg(HIGHLIGHT_THEME[hl.0]) + } else { + span + } +} diff --git a/src/widgets/luaeditor/treesitter.rs b/src/widgets/luaeditor/treesitter.rs new file mode 100644 index 0000000..57013f9 --- /dev/null +++ b/src/widgets/luaeditor/treesitter.rs @@ -0,0 +1,75 @@ +use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter}; + +static HIGHLIGHT_NAMES: [&'static str; 29] = [ + "attribute", + "boolean", + "comment", + "conditional", + "constant", + "constant.builtin", + "constructor", + "field", + "function", + "function.builtin", + "function.call", + "keyword", + "keyword.function", + "keyword.operator", + "keyword.return", + "label", + "method", + "method.call", + "number", + "operator", + "parameter", + "preproc", + "punctuation.bracket", + "punctuation.delimiter", + "repeat", + "string", + "string.escape", + "variable", + "variable.builtin", +]; + +pub fn new_highlight_configuration() -> HighlightConfiguration { + let mut highlight_config = HighlightConfiguration::new( + tree_sitter_lua::language(), + "lua", + tree_sitter_lua::HIGHLIGHTS_QUERY, + tree_sitter_lua::INJECTIONS_QUERY, + tree_sitter_lua::LOCALS_QUERY, + ) + .unwrap(); + + highlight_config.configure(&HIGHLIGHT_NAMES); + + highlight_config +} + +pub fn highlight_name(h: Highlight) -> &'static str { + HIGHLIGHT_NAMES[h.0] +} + +pub fn highlighter_split<'a>( + s: &'a [u8], + config: &'a HighlightConfiguration, +) -> Vec<(Option, &'a str)> { + let mut splits = Vec::new(); + let mut current = None; + + let mut highlighter = Highlighter::new(); + let highlights = highlighter.highlight(config, s, None, |_| None).unwrap(); + + for event in highlights { + match event.unwrap() { + HighlightEvent::Source { start, end } => { + splits.push((current, std::str::from_utf8(&s[start..end]).unwrap())); + } + HighlightEvent::HighlightStart(s) => current = Some(s), + HighlightEvent::HighlightEnd => current = None, + } + } + + splits +} diff --git a/src/widgets/sheetview.rs b/src/widgets/sheetview.rs index 2584c96..c4f7a73 100644 --- a/src/widgets/sheetview.rs +++ b/src/widgets/sheetview.rs @@ -18,7 +18,6 @@ use super::luaeditor::LuaEditor; const DEFAULT_COLUMN_WIDTH: u16 = 10; -#[derive(Debug)] pub struct SheetView<'a> { block: Option>, luaeditor: Option>, -- cgit v1.2.3-70-g09d2