master
LordMZTE 1 year ago
commit be6653be6f
Signed by: LordMZTE
GPG Key ID: B64802DC33A64FF6

2
.gitignore vendored

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

6
.gitmodules vendored

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

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

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

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

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

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

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

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

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

@ -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),
},
},
},
}
}
}

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

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

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

@ -0,0 +1 @@
Subproject commit 547184a6cfcc900fcac4a2a56538fa8bcdb293e6
Loading…
Cancel
Save