From be6653be6f688538ed8d7cfe33e3390e56309d8e Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Sun, 6 Feb 2022 20:30:45 +0100 Subject: [PATCH] init --- .gitignore | 2 + .gitmodules | 6 + Cargo.toml | 19 +++ assets/style.css | 7 + build.rs | 14 ++ rustfmt.toml | 12 ++ src/hl.rs | 99 ++++++++++++++ src/lua.rs | 34 +++++ src/main.rs | 27 ++++ src/ui/entry.rs | 116 +++++++++++++++++ src/ui/mod.rs | 283 +++++++++++++++++++++++++++++++++++++++++ ts/json_highlights.scm | 14 ++ ts/lua_highlights.scm | 194 ++++++++++++++++++++++++++++ ts/tree-sitter-json | 1 + ts/tree-sitter-lua | 1 + 15 files changed, 829 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Cargo.toml create mode 100644 assets/style.css create mode 100644 build.rs create mode 100644 rustfmt.toml create mode 100644 src/hl.rs create mode 100644 src/lua.rs create mode 100644 src/main.rs create mode 100644 src/ui/entry.rs create mode 100644 src/ui/mod.rs create mode 100644 ts/json_highlights.scm create mode 100644 ts/lua_highlights.scm create mode 160000 ts/tree-sitter-json create mode 160000 ts/tree-sitter-lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..24ed6c6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "ts/tree-sitter-json"] + path = ts/tree-sitter-json + url = https://github.com/tree-sitter/tree-sitter-json +[submodule "ts/tree-sitter-lua"] + path = ts/tree-sitter-lua + url = https://github.com/MunifTanjim/tree-sitter-lua diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..849ff57 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "luna" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.53" +mlua = { version = "0.7.3", features = ["luajit", "serialize", "send"] } +relm4 = { version = "0.4.2", features = ["macros"] } +serde_json = "1.0.78" +tree-sitter = "0.20.4" +tree-sitter-highlight = "0.20.1" + +[features] + +[build-dependencies] +cc = "1.0.72" diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..0f9a8be --- /dev/null +++ b/assets/style.css @@ -0,0 +1,7 @@ +textview text { + background-color: #2a2c39; +} + +textview { + color: #f8f8f2; +} diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..029c5d8 --- /dev/null +++ b/build.rs @@ -0,0 +1,14 @@ +fn main() { + cc::Build::new() + .include("ts/tree-sitter-json") + .file("ts/tree-sitter-json/src/parser.c") + .compile("tree-sitter-json"); + + cc::Build::new() + .include("ts/tree-sitter-lua") + .files([ + "ts/tree-sitter-lua/src/parser.c", + "ts/tree-sitter-lua/src/scanner.c", + ]) + .compile("tree-sitter-lua"); +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..1059111 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,12 @@ +unstable_features = true +binop_separator = "Back" +format_code_in_doc_comments = true +format_macro_matchers = true +format_strings = true +imports_layout = "HorizontalVertical" +match_block_trailing_comma = true +merge_imports = true +normalize_comments = true +use_field_init_shorthand = true +use_try_shorthand = true +wrap_comments = true diff --git a/src/hl.rs b/src/hl.rs new file mode 100644 index 0000000..cdfea91 --- /dev/null +++ b/src/hl.rs @@ -0,0 +1,99 @@ +use relm4::gtk::{prelude::*, TextBuffer}; +use tree_sitter::Language; +use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent}; + +const HIGHLIGHT_NAMES: &[&str] = &[ + "attribute", + "comment", + "conditional", + "constant", + "function", + "function.builtin", + "keyword", + "label", + "number", + "operator", + "punctuation", + "repeat", + "string", + "type", + "variable", +]; + +pub const JSON_HL_QUERY: &str = include_str!("../ts/json_highlights.scm"); +pub const LUA_HL_QUERY: &str = include_str!("../ts/lua_highlights.scm"); + +pub fn highlight_text_buffer(buf: &TextBuffer, lang: Language, highlight_query: &str) { + buf.remove_all_tags(&buf.start_iter(), &buf.end_iter()); + + let mut conf = HighlightConfiguration::new(lang, highlight_query, "", "").unwrap(); + conf.configure(HIGHLIGHT_NAMES); + + let table = buf.tag_table(); + let tag = |name: &str| table.lookup(name).expect("missing text tag!"); + + let txt = buf.text(&buf.start_iter(), &buf.end_iter(), false); + crate::HIGHLIGHTER.with(|hl| { + let mut hl = hl.borrow_mut(); + let hls = hl.highlight(&conf, txt.as_bytes(), None, |_| None); + + if let Ok(hls) = hls.and_then(|hls| hls.collect::, _>>()) { + let mut acc = HighlightAcc::default(); + + for hl in hls { + acc.push(hl); + } + + for Span { + start, + end, + hl: Highlight(hl), + } in acc.spans + { + buf.apply_tag( + &tag(HIGHLIGHT_NAMES[hl]), + &buf.iter_at_offset(start as i32), + &buf.iter_at_offset(end as i32), + ); + } + } else { + buf.apply_tag(&tag("error"), &buf.start_iter(), &buf.end_iter()); + } + }); +} + +#[derive(Debug, Default)] +struct HighlightAcc { + prev_pos: usize, + pos: usize, + cur_hl: Option, + pub spans: Vec, +} + +impl HighlightAcc { + pub fn push(&mut self, ev: HighlightEvent) { + match ev { + HighlightEvent::Source { end, .. } => { + self.prev_pos = self.pos; + self.pos = end; + }, + HighlightEvent::HighlightStart(hl) => self.cur_hl = Some(hl), + HighlightEvent::HighlightEnd => { + if let Some(hl) = self.cur_hl.take() { + self.spans.push(Span { + start: self.prev_pos, + end: self.pos, + hl, + }); + } + }, + } + } +} + +#[derive(Debug)] +struct Span { + start: usize, + end: usize, + hl: Highlight, +} diff --git a/src/lua.rs b/src/lua.rs new file mode 100644 index 0000000..f78741c --- /dev/null +++ b/src/lua.rs @@ -0,0 +1,34 @@ +use std::{cell::RefCell, rc::Rc}; + +use mlua::{DeserializeOptions, Function, Lua, LuaSerdeExt, Value}; + +pub fn try_eval(lua: &Lua, src: &str) -> mlua::Result<(String, String)> { + lua.scope(|s| { + let output = Rc::new(RefCell::new(String::new())); + let output_ = Rc::clone(&output); + let print = s.create_function(move |lua, x: Value| { + let s = lua + .globals() + .get::<_, Function>("tostring")? + .call::<_, String>(x)?; + + let mut output = output_.borrow_mut(); + output.push_str(&s); + output.push('\n'); + + Ok(()) + })?; + + lua.globals().set("print", print)?; + + let res = lua.from_value_with::( + lua.load(src).eval()?, + DeserializeOptions::new() + .deny_unsupported_types(false) + .deny_recursive_tables(false), + )?; + let res = serde_json::to_string_pretty(&res).unwrap(); + + Ok((output.take(), res)) + }) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b693b25 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,27 @@ +use relm4::{gtk, RelmApp}; +use std::cell::RefCell; +use tree_sitter_highlight::Highlighter; +use tree_sitter::Language; + +mod hl; +mod lua; +mod ui; + +thread_local! { + pub static HIGHLIGHTER: RefCell = RefCell::new(Highlighter::new()); +} + +fn main() -> anyhow::Result<()> { + gtk::init()?; + relm4::set_global_css(include_bytes!("../assets/style.css")); + let model = ui::AppModel::new()?; + let app = RelmApp::new(model); + app.run(); + + Ok(()) +} + +extern "C" { + fn tree_sitter_json() -> Language; + fn tree_sitter_lua() -> Language; +} diff --git a/src/ui/entry.rs b/src/ui/entry.rs new file mode 100644 index 0000000..32030da --- /dev/null +++ b/src/ui/entry.rs @@ -0,0 +1,116 @@ +use relm4::{ + factory::{DynamicIndex, FactoryPrototype, FactoryVecDeque}, + gtk::{self, prelude::*, Orientation, TextBuffer}, +}; + +use crate::hl::{self, highlight_text_buffer}; + +use super::AppMsg; + +pub enum Entry { + Error(TextBuffer), + LuaData { + src: TextBuffer, + out: String, + result: TextBuffer, + }, +} + +impl FactoryPrototype for Entry { + type Factory = FactoryVecDeque; + type Widgets = EntryWidgets; + type View = gtk::Box; + type Msg = AppMsg; + type Root = gtk::Box; + + fn position(&self, _key: &DynamicIndex) {} + + fn init_view(&self, _key: &DynamicIndex, _sender: relm4::Sender) -> Self::Widgets { + let (first_buf, second_buf) = match self { + Self::Error(e) => (e, None), + Self::LuaData { src, result, .. } => (src, Some(result)), + }; + + if let Some(json) = second_buf { + highlight_text_buffer( + first_buf, + unsafe { crate::tree_sitter_lua() }, + hl::LUA_HL_QUERY, + ); + + highlight_text_buffer( + json, + unsafe { crate::tree_sitter_json() }, + hl::JSON_HL_QUERY, + ); + } else { + first_buf.apply_tag( + &first_buf.tag_table().lookup("error").unwrap(), + &first_buf.start_iter(), + &first_buf.end_iter(), + ); + } + + let main_box = gtk::Box::new(Orientation::Vertical, 5); + main_box.set_valign(gtk::Align::Start); + main_box.set_baseline_position(gtk::BaselinePosition::Top); + + let first_view = gtk::TextView::builder() + .height_request(20) + .editable(false) + .monospace(true) + .buffer(first_buf) + .build(); + + let second_view = match self { + Self::LuaData { out, .. } if !out.is_empty() => { + let buf = gtk::TextBuffer::new(None); + buf.set_text(out); + Some( + gtk::TextView::builder() + .height_request(20) + .editable(false) + .monospace(true) + .buffer(&buf) + .build(), + ) + }, + _ => None, + }; + + let third_view = second_buf.map(|b| { + gtk::TextView::builder() + .height_request(20) + .editable(false) + .monospace(true) + .buffer(b) + .build() + }); + + main_box.append(&first_view); + if let Some(v) = second_view { + main_box.append(&v); + } + if let Some(v) = third_view { + main_box.append(&v); + } + + main_box.set_visible(true); + + + EntryWidgets { main_box } + } + + fn view(&self, _key: &DynamicIndex, _widgets: &Self::Widgets) { + // This widget is never updated :P + } + + fn root_widget(widgets: &Self::Widgets) -> &Self::Root { + &widgets.main_box + } +} + +#[derive(Debug)] +pub struct EntryWidgets { + main_box: gtk::Box, +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..6fd9a15 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,283 @@ +use std::{ + sync::{Arc, Mutex}, + thread, +}; + +use entry::Entry; +use mlua::Lua; +use relm4::{ + factory::FactoryVecDeque, + gtk::{self, gdk, prelude::*, Inhibit, Orientation, TextBuffer, TextTag, TextTagTable}, + send, + AppUpdate, + Model, + WidgetPlus, + Widgets, +}; + +use crate::{ + hl::{self, highlight_text_buffer}, + lua::try_eval, +}; + +mod entry; + +pub struct AppModel { + lua: Arc>, + input: TextBuffer, + entries: FactoryVecDeque, + loading: bool, + history: Vec, + history_idx: usize, +} + +impl AppModel { + pub fn new() -> anyhow::Result { + let table = TextTagTable::new(); + + fn tag(name: &str, color: &str) -> TextTag { + TextTag::builder().name(name).foreground(color).build() + } + + table.add( + &TextTag::builder() + .name("error") + .foreground("red") + .background("black") + .build(), + ); + + table.add(&tag("attribute", "#50fa7b")); + table.add(&tag("comment", "#6272a4")); + table.add(&tag("conditional", "#ff79c6")); + table.add(&tag("constant", "#6be5fd")); + table.add(&tag("function", "#50fa7b")); + table.add(&tag("function.builtin", "#8be9fd")); + table.add(&tag("keyword", "#ff79c6")); + table.add(&tag("label", "#bd93f9")); + table.add(&tag("number", "#bd93f9")); + table.add(&tag("operator", "#ff79c6")); + table.add(&tag("punctuation", "#f8f8f2")); + table.add(&tag("repeat", "#ff79c6")); + table.add(&tag("string", "#f1fa8c")); + table.add(&tag("type", "#8be9fd")); + table.add(&tag("variable", "#f8f8f2")); + + Ok(Self { + lua: unsafe { Arc::new(Mutex::new(Lua::unsafe_new())) }, + input: TextBuffer::new(Some(&table)), + entries: FactoryVecDeque::new(), + loading: false, + history: Vec::new(), + history_idx: 0, + }) + } +} + +impl AppUpdate for AppModel { + fn update( + &mut self, + msg: Self::Msg, + _components: &Self::Components, + sender: relm4::Sender, + ) -> bool { + match msg { + AppMsg::Eval => { + self.loading = true; + let src = self + .input + .text(&self.input.start_iter(), &self.input.end_iter(), false) + .to_string(); + let lua = Arc::clone(&self.lua); + thread::spawn(move || match try_eval(&*lua.lock().unwrap(), &src) { + Ok((out, r)) => { + send!( + sender, + AppMsg::AddEntry(StringEntry::LuaData { + src, + result: r, + out, + }) + ); + send!(sender, AppMsg::ClearInput); + }, + + Err(e) => send!(sender, AppMsg::AddEntry(StringEntry::Error(e.to_string()))), + }); + }, + AppMsg::AddEntry(e) => { + self.entries.push_front(match e { + StringEntry::Error(e) => Entry::Error( + gtk::TextBuffer::builder() + .tag_table(&self.input.tag_table()) + .text(&e) + .build(), + ), + StringEntry::LuaData { src, out, result } => Entry::LuaData { + src: gtk::TextBuffer::builder() + .tag_table(&self.input.tag_table()) + .text(&src) + .build(), + result: gtk::TextBuffer::builder() + .tag_table(&self.input.tag_table()) + .text(&result) + .build(), + out, + }, + }); + self.loading = false; + }, + AppMsg::ClearInput => { + let input = self + .input + .text(&self.input.start_iter(), &self.input.end_iter(), false) + .to_string(); + + if !input.is_empty() { + self.history.push(input); + } + self.history_idx += 1; + self.input.set_text(""); + }, + AppMsg::ClearEntries => self.entries.clear(), + AppMsg::InputUpdate => highlight_text_buffer( + &self.input, + unsafe { crate::tree_sitter_lua() }, + hl::LUA_HL_QUERY, + ), + AppMsg::History(HistoryChange::Prev) => { + if self.history.len() == self.history_idx { + let input = self + .input + .text(&self.input.start_iter(), &self.input.end_iter(), false) + .to_string(); + + if !input.is_empty() { + self.history.push(input); + } + } + + if let Some(idx) = self.history_idx.checked_sub(1) { + self.history_idx = idx; + self.input.set_text(&self.history[idx]); + send!(sender, AppMsg::InputUpdate); + } + }, + AppMsg::History(HistoryChange::Next) => { + match (self.history_idx + 1).cmp(&self.history.len()) { + std::cmp::Ordering::Less => { + self.history_idx += 1; + self.input.set_text(&self.history[self.history_idx]); + }, + std::cmp::Ordering::Equal => { + self.history_idx += 1; + self.input.set_text(""); + }, + _ => {}, + } + send!(sender, AppMsg::InputUpdate); + }, + } + true + } +} + +impl Model for AppModel { + type Msg = AppMsg; + type Widgets = AppWidgets; + type Components = (); +} + +pub enum StringEntry { + Error(String), + LuaData { + src: String, + out: String, + result: String, + }, +} + +pub enum AppMsg { + Eval, + AddEntry(StringEntry), + ClearInput, + ClearEntries, + InputUpdate, + History(HistoryChange), +} + +pub enum HistoryChange { + Next, + Prev, +} + +#[relm4::widget(pub)] +impl Widgets for AppWidgets { + view! { + gtk::ApplicationWindow { + set_title: Some("luna"), + set_default_width: 300, + set_default_height: 200, + + set_child = Some(>k::Box) { + set_orientation: Orientation::Vertical, + set_margin_all: 5, + set_spacing: 5, + + append: scroll = >k::ScrolledWindow { + set_child = Some(>k::Box) { + set_orientation: Orientation::Vertical, + set_hexpand: true, + set_vexpand: true, + set_spacing: 8, + + factory!(model.entries), + } + }, + append = >k::Box { + set_orientation: Orientation::Horizontal, + + append = >k::TextView { + set_hexpand: true, + set_buffer: Some(&model.input), + set_monospace: true, + + add_controller = >k::EventControllerKey { + connect_key_pressed(sender) => move |_, key, _, state| { + send!(sender, AppMsg::InputUpdate); + if !state.contains(gdk::ModifierType::SHIFT_MASK) { + match key { + gdk::Key::Return => { + send!(sender, AppMsg::Eval); + return Inhibit(true); + } + gdk::Key::Up => { + send!(sender, AppMsg::History(HistoryChange::Prev)); + return Inhibit(true); + } + gdk::Key::Down => { + send!(sender, AppMsg::History(HistoryChange::Next)); + return Inhibit(true); + } + _ => {} + } + } + Inhibit(false) + }, + }, + }, + + append = >k::Spinner { + set_spinning: watch! { model.loading }, + }, + + append = >k::Button { + set_label: "Clear List", + + connect_clicked(sender) => move |_| send!(sender, AppMsg::ClearEntries), + }, + }, + }, + } + } +} diff --git a/ts/json_highlights.scm b/ts/json_highlights.scm new file mode 100644 index 0000000..9bf4279 --- /dev/null +++ b/ts/json_highlights.scm @@ -0,0 +1,14 @@ +(true) @boolean +(false) @boolean +(null) @constant.builtin +(number) @number +(pair key: (string) @label) +(pair value: (string) @string) +(array (string) @string) +(string_content (escape_sequence) @string.escape) +(ERROR) @error +"," @punctuation.delimiter +"[" @punctuation.bracket +"]" @punctuation.bracket +"{" @punctuation.bracket +"}" @punctuation.bracket diff --git a/ts/lua_highlights.scm b/ts/lua_highlights.scm new file mode 100644 index 0000000..68e7015 --- /dev/null +++ b/ts/lua_highlights.scm @@ -0,0 +1,194 @@ +;;; Builtins + +[ + (false) + (true) +] @boolean + +(nil) @constant.builtin + +((identifier) @variable.builtin + (#match? @variable.builtin "self")) + +;; Keywords + +"return" @keyword.return + +[ + "goto" + "in" + "local" +] @keyword + +(label_statement) @label + +(break_statement) @keyword + +(do_statement +[ + "do" + "end" +] @keyword) + +(while_statement +[ + "while" + "do" + "end" +] @repeat) + +(repeat_statement +[ + "repeat" + "until" +] @repeat) + +(if_statement +[ + "if" + "elseif" + "else" + "then" + "end" +] @conditional) + +(elseif_statement +[ + "elseif" + "then" + "end" +] @conditional) + +(else_statement +[ + "else" + "end" +] @conditional) + +(for_statement +[ + "for" + "do" + "end" +] @repeat) + +(function_declaration +[ + "function" + "end" +] @keyword) + +(function_definition +[ + "function" + "end" +] @keyword) + +;; Operators + +[ + "and" + "not" + "or" +] @keyword.operator + +[ + "+" + "-" + "*" + "/" + "%" + "^" + "#" + "==" + "~=" + "<=" + ">=" + "<" + ">" + "=" + "&" + "~" + "|" + "<<" + ">>" + "//" + ".." +] @operator + +;; Punctuations + +[ + ";" + ":" + "," + "." +] @punctuation.delimiter + +;; Brackets + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +;; Variables + +(identifier) @variable + +;; Constants + +(vararg_expression) @constant + +((identifier) @constant + (#lua-match? @constant "^[A-Z][A-Z_0-9]*$")) + +;; Tables + +(field name: (identifier) @field) + +(dot_index_expression field: (identifier) @field) + +(table_constructor +[ + "{" + "}" +] @constructor) + +;; Functions + +(parameters (identifier) @parameter) + +(function_call name: (identifier) @function) +(function_declaration name: (identifier) @function) + +(function_call name: (dot_index_expression field: (identifier) @function)) +(function_declaration name: (dot_index_expression field: (identifier) @function)) + +(method_index_expression method: (identifier) @method) + +(function_call + (identifier) @function.builtin + (#any-of? @function.builtin + ;; built-in functions in Lua 5.1 + "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs" + "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print" + "rawequal" "rawget" "rawset" "require" "select" "setfenv" "setmetatable" + "tonumber" "tostring" "type" "unpack" "xpcall")) + +;; Others + +(comment) @comment + +(hash_bang_line) @comment + +(number) @number + +(string) @string + +;; Error +(ERROR) @error diff --git a/ts/tree-sitter-json b/ts/tree-sitter-json new file mode 160000 index 0000000..203e239 --- /dev/null +++ b/ts/tree-sitter-json @@ -0,0 +1 @@ +Subproject commit 203e239408d642be83edde8988d6e7b20a19f0e8 diff --git a/ts/tree-sitter-lua b/ts/tree-sitter-lua new file mode 160000 index 0000000..547184a --- /dev/null +++ b/ts/tree-sitter-lua @@ -0,0 +1 @@ +Subproject commit 547184a6cfcc900fcac4a2a56538fa8bcdb293e6