From 6be92041233ce9866d4c99b9f2baf00f01353852 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 29 Dec 2023 14:01:54 -0500 Subject: [PATCH] Improve Intepreter API for shell use --- Cargo.lock | 2 +- src/abstract_tree/assignment.rs | 10 +- src/abstract_tree/function_call.rs | 8 +- src/abstract_tree/if_else.rs | 15 +- src/abstract_tree/index.rs | 8 +- src/abstract_tree/match.rs | 6 +- src/abstract_tree/type_definition.rs | 6 +- src/abstract_tree/use.rs | 4 +- src/abstract_tree/value_node.rs | 44 ++--- src/abstract_tree/while.rs | 4 +- src/built_in_functions/map.rs | 30 ++++ src/error.rs | 14 +- src/evaluate.rs | 82 --------- src/interpret.rs | 109 ++++++++++++ src/lib.rs | 4 +- src/main.rs | 4 +- src/tui/editor.rs | 182 ++++++++++++++++++++ src/tui/log.rs | 84 +++++++++ src/tui/mod.rs | 245 +++++++++++++++++++++++++++ src/tui/output_display.rs | 33 ++++ src/tui/search_box.rs | 0 test.txt | 1 + tests/dust_examples.rs | 32 ++-- 23 files changed, 772 insertions(+), 155 deletions(-) create mode 100644 src/built_in_functions/map.rs delete mode 100644 src/evaluate.rs create mode 100644 src/interpret.rs create mode 100644 src/tui/editor.rs create mode 100644 src/tui/log.rs create mode 100644 src/tui/mod.rs create mode 100644 src/tui/output_display.rs create mode 100644 src/tui/search_box.rs create mode 100644 test.txt diff --git a/Cargo.lock b/Cargo.lock index 7496f6f..a1449cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,7 +316,7 @@ dependencies = [ [[package]] name = "dust-lang" -version = "0.3.8" +version = "0.3.9" dependencies = [ "ansi_term", "cc", diff --git a/src/abstract_tree/assignment.rs b/src/abstract_tree/assignment.rs index 01fd935..6f168a0 100644 --- a/src/abstract_tree/assignment.rs +++ b/src/abstract_tree/assignment.rs @@ -153,25 +153,25 @@ impl AbstractTree for Assignment { #[cfg(test)] mod tests { - use crate::{evaluate, Error, List, Type, Value}; + use crate::{interpret, Error, List, Type, Value}; #[test] fn simple_assignment() { - let test = evaluate("x = 1 x").unwrap(); + let test = interpret("x = 1 x").unwrap(); assert_eq!(Value::Integer(1), test); } #[test] fn simple_assignment_with_type() { - let test = evaluate("x = 1 x").unwrap(); + let test = interpret("x = 1 x").unwrap(); assert_eq!(Value::Integer(1), test); } #[test] fn list_add_assign() { - let test = evaluate( + let test = interpret( " x <[int]> = [] x += 1 @@ -185,7 +185,7 @@ mod tests { #[test] fn list_add_wrong_type() { - let result = evaluate( + let result = interpret( " x <[str]> = [] x += 1 diff --git a/src/abstract_tree/function_call.rs b/src/abstract_tree/function_call.rs index 32db0a6..4ccbdd2 100644 --- a/src/abstract_tree/function_call.rs +++ b/src/abstract_tree/function_call.rs @@ -173,12 +173,12 @@ impl AbstractTree for FunctionCall { #[cfg(test)] mod tests { - use crate::{evaluate, Value}; + use crate::{interpret, Value}; #[test] fn evaluate_function_call() { assert_eq!( - evaluate( + interpret( " foobar = (fn message ) { message } (foobar 'Hiya') @@ -191,7 +191,7 @@ mod tests { #[test] fn evaluate_callback() { assert_eq!( - evaluate( + interpret( " foobar = (fn cb <() -> str>) { (cb) @@ -206,6 +206,6 @@ mod tests { #[test] fn evaluate_built_in_function_call() { - assert_eq!(evaluate("(output 'Hiya')"), Ok(Value::Option(None))); + assert_eq!(interpret("(output 'Hiya')"), Ok(Value::Option(None))); } } diff --git a/src/abstract_tree/if_else.rs b/src/abstract_tree/if_else.rs index 715a22c..7c6886c 100644 --- a/src/abstract_tree/if_else.rs +++ b/src/abstract_tree/if_else.rs @@ -88,21 +88,24 @@ impl AbstractTree for IfElse { #[cfg(test)] mod tests { - use crate::{evaluate, Value}; + use crate::{interpret, Value}; #[test] fn evaluate_if() { assert_eq!( - evaluate("if true { 'true' }"), + interpret("if true { 'true' }"), Ok(Value::String("true".to_string())) ); } #[test] fn evaluate_if_else() { - assert_eq!(evaluate("if false { 1 } else { 2 }"), Ok(Value::Integer(2))); assert_eq!( - evaluate("if true { 1.0 } else { 42.0 }"), + interpret("if false { 1 } else { 2 }"), + Ok(Value::Integer(2)) + ); + assert_eq!( + interpret("if true { 1.0 } else { 42.0 }"), Ok(Value::Float(1.0)) ); } @@ -110,7 +113,7 @@ mod tests { #[test] fn evaluate_if_else_else_if_else() { assert_eq!( - evaluate( + interpret( " if false { 'no' @@ -128,7 +131,7 @@ mod tests { #[test] fn evaluate_if_else_if_else_if_else_if_else() { assert_eq!( - evaluate( + interpret( " if false { 'no' diff --git a/src/abstract_tree/index.rs b/src/abstract_tree/index.rs index cfe7e53..f1886fa 100644 --- a/src/abstract_tree/index.rs +++ b/src/abstract_tree/index.rs @@ -100,25 +100,25 @@ impl AbstractTree for Index { #[cfg(test)] mod tests { use super::*; - use crate::evaluate; + use crate::interpret; #[test] fn list_index() { - let test = evaluate("x = [1 [2] 3] x:1:0").unwrap(); + let test = interpret("x = [1 [2] 3] x:1:0").unwrap(); assert_eq!(Value::Integer(2), test); } #[test] fn map_index() { - let test = evaluate("x = {y = {z = 2}} x:y:z").unwrap(); + let test = interpret("x = {y = {z = 2}} x:y:z").unwrap(); assert_eq!(Value::Integer(2), test); } #[test] fn complex_index() { - let test = evaluate( + let test = interpret( " x = [1 2 3] y = (fn) { 0 } diff --git a/src/abstract_tree/match.rs b/src/abstract_tree/match.rs index 298bb3e..a146489 100644 --- a/src/abstract_tree/match.rs +++ b/src/abstract_tree/match.rs @@ -85,11 +85,11 @@ impl AbstractTree for Match { #[cfg(test)] mod tests { - use crate::{evaluate, Value}; + use crate::{interpret, Value}; #[test] fn evaluate_match() { - let test = evaluate( + let test = interpret( " match 1 { 3 => false @@ -105,7 +105,7 @@ mod tests { #[test] fn evaluate_match_assignment() { - let test = evaluate( + let test = interpret( " x = match 1 { 3 => false diff --git a/src/abstract_tree/type_definition.rs b/src/abstract_tree/type_definition.rs index e9b3f62..394b2b3 100644 --- a/src/abstract_tree/type_definition.rs +++ b/src/abstract_tree/type_definition.rs @@ -261,13 +261,13 @@ impl Display for Type { #[cfg(test)] mod tests { - use crate::evaluate; + use crate::interpret; use super::*; #[test] fn simple_type_check() { - let result = evaluate("x = 1"); + let result = interpret("x = 1"); assert!(result.unwrap_err().is_type_check_error(&Error::TypeCheck { expected: Type::Boolean, @@ -277,7 +277,7 @@ mod tests { #[test] fn callback_type_check() { - let result = evaluate( + let result = interpret( " x = (fn cb <() -> bool>) { (cb) diff --git a/src/abstract_tree/use.rs b/src/abstract_tree/use.rs index a84b935..69678e8 100644 --- a/src/abstract_tree/use.rs +++ b/src/abstract_tree/use.rs @@ -3,7 +3,7 @@ use std::fs::read_to_string; use serde::{Deserialize, Serialize}; use tree_sitter::Node; -use crate::{evaluate_with_context, AbstractTree, Error, Map, Result, Type, Value}; +use crate::{interpret_with_context, AbstractTree, Error, Map, Result, Type, Value}; /// Abstract representation of a use statement. /// @@ -29,7 +29,7 @@ impl AbstractTree for Use { let file_contents = read_to_string(&self.path)?; let mut file_context = Map::new(); - evaluate_with_context(&file_contents, &mut file_context)?; + interpret_with_context(&file_contents, &mut file_context)?; for (key, (value, r#type)) in file_context.variables()?.iter() { context.set(key.clone(), value.clone(), Some(r#type.clone()))?; diff --git a/src/abstract_tree/value_node.rs b/src/abstract_tree/value_node.rs index 4194ca9..57bacc1 100644 --- a/src/abstract_tree/value_node.rs +++ b/src/abstract_tree/value_node.rs @@ -247,39 +247,39 @@ impl AbstractTree for ValueNode { } #[cfg(test)] mod tests { - use crate::{evaluate, List}; + use crate::{interpret, List}; use super::*; #[test] fn evaluate_empty() { - assert_eq!(evaluate("x = 9"), Ok(Value::Option(None))); - assert_eq!(evaluate("x = 1 + 1"), Ok(Value::Option(None))); + assert_eq!(interpret("x = 9"), Ok(Value::Option(None))); + assert_eq!(interpret("x = 1 + 1"), Ok(Value::Option(None))); } #[test] fn evaluate_integer() { - assert_eq!(evaluate("1"), Ok(Value::Integer(1))); - assert_eq!(evaluate("123"), Ok(Value::Integer(123))); - assert_eq!(evaluate("-666"), Ok(Value::Integer(-666))); + assert_eq!(interpret("1"), Ok(Value::Integer(1))); + assert_eq!(interpret("123"), Ok(Value::Integer(123))); + assert_eq!(interpret("-666"), Ok(Value::Integer(-666))); } #[test] fn evaluate_float() { - assert_eq!(evaluate("0.1"), Ok(Value::Float(0.1))); - assert_eq!(evaluate("12.3"), Ok(Value::Float(12.3))); - assert_eq!(evaluate("-6.66"), Ok(Value::Float(-6.66))); + assert_eq!(interpret("0.1"), Ok(Value::Float(0.1))); + assert_eq!(interpret("12.3"), Ok(Value::Float(12.3))); + assert_eq!(interpret("-6.66"), Ok(Value::Float(-6.66))); } #[test] fn evaluate_string() { - assert_eq!(evaluate("\"one\""), Ok(Value::String("one".to_string()))); - assert_eq!(evaluate("'one'"), Ok(Value::String("one".to_string()))); - assert_eq!(evaluate("`one`"), Ok(Value::String("one".to_string()))); - assert_eq!(evaluate("`'one'`"), Ok(Value::String("'one'".to_string()))); - assert_eq!(evaluate("'`one`'"), Ok(Value::String("`one`".to_string()))); + assert_eq!(interpret("\"one\""), Ok(Value::String("one".to_string()))); + assert_eq!(interpret("'one'"), Ok(Value::String("one".to_string()))); + assert_eq!(interpret("`one`"), Ok(Value::String("one".to_string()))); + assert_eq!(interpret("`'one'`"), Ok(Value::String("'one'".to_string()))); + assert_eq!(interpret("'`one`'"), Ok(Value::String("`one`".to_string()))); assert_eq!( - evaluate("\"'one'\""), + interpret("\"'one'\""), Ok(Value::String("'one'".to_string())) ); } @@ -287,7 +287,7 @@ mod tests { #[test] fn evaluate_list() { assert_eq!( - evaluate("[1, 2, 'foobar']"), + interpret("[1, 2, 'foobar']"), Ok(Value::List(List::with_items(vec![ Value::Integer(1), Value::Integer(2), @@ -304,7 +304,7 @@ mod tests { map.set("foo".to_string(), Value::String("bar".to_string()), None) .unwrap(); - assert_eq!(evaluate("{ x = 1, foo = 'bar' }"), Ok(Value::Map(map))); + assert_eq!(interpret("{ x = 1, foo = 'bar' }"), Ok(Value::Map(map))); } #[test] @@ -321,14 +321,14 @@ mod tests { .unwrap(); assert_eq!( - evaluate("{ x = 1, foo = 'bar' }"), + interpret("{ x = 1, foo = 'bar' }"), Ok(Value::Map(map)) ); } #[test] fn evaluate_map_type_errors() { - assert!(evaluate("{ foo = 'bar' }") + assert!(interpret("{ foo = 'bar' }") .unwrap_err() .is_type_check_error(&Error::TypeCheck { expected: Type::Boolean, @@ -338,14 +338,14 @@ mod tests { #[test] fn evaluate_function() { - let result = evaluate("(fn) { 1 }"); + let result = interpret("(fn) { 1 }"); let value = result.unwrap(); let function = value.as_function().unwrap(); assert_eq!(&Vec::::with_capacity(0), function.parameters()); assert_eq!(Ok(&Type::Integer), function.return_type()); - let result = evaluate("(fn x ) {true}"); + let result = interpret("(fn x ) {true}"); let value = result.unwrap(); let function = value.as_function().unwrap(); @@ -358,7 +358,7 @@ mod tests { #[test] fn evaluate_option() { - let result = evaluate("x = some(1); x").unwrap(); + let result = interpret("x = some(1); x").unwrap(); assert_eq!(Value::Option(Some(Box::new(Value::Integer(1)))), result); } diff --git a/src/abstract_tree/while.rs b/src/abstract_tree/while.rs index b976c15..4d3d145 100644 --- a/src/abstract_tree/while.rs +++ b/src/abstract_tree/while.rs @@ -40,10 +40,10 @@ impl AbstractTree for While { #[cfg(test)] mod tests { - use crate::{evaluate, Value}; + use crate::{interpret, Value}; #[test] fn evalualate_while_loop() { - assert_eq!(evaluate("while false { 'foo' }"), Ok(Value::Option(None))) + assert_eq!(interpret("while false { 'foo' }"), Ok(Value::Option(None))) } } diff --git a/src/built_in_functions/map.rs b/src/built_in_functions/map.rs new file mode 100644 index 0000000..27ed3cd --- /dev/null +++ b/src/built_in_functions/map.rs @@ -0,0 +1,30 @@ +//! Functions related to [Map][crate::Map] values. + +use crate::{BuiltInFunction, List, Map, Result, Type, Value}; + +pub struct Keys; + +impl BuiltInFunction for Keys { + fn name(&self) -> &'static str { + "keys" + } + + fn run(&self, arguments: &[Value], _context: &Map) -> Result { + let map = arguments.first().unwrap_or_default().as_map()?; + let variables = map.variables()?; + let mut keys = Vec::with_capacity(variables.len()); + + for (key, _) in variables.iter() { + keys.push(Value::String(key.clone())) + } + + Ok(Value::List(List::with_items(keys))) + } + + fn r#type(&self) -> Type { + Type::Function { + parameter_types: vec![Type::Map], + return_type: Box::new(Type::List(Box::new(Type::String))), + } + } +} diff --git a/src/error.rs b/src/error.rs index 8a45078..e9bcd81 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,7 @@ //! To deal with errors from dependencies, either create a new error variant //! or use the ToolFailure variant if the error can only occur inside a tool. -use tree_sitter::{Node, Point}; +use tree_sitter::{LanguageError, Node, Point}; use crate::{value::Value, BuiltInFunction, Type}; @@ -164,6 +164,8 @@ pub enum Error { }, SerdeJson(String), + + ParserCancelled, } impl Error { @@ -225,6 +227,12 @@ impl Error { } } +impl From for Error { + fn from(value: LanguageError) -> Self { + Error::External(value.to_string()) + } +} + impl From> for Error { fn from(value: PoisonError) -> Self { Error::External(value.to_string()) @@ -412,6 +420,10 @@ impl fmt::Display for Error { source, } => write!(f, "{error} Occured at {location}: \"{source}\""), SerdeJson(message) => write!(f, "JSON processing error: {message}"), + ParserCancelled => write!( + f, + "Parsing was cancelled either manually or because it took too long." + ), } } } diff --git a/src/evaluate.rs b/src/evaluate.rs deleted file mode 100644 index 9c9c0a5..0000000 --- a/src/evaluate.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! The top level of Dust's API with functions to interpret Dust code. -//! -//! You can use this library externally by calling either of the "eval" -//! functions or by constructing your own Evaluator. -use tree_sitter::{Parser, Tree as TSTree}; - -use crate::{language, AbstractTree, Map, Result, Root, Value}; - -/// Evaluate the given source code. -/// -/// Returns a vector of results from evaluating the source code. Each comment -/// and statemtent will have its own result. -/// -/// # Examples -/// -/// ```rust -/// # use dust_lang::*; -/// assert_eq!(evaluate("1 + 2 + 3"), Ok(Value::Integer(6))); -/// ``` -pub fn evaluate(source: &str) -> Result { - let mut context = Map::new(); - - evaluate_with_context(source, &mut context) -} - -/// Evaluate the given source code with the given context. -/// -/// # Examples -/// -/// ```rust -/// # use dust_lang::*; -/// let mut context = Map::new(); -/// -/// context.set("one".into(), 1.into(), None); -/// context.set("two".into(), 2.into(), None); -/// context.set("three".into(), 3.into(), None); -/// -/// let dust_code = "four = 4 one + two + three + four"; -/// -/// assert_eq!( -/// evaluate_with_context(dust_code, &mut context), -/// Ok(Value::Integer(10)) -/// ); -/// ``` -pub fn evaluate_with_context(source: &str, context: &mut Map) -> Result { - let mut parser = Parser::new(); - parser.set_language(language()).unwrap(); - - Interpreter::parse(parser, context, source)?.run() -} - -/// A source code interpreter for the Dust language. -pub struct Interpreter<'c, 's> { - _parser: Parser, - context: &'c mut Map, - source: &'s str, - syntax_tree: TSTree, - abstract_tree: Root, -} - -impl<'c, 's> Interpreter<'c, 's> { - pub fn parse(mut parser: Parser, context: &'c mut Map, source: &'s str) -> Result { - let syntax_tree = parser.parse(source, None).unwrap(); - let abstract_tree = Root::from_syntax_node(source, syntax_tree.root_node(), context)?; - - Ok(Interpreter { - _parser: parser, - context, - source, - syntax_tree, - abstract_tree, - }) - } - - pub fn run(&mut self) -> Result { - self.abstract_tree.run(self.source, self.context) - } - - pub fn syntax_tree(&self) -> String { - self.syntax_tree.root_node().to_sexp() - } -} diff --git a/src/interpret.rs b/src/interpret.rs new file mode 100644 index 0000000..d4f66ea --- /dev/null +++ b/src/interpret.rs @@ -0,0 +1,109 @@ +//! The top level of Dust's API with functions to interpret Dust code. +//! +//! You can use this library externally by calling either of the "eval" +//! functions or by constructing your own Evaluator. +use tree_sitter::{Parser, Tree as TSTree}; + +use crate::{language, AbstractTree, Map, Result, Root, Value}; + +/// Interpret the given source code. +/// +/// Returns a vector of results from evaluating the source code. Each comment +/// and statemtent will have its own result. +/// +/// # Examples +/// +/// ```rust +/// # use dust_lang::*; +/// assert_eq!(interpret("1 + 2 + 3"), Ok(Value::Integer(6))); +/// ``` +pub fn interpret(source: &str) -> Result { + let mut context = Map::new(); + + interpret_with_context(source, &mut context) +} + +/// Interpret the given source code with the given context. +/// +/// # Examples +/// +/// ```rust +/// # use dust_lang::*; +/// let mut context = Map::new(); +/// +/// context.set("one".into(), 1.into(), None); +/// context.set("two".into(), 2.into(), None); +/// context.set("three".into(), 3.into(), None); +/// +/// let dust_code = "four = 4 one + two + three + four"; +/// +/// assert_eq!( +/// interpret_with_context(dust_code, &mut context), +/// Ok(Value::Integer(10)) +/// ); +/// ``` +pub fn interpret_with_context(source: &str, context: &mut Map) -> Result { + let mut parser = Parser::new(); + parser.set_language(language())?; + + let mut interpreter = Interpreter::new(context, source)?; + let value = interpreter.run()?; + + Ok(value) +} + +/// A source code interpreter for the Dust language. +pub struct Interpreter<'c, 's> { + _parser: Parser, + context: &'c mut Map, + source: &'s str, + syntax_tree: Option, + abstract_tree: Option, +} + +impl<'c, 's> Interpreter<'c, 's> { + pub fn new(context: &'c mut Map, source: &'s str) -> Result { + let mut parser = Parser::new(); + + parser.set_language(language())?; + + Ok(Interpreter { + _parser: parser, + context, + source, + syntax_tree: None, + abstract_tree: None, + }) + } + + pub fn set_source(&mut self, source: &'s str) { + self.source = source; + } + + pub fn run(&mut self) -> Result { + self.syntax_tree = self._parser.parse(self.source, self.syntax_tree.as_ref()); + self.abstract_tree = if let Some(syntax_tree) = &self.syntax_tree { + Some(Root::from_syntax_node( + self.source, + syntax_tree.root_node(), + &self.context, + )?) + } else { + return Err(crate::Error::ParserCancelled); + }; + + if let Some(abstract_tree) = &self.abstract_tree { + abstract_tree.run(self.source, &self.context) + } else { + Ok(Value::Option(None)) + } + } + + pub fn syntax_tree(&self) -> Option { + if let Some(syntax_tree) = &self.syntax_tree { + Some(syntax_tree.root_node().to_sexp()) + } else { + None + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 41661ee..cc260ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,14 +8,14 @@ pub use crate::{ abstract_tree::*, built_in_functions::{BuiltInFunction, BUILT_IN_FUNCTIONS}, error::*, - evaluate::*, + interpret::*, value::{function::Function, list::List, map::Map, Value}, }; mod abstract_tree; pub mod built_in_functions; mod error; -mod evaluate; +mod interpret; mod value; use tree_sitter::Language; diff --git a/src/main.rs b/src/main.rs index 479e192..9d0b622 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use tree_sitter::Parser as TSParser; use std::{borrow::Cow, fs::read_to_string}; -use dust_lang::{evaluate_with_context, language, Interpreter, Map, Value}; +use dust_lang::{interpret_with_context, language, Interpreter, Map, Value}; /// Command-line arguments to be parsed. #[derive(Parser, Debug)] @@ -195,7 +195,7 @@ fn run_cli_shell() { rl.add_history_entry(line).unwrap(); - let eval_result = evaluate_with_context(line, &mut context); + let eval_result = interpret_with_context(line, &mut context); match eval_result { Ok(value) => println!("{value}"), diff --git a/src/tui/editor.rs b/src/tui/editor.rs new file mode 100644 index 0000000..c88e877 --- /dev/null +++ b/src/tui/editor.rs @@ -0,0 +1,182 @@ +use dust_lang::Result; +use ratatui::{ + layout::{Constraint, Direction, Layout}, + prelude::*, + style::{Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, +}; +use std::{ + borrow::Cow, + fmt::Display, + fs::File, + io::{self, Write}, + path::PathBuf, +}; +use tui_textarea::{CursorMove, Input, Key, TextArea}; + +use super::Action; + +pub struct Editor<'a> { + current: usize, + buffers: Vec>, + message: Option>, +} + +impl<'a> Editor<'a> { + pub fn new() -> Result { + Ok(Self { + current: 0, + buffers: Vec::new(), + message: None, + }) + } + + pub fn current_buffer(&self) -> &Buffer { + &self.buffers[self.current] + } + + pub fn add_buffer(&mut self, buffer: Buffer<'a>) { + self.buffers.push(buffer); + } + + pub fn run(&mut self, frame: &mut Frame, areas: &[Rect]) -> Option { + let buffer = &self.buffers[self.current]; + let textarea = &buffer.textarea; + let widget = textarea.widget(); + + frame.render_widget(widget, areas[0]); + + // Render status line + let modified = if buffer.modified { " [modified]" } else { "" }; + let slot = format!("[{}/{}]", self.current + 1, self.buffers.len()); + let path_text = if let Some(path) = &buffer.path { + format!(" {}{} ", path.display(), modified) + } else { + "scratch".to_string() + }; + let (row, col) = textarea.cursor(); + let cursor = format!("({},{})", row + 1, col + 1); + + let status_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Length(slot.len() as u16), + Constraint::Min(1), + Constraint::Length(cursor.len() as u16), + ] + .as_ref(), + ) + .split(areas[1]); + let status_style = Style::default().add_modifier(Modifier::REVERSED); + frame.render_widget(Paragraph::new(slot).style(status_style), status_chunks[0]); + frame.render_widget( + Paragraph::new(path_text).style(status_style), + status_chunks[1], + ); + frame.render_widget(Paragraph::new(cursor).style(status_style), status_chunks[2]); + + // Render message at bottom + let message = if let Some(message) = self.message.take() { + Line::from(Span::raw(message)) + } else { + Line::from(vec![ + Span::raw("Press "), + Span::styled("^Q", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to quit, "), + Span::styled("^S", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to save, "), + Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to search, "), + Span::styled("^T", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to switch buffer "), + Span::styled("^R", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to run"), + ]) + }; + frame.render_widget(Paragraph::new(message), areas[2]); + + match crossterm::event::read().unwrap().into() { + Input { + key: Key::Char('r'), + ctrl: true, + .. + } => return Some(Action::Submit), + Input { + key: Key::Char('q'), + ctrl: true, + .. + } => return Some(Action::Quit), + Input { + key: Key::Char('t'), + ctrl: true, + .. + } => { + self.current = (self.current + 1) % self.buffers.len(); + self.message = Some(format!("Switched to buffer #{}", self.current + 1).into()); + } + Input { + key: Key::Char('s'), + ctrl: true, + .. + } => { + self.buffers[self.current].save().unwrap(); + self.message = Some("Saved!".into()); + } + input => { + let buffer = &mut self.buffers[self.current]; + buffer.modified = buffer.textarea.input(input); + } + } + + None + } +} + +pub struct Buffer<'a> { + textarea: TextArea<'a>, + path: Option, + modified: bool, +} + +impl<'a> Buffer<'a> { + pub fn new(content: String) -> Result { + let mut textarea = TextArea::new(content.lines().map(|line| line.to_string()).collect()); + + textarea.set_line_number_style(Style::default().fg(Color::DarkGray)); + + Ok(Self { + textarea, + path: None, + modified: false, + }) + } + + pub fn content(&self) -> String { + self.textarea.lines().join("\n") + } + + fn save(&mut self) -> io::Result<()> { + if !self.modified { + return Ok(()); + } + + let file = if let Some(path) = &self.path { + File::create(path)? + } else { + File::create("/tmp/dust_buffer")? + }; + + let mut writer = io::BufWriter::new(file); + + for line in self.textarea.lines() { + writer.write_all(line.as_bytes())?; + writer.write_all(b"\n")?; + } + + self.modified = false; + + Ok(()) + } +} diff --git a/src/tui/log.rs b/src/tui/log.rs new file mode 100644 index 0000000..25003f7 --- /dev/null +++ b/src/tui/log.rs @@ -0,0 +1,84 @@ +use std::path::PathBuf; + +use color_eyre::eyre::Result; +use directories::ProjectDirs; +use lazy_static::lazy_static; +pub use tracing::error; +use tracing_error::ErrorLayer; +use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, Layer}; + +lazy_static! { + pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase().to_string(); + pub static ref DATA_FOLDER: Option = + std::env::var(format!("{}_DATA", PROJECT_NAME.clone())) + .ok() + .map(PathBuf::from); + pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone()); + pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME")); +} + +fn project_directory() -> Option { + ProjectDirs::from("io", "jeffa", env!("CARGO_PKG_NAME")) +} + +pub fn get_data_dir() -> PathBuf { + let directory = if let Some(s) = DATA_FOLDER.clone() { + s + } else if let Some(proj_dirs) = project_directory() { + proj_dirs.data_local_dir().to_path_buf() + } else { + PathBuf::from(".").join(".data") + }; + directory +} + +pub fn initialize_logging() -> Result<()> { + let directory = get_data_dir(); + std::fs::create_dir_all(directory.clone())?; + let log_path = directory.join(LOG_FILE.clone()); + let log_file = std::fs::File::create(log_path)?; + std::env::set_var( + "RUST_LOG", + std::env::var("RUST_LOG") + .or_else(|_| std::env::var(LOG_ENV.clone())) + .unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))), + ); + let file_subscriber = tracing_subscriber::fmt::layer() + .with_file(true) + .with_line_number(true) + .with_writer(log_file) + .with_target(false) + .with_ansi(false) + .with_filter(tracing_subscriber::filter::EnvFilter::from_default_env()); + tracing_subscriber::registry() + .with(file_subscriber) + .with(ErrorLayer::default()) + .init(); + Ok(()) +} + +/// Similar to the `std::dbg!` macro, but generates `tracing` events rather +/// than printing to stdout. +/// +/// By default, the verbosity level for the generated events is `DEBUG`, but +/// this can be customized. +#[macro_export] +macro_rules! trace_dbg { + (target: $target:expr, level: $level:expr, $ex:expr) => {{ + match $ex { + value => { + tracing::event!(target: $target, $level, ?value, stringify!($ex)); + value + } + } + }}; + (level: $level:expr, $ex:expr) => { + trace_dbg!(target: module_path!(), level: $level, $ex) + }; + (target: $target:expr, $ex:expr) => { + trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex) + }; + ($ex:expr) => { + trace_dbg!(level: tracing::Level::DEBUG, $ex) + }; +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 0000000..3d17658 --- /dev/null +++ b/src/tui/mod.rs @@ -0,0 +1,245 @@ +mod log; + +use std::{ + ops::{Deref, DerefMut}, + time::Duration, +}; + +use color_eyre::eyre::Result; +use crossterm::{ + cursor, + event::{ + DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture, + Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent, + }, + terminal::{EnterAlternateScreen, LeaveAlternateScreen}, +}; +use futures::{FutureExt, StreamExt}; +use ratatui::backend::CrosstermBackend as Backend; +use serde::{Deserialize, Serialize}; +use tokio::{ + sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, + task::JoinHandle, +}; +use tokio_util::sync::CancellationToken; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Event { + Init, + Quit, + Error, + Closed, + Tick, + Render, + FocusGained, + FocusLost, + Paste(String), + Key(KeyEvent), + Mouse(MouseEvent), + Resize(u16, u16), +} + +pub struct Tui { + pub terminal: ratatui::Terminal>, + pub task: JoinHandle<()>, + pub cancellation_token: CancellationToken, + pub event_rx: UnboundedReceiver, + pub event_tx: UnboundedSender, + pub frame_rate: f64, + pub tick_rate: f64, + pub mouse: bool, + pub paste: bool, +} + +impl Tui { + pub fn new() -> Result { + let tick_rate = 4.0; + let frame_rate = 60.0; + let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?; + let (event_tx, event_rx) = mpsc::unbounded_channel(); + let cancellation_token = CancellationToken::new(); + let task = tokio::spawn(async {}); + let mouse = false; + let paste = false; + Ok(Self { + terminal, + task, + cancellation_token, + event_rx, + event_tx, + frame_rate, + tick_rate, + mouse, + paste, + }) + } + + pub fn tick_rate(mut self, tick_rate: f64) -> Self { + self.tick_rate = tick_rate; + self + } + + pub fn frame_rate(mut self, frame_rate: f64) -> Self { + self.frame_rate = frame_rate; + self + } + + pub fn mouse(mut self, mouse: bool) -> Self { + self.mouse = mouse; + self + } + + pub fn paste(mut self, paste: bool) -> Self { + self.paste = paste; + self + } + + pub fn start(&mut self) { + let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate); + let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate); + self.cancel(); + self.cancellation_token = CancellationToken::new(); + let _cancellation_token = self.cancellation_token.clone(); + let _event_tx = self.event_tx.clone(); + self.task = tokio::spawn(async move { + let mut reader = crossterm::event::EventStream::new(); + let mut tick_interval = tokio::time::interval(tick_delay); + let mut render_interval = tokio::time::interval(render_delay); + _event_tx.send(Event::Init).unwrap(); + loop { + let tick_delay = tick_interval.tick(); + let render_delay = render_interval.tick(); + let crossterm_event = reader.next().fuse(); + tokio::select! { + _ = _cancellation_token.cancelled() => { + break; + } + maybe_event = crossterm_event => { + match maybe_event { + Some(Ok(evt)) => { + match evt { + CrosstermEvent::Key(key) => { + if key.kind == KeyEventKind::Press { + _event_tx.send(Event::Key(key)).unwrap(); + } + }, + CrosstermEvent::Mouse(mouse) => { + _event_tx.send(Event::Mouse(mouse)).unwrap(); + }, + CrosstermEvent::Resize(x, y) => { + _event_tx.send(Event::Resize(x, y)).unwrap(); + }, + CrosstermEvent::FocusLost => { + _event_tx.send(Event::FocusLost).unwrap(); + }, + CrosstermEvent::FocusGained => { + _event_tx.send(Event::FocusGained).unwrap(); + }, + CrosstermEvent::Paste(s) => { + _event_tx.send(Event::Paste(s)).unwrap(); + }, + } + } + Some(Err(_)) => { + _event_tx.send(Event::Error).unwrap(); + } + None => {}, + } + }, + _ = tick_delay => { + _event_tx.send(Event::Tick).unwrap(); + }, + _ = render_delay => { + _event_tx.send(Event::Render).unwrap(); + }, + } + } + }); + } + + pub fn stop(&self) -> Result<()> { + self.cancel(); + let mut counter = 0; + while !self.task.is_finished() { + std::thread::sleep(Duration::from_millis(1)); + counter += 1; + if counter > 50 { + self.task.abort(); + } + if counter > 100 { + log::error!("Failed to abort task in 100 milliseconds for unknown reason"); + break; + } + } + Ok(()) + } + + pub fn enter(&mut self) -> Result<()> { + crossterm::terminal::enable_raw_mode()?; + crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?; + if self.mouse { + crossterm::execute!(std::io::stderr(), EnableMouseCapture)?; + } + if self.paste { + crossterm::execute!(std::io::stderr(), EnableBracketedPaste)?; + } + self.start(); + Ok(()) + } + + pub fn exit(&mut self) -> Result<()> { + self.stop()?; + if crossterm::terminal::is_raw_mode_enabled()? { + self.flush()?; + if self.paste { + crossterm::execute!(std::io::stderr(), DisableBracketedPaste)?; + } + if self.mouse { + crossterm::execute!(std::io::stderr(), DisableMouseCapture)?; + } + crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?; + crossterm::terminal::disable_raw_mode()?; + } + Ok(()) + } + + pub fn cancel(&self) { + self.cancellation_token.cancel(); + } + + pub fn suspend(&mut self) -> Result<()> { + self.exit()?; + #[cfg(not(windows))] + signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; + Ok(()) + } + + pub fn resume(&mut self) -> Result<()> { + self.enter()?; + Ok(()) + } + + pub async fn next(&mut self) -> Option { + self.event_rx.recv().await + } +} + +impl Deref for Tui { + type Target = ratatui::Terminal>; + + fn deref(&self) -> &Self::Target { + &self.terminal + } +} + +impl DerefMut for Tui { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.terminal + } +} + +impl Drop for Tui { + fn drop(&mut self) { + self.exit().unwrap(); + } +} diff --git a/src/tui/output_display.rs b/src/tui/output_display.rs new file mode 100644 index 0000000..e908187 --- /dev/null +++ b/src/tui/output_display.rs @@ -0,0 +1,33 @@ +use dust_lang::Value; +use ratatui::{prelude::Rect, widgets::Paragraph, Frame}; + +pub struct OutputDisplay { + values: Vec, +} + +impl OutputDisplay { + pub fn new() -> Self { + OutputDisplay { values: Vec::new() } + } + + pub fn add_value(&mut self, value: Value) { + self.values.push(value); + } + + pub fn run(&self, frame: &mut Frame, area: Rect) { + for value in &self.values { + match value { + Value::List(_) => todo!(), + Value::Map(_) => todo!(), + Value::Function(_) => todo!(), + Value::String(string) => frame.render_widget(Paragraph::new(string.as_str()), area), + Value::Float(_) => todo!(), + Value::Integer(integer) => { + frame.render_widget(Paragraph::new(integer.to_string()), area) + } + Value::Boolean(_) => todo!(), + Value::Option(_) => todo!(), + } + } + } +} diff --git a/src/tui/search_box.rs b/src/tui/search_box.rs new file mode 100644 index 0000000..e69de29 diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ + diff --git a/tests/dust_examples.rs b/tests/dust_examples.rs index c275168..aaae467 100644 --- a/tests/dust_examples.rs +++ b/tests/dust_examples.rs @@ -6,7 +6,7 @@ use dust_lang::*; fn r#async() { let file_contents = read_to_string("examples/async.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] @@ -14,14 +14,14 @@ fn r#async() { fn async_download() { let file_contents = read_to_string("examples/async_download.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn clue_solver() { let file_contents = read_to_string("examples/clue_solver.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] @@ -29,89 +29,89 @@ fn clue_solver() { fn fetch() { let file_contents = read_to_string("examples/fetch.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn fibonacci() { let file_contents = read_to_string("examples/fibonacci.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn fizzbuzz() { let file_contents = read_to_string("examples/fizzbuzz.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn for_loop() { let file_contents = read_to_string("examples/for_loop.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn hello_world() { let file_contents = read_to_string("examples/hello_world.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn jq_data() { let file_contents = read_to_string("examples/jq_data.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn list() { let file_contents = read_to_string("examples/list.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn map() { let file_contents = read_to_string("examples/map.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn random() { let file_contents = read_to_string("examples/random.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn sea_creatures() { let file_contents = read_to_string("examples/sea_creatures.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn variables() { let file_contents = read_to_string("examples/variables.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn while_loop() { let file_contents = read_to_string("examples/while_loop.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); } #[test] fn r#yield() { let file_contents = read_to_string("examples/yield.ds").unwrap(); - evaluate(&file_contents).unwrap(); + interpret(&file_contents).unwrap(); }