From 43d472a5e49cfd27c555459e12255e606f65dac2 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 27 Dec 2023 07:50:09 -0500 Subject: [PATCH] Implement basic input/output TUI --- Cargo.lock | 12 ++++++++ Cargo.toml | 1 + src/error.rs | 3 ++ src/evaluate.rs | 68 +++++++++++++++++++++++++++++++--------- src/main.rs | 82 ++++++++++++++++++++++++++++++++++--------------- 5 files changed, 126 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b287a2..26b2e67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,7 @@ dependencies = [ "serde_json", "toml", "tree-sitter", + "tui-textarea", ] [[package]] @@ -1465,6 +1466,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tui-textarea" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e38ced1f941a9cfc923fbf2fe6858443c42cc5220bfd35bdd3648371e7bd8e" +dependencies = [ + "crossterm", + "ratatui", + "unicode-width", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index 58ad2a5..b905cd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" toml = "0.8.8" tree-sitter = "0.20.10" +tui-textarea = "0.4.0" [build-dependencies] cc = "1.0" diff --git a/src/error.rs b/src/error.rs index 8a45078..7036d3e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -20,6 +20,8 @@ pub type Result = std::result::Result; #[derive(Clone, PartialEq)] pub enum Error { + NoUserInput, + WithContext { error: Box, location: Point, @@ -412,6 +414,7 @@ impl fmt::Display for Error { source, } => write!(f, "{error} Occured at {location}: \"{source}\""), SerdeJson(message) => write!(f, "JSON processing error: {message}"), + NoUserInput => write!(f, "No input was provided."), } } } diff --git a/src/evaluate.rs b/src/evaluate.rs index 9c9c0a5..fab47da 100644 --- a/src/evaluate.rs +++ b/src/evaluate.rs @@ -2,9 +2,11 @@ //! //! You can use this library externally by calling either of the "eval" //! functions or by constructing your own Evaluator. +use std::cell::RefCell; + use tree_sitter::{Parser, Tree as TSTree}; -use crate::{language, AbstractTree, Map, Result, Root, Value}; +use crate::{language, AbstractTree, Error, Map, Result, Root, Value}; /// Evaluate the given source code. /// @@ -43,28 +45,37 @@ pub fn evaluate(source: &str) -> Result { /// ); /// ``` 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() + Interpreter::parse(context, &RefCell::new(source.to_string()))?.run() } /// A source code interpreter for the Dust language. pub struct Interpreter<'c, 's> { - _parser: Parser, + parser: Parser, context: &'c mut Map, - source: &'s str, - syntax_tree: TSTree, - abstract_tree: Root, + source: &'s RefCell, + syntax_tree: Option, + abstract_tree: Option, } 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)?; + pub fn parse(context: &'c mut Map, source: &'s RefCell) -> Result { + let mut parser = Parser::new(); + + parser.set_language(language()).unwrap(); + + let syntax_tree = parser.parse(source.borrow().as_str(), None); + let abstract_tree = if let Some(syntax_tree) = &syntax_tree { + Some(Root::from_syntax_node( + source.borrow().as_str(), + syntax_tree.root_node(), + context, + )?) + } else { + panic!() + }; Ok(Interpreter { - _parser: parser, + parser, context, source, syntax_tree, @@ -72,11 +83,38 @@ impl<'c, 's> Interpreter<'c, 's> { }) } + pub fn update(&mut self) -> Result<()> { + let source = self.source.borrow(); + let syntax_tree = self.parser.parse(source.as_str(), None); + let abstract_tree = if let Some(syntax_tree) = &syntax_tree { + Some(Root::from_syntax_node( + source.as_str(), + syntax_tree.root_node(), + self.context, + )?) + } else { + None + }; + + self.syntax_tree = syntax_tree; + self.abstract_tree = abstract_tree; + + Ok(()) + } + pub fn run(&mut self) -> Result { - self.abstract_tree.run(self.source, self.context) + if let Some(abstract_tree) = &self.abstract_tree { + abstract_tree.run(self.source.borrow().as_ref(), self.context) + } else { + Err(Error::NoUserInput) + } } pub fn syntax_tree(&self) -> String { - self.syntax_tree.root_node().to_sexp() + if let Some(syntax_tree) = &self.syntax_tree { + syntax_tree.root_node().to_sexp() + } else { + "".to_string() + } } } diff --git a/src/main.rs b/src/main.rs index 765faf4..71cf10b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,19 @@ //! Command line interface for the dust programming language. use clap::Parser; use crossterm::{ - event::{self, KeyCode, KeyEventKind}, + event::{poll, read, Event, KeyCode}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; -use ratatui::{prelude::CrosstermBackend, style::Stylize, widgets::Paragraph, Terminal}; -use tree_sitter::Parser as TSParser; +use ratatui::{ + prelude::{CrosstermBackend, Rect}, + widgets::Paragraph, + Terminal, +}; +use std::{cell::RefCell, fs::read_to_string, io::stdout, time::Duration}; +use tui_textarea::TextArea; -use std::{fs::read_to_string, io::stdout}; - -use dust_lang::{language, Interpreter, Map, Result, Value}; +use dust_lang::{Interpreter, Map, Result, Value}; /// Command-line arguments to be parsed. #[derive(Parser, Debug)] @@ -44,16 +47,19 @@ fn main() { let args = Args::parse(); if args.path.is_none() && args.command.is_none() { - return run_cli_shell().unwrap(); + run_tui().unwrap(); + + return; } - let source = if let Some(path) = &args.path { + let source_input = if let Some(path) = &args.path { read_to_string(path).unwrap() } else if let Some(command) = &args.command { command.clone() } else { - "".to_string() + "(output 'Hello dust!')".to_string() }; + let source = RefCell::new(source_input); let mut context = Map::new(); @@ -71,10 +77,7 @@ fn main() { .unwrap(); } - let mut parser = TSParser::new(); - parser.set_language(language()).unwrap(); - - let mut interpreter = Interpreter::parse(parser, &mut context, &source).unwrap(); + let mut interpreter = Interpreter::parse(&mut context, &source).unwrap(); if args.interactive { loop { @@ -100,30 +103,59 @@ fn main() { } } -fn run_cli_shell() -> Result<()> { - let mut _context = Map::new(); +fn run_tui() -> Result<()> { + let user_input = RefCell::new("(output 'Hello dust!')".to_string()); + let mut context = Map::new(); + let mut interpreter = Interpreter::parse(&mut context, &user_input)?; + + interpreter.update()?; + + let mut interpreter_output = interpreter.run(); stdout().execute(EnterAlternateScreen)?; enable_raw_mode()?; let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; + let mut textarea = TextArea::default(); loop { terminal.draw(|frame| { - let area = frame.size(); - frame.render_widget( - Paragraph::new("Hello Ratatui! (press 'q' to quit)") - .white() - .on_blue(), - area, - ); + let input_area = Rect { + x: 0, + y: 0, + width: frame.size().width, + height: frame.size().height / 2, + }; + + frame.render_widget(textarea.widget(), input_area); + + let output_area = Rect { + x: input_area.left(), + y: input_area.bottom(), + width: frame.size().width, + height: frame.size().height / 2, + }; + + match &interpreter_output { + Ok(value) => frame.render_widget(Paragraph::new(value.to_string()), output_area), + Err(error) => frame.render_widget(Paragraph::new(error.to_string()), output_area), + } })?; - if event::poll(std::time::Duration::from_millis(16))? { - if let event::Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { + if poll(Duration::from_millis(16))? { + if let Event::Key(key) = read()? { + if key.code == KeyCode::Esc { break; } + if key.code == KeyCode::Enter { + let input = textarea.lines().join("\n"); + + user_input.replace(input); + interpreter.update()?; + interpreter_output = interpreter.run(); + } + + textarea.input(key); } } }