This commit is contained in:
LordMZTE 2022-02-06 20:30:45 +01:00
commit be6653be6f
Signed by: LordMZTE
GPG Key ID: B64802DC33A64FF6
15 changed files with 829 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

6
.gitmodules vendored Normal file
View File

@ -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

19
Cargo.toml Normal file
View File

@ -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"

7
assets/style.css Normal file
View File

@ -0,0 +1,7 @@
textview text {
background-color: #2a2c39;
}
textview {
color: #f8f8f2;
}

14
build.rs Normal file
View File

@ -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");
}

12
rustfmt.toml Normal file
View File

@ -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

99
src/hl.rs Normal file
View File

@ -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::<Result<Vec<_>, _>>()) {
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<Highlight>,
pub spans: Vec<Span>,
}
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,
}

34
src/lua.rs Normal file
View File

@ -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::<serde_json::Value>(
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))
})
}

27
src/main.rs Normal file
View File

@ -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<Highlighter> = 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;
}

116
src/ui/entry.rs Normal file
View File

@ -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<Self>;
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::Msg>) -> 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,
}

283
src/ui/mod.rs Normal file
View File

@ -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<Mutex<Lua>>,
input: TextBuffer,
entries: FactoryVecDeque<Entry>,
loading: bool,
history: Vec<String>,
history_idx: usize,
}
impl AppModel {
pub fn new() -> anyhow::Result<Self> {
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<Self::Msg>,
) -> 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<AppModel, ()> for AppWidgets {
view! {
gtk::ApplicationWindow {
set_title: Some("luna"),
set_default_width: 300,
set_default_height: 200,
set_child = Some(&gtk::Box) {
set_orientation: Orientation::Vertical,
set_margin_all: 5,
set_spacing: 5,
append: scroll = &gtk::ScrolledWindow {
set_child = Some(&gtk::Box) {
set_orientation: Orientation::Vertical,
set_hexpand: true,
set_vexpand: true,
set_spacing: 8,
factory!(model.entries),
}
},
append = &gtk::Box {
set_orientation: Orientation::Horizontal,
append = &gtk::TextView {
set_hexpand: true,
set_buffer: Some(&model.input),
set_monospace: true,
add_controller = &gtk::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 = &gtk::Spinner {
set_spinning: watch! { model.loading },
},
append = &gtk::Button {
set_label: "Clear List",
connect_clicked(sender) => move |_| send!(sender, AppMsg::ClearEntries),
},
},
},
}
}
}

14
ts/json_highlights.scm Normal file
View File

@ -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

194
ts/lua_highlights.scm Normal file
View File

@ -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

1
ts/tree-sitter-json Submodule

@ -0,0 +1 @@
Subproject commit 203e239408d642be83edde8988d6e7b20a19f0e8

1
ts/tree-sitter-lua Submodule

@ -0,0 +1 @@
Subproject commit 547184a6cfcc900fcac4a2a56538fa8bcdb293e6