From 3c072786ae69c844fd1172e4fc1eb1a640a28413 Mon Sep 17 00:00:00 2001 From: Gabriel Konat Date: Wed, 22 Nov 2023 10:43:32 +0100 Subject: [PATCH] Interactive parser development example writing. --- src/4_example/c_2_cli.rs | 2 +- src/4_example/c_3_compile_parse.rs | 3 +- src/4_example/d_1_Cargo.toml | 18 ++++++++ src/4_example/d_2_main_editor.rs | 53 ++++++++++++++++++++++ src/4_example/d_3_editor.rs | 59 +++++++++++++++++++++++++ src/4_example/d_4_main_cli.rs | 70 ++++++++++++++++++++++++++++++ src/4_example/index.md | 70 ++++++++++++++++++++++++++---- stepper/src/app.rs | 7 +++ 8 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 src/4_example/d_1_Cargo.toml create mode 100644 src/4_example/d_2_main_editor.rs create mode 100644 src/4_example/d_3_editor.rs create mode 100644 src/4_example/d_4_main_cli.rs diff --git a/src/4_example/c_2_cli.rs b/src/4_example/c_2_cli.rs index b8c9f7a..20c4b3c 100644 --- a/src/4_example/c_2_cli.rs +++ b/src/4_example/c_2_cli.rs @@ -12,7 +12,7 @@ pub struct Args { /// Rule name (from the pest grammar file) used to parse program files. rule_name: String, /// Paths to program files to parse with the pest grammar. - program_file_paths: Vec + program_file_paths: Vec, } fn main() { diff --git a/src/4_example/c_3_compile_parse.rs b/src/4_example/c_3_compile_parse.rs index 5a2098f..e1f5f64 100644 --- a/src/4_example/c_3_compile_parse.rs +++ b/src/4_example/c_3_compile_parse.rs @@ -2,6 +2,7 @@ use std::fmt::Write; use std::path::PathBuf; use clap::Parser; + use pie::Pie; use pie::tracker::writing::WritingTracker; @@ -17,7 +18,7 @@ pub struct Args { /// Rule name (from the pest grammar file) used to parse program files. rule_name: String, /// Paths to program files to parse with the pest grammar. - program_file_paths: Vec + program_file_paths: Vec, } fn main() { diff --git a/src/4_example/d_1_Cargo.toml b/src/4_example/d_1_Cargo.toml new file mode 100644 index 0000000..0d137f8 --- /dev/null +++ b/src/4_example/d_1_Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pie" +version = "0.1.0" +edition = "2021" + +[dependencies] +pie_graph = "0.0.1" + +[dev-dependencies] +dev_shared = { path = "../dev_shared" } +assert_matches = "1" +pest = "2" +pest_meta = "2" +pest_vm = "2" +clap = { version = "4", features = ["derive"] } +ratatui = "0.24" +tui-textarea = "0.4" +crossterm = "0.27" diff --git a/src/4_example/d_2_main_editor.rs b/src/4_example/d_2_main_editor.rs new file mode 100644 index 0000000..5381f9a --- /dev/null +++ b/src/4_example/d_2_main_editor.rs @@ -0,0 +1,53 @@ +use std::fmt::Write; +use std::path::PathBuf; + +use clap::Parser; + +use pie::Pie; +use pie::tracker::writing::WritingTracker; + +use crate::task::{Outputs, Tasks}; + +pub mod parse; +pub mod task; +pub mod editor; + +#[derive(Parser)] +pub struct Args { + /// Path to the pest grammar file. + grammar_file_path: PathBuf, + /// Rule name (from the pest grammar file) used to parse program files. + rule_name: String, + /// Paths to program files to parse with the pest grammar. + program_file_paths: Vec, +} + +fn main() { + let args = Args::parse(); + compile_grammar_and_parse(args); +} + +fn compile_grammar_and_parse(args: Args) { + let mut pie = Pie::with_tracker(WritingTracker::with_stderr()); + + let mut session = pie.new_session(); + let mut errors = String::new(); + + let compile_grammar_task = Tasks::compile_grammar(&args.grammar_file_path); + if let Err(error) = session.require(&compile_grammar_task) { + let _ = writeln!(errors, "{}", error); // Ignore error: writing to String cannot fail. + } + + for path in args.program_file_paths { + let task = Tasks::parse(&compile_grammar_task, &path, &args.rule_name); + match session.require(&task) { + Err(error) => { let _ = writeln!(errors, "{}", error); } + Ok(Outputs::Parsed(Some(output))) => println!("Parsing '{}' succeeded: {}", path.display(), output), + _ => {} + } + } + + if !errors.is_empty() { + println!("Errors:\n{}", errors); + } +} diff --git a/src/4_example/d_3_editor.rs b/src/4_example/d_3_editor.rs new file mode 100644 index 0000000..0312780 --- /dev/null +++ b/src/4_example/d_3_editor.rs @@ -0,0 +1,59 @@ +use std::io; + +use crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; +use ratatui::backend::{Backend, CrosstermBackend}; +use ratatui::Terminal; +use ratatui::widgets::Paragraph; + +use crate::Args; + +/// Live parser development editor. +pub struct Editor {} + +impl Editor { + /// Create a new editor from `args`. + pub fn new(_args: Args) -> Result { + Ok(Self {}) + } + + /// Run the editor, drawing it into an alternate screen of the terminal. + pub fn run(&mut self) -> Result<(), io::Error> { + // Setup terminal for GUI rendering. + enable_raw_mode()?; + let mut backend = CrosstermBackend::new(io::stdout()); + crossterm::execute!(backend, EnterAlternateScreen, EnableMouseCapture)?; + let mut terminal = Terminal::new(backend)?; + terminal.clear()?; + + // Draw and process events in a loop until a quit is requested or an error occurs. + let result = loop { + match self.draw_and_process_event(&mut terminal) { + Ok(false) => break Ok(()), // Quit requested + Err(e) => break Err(e), // Error + _ => {}, + } + }; + + // First undo our changes to the terminal. + disable_raw_mode()?; + crossterm::execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; + terminal.show_cursor()?; + // Then present the result to the user. + result + } + + fn draw_and_process_event(&mut self, terminal: &mut Terminal) -> Result { + terminal.draw(|frame| { + frame.render_widget(Paragraph::new("Hello World! Press Esc to exit."), frame.size()); + })?; + + match crossterm::event::read()? { + Event::Key(key) if key.kind == KeyEventKind::Release => return Ok(true), // Skip releases. + Event::Key(key) if key.code == KeyCode::Esc => return Ok(false), + _ => {} + }; + + Ok(true) + } +} diff --git a/src/4_example/d_4_main_cli.rs b/src/4_example/d_4_main_cli.rs new file mode 100644 index 0000000..96ad665 --- /dev/null +++ b/src/4_example/d_4_main_cli.rs @@ -0,0 +1,70 @@ +use std::fmt::Write; +use std::io; +use std::path::PathBuf; + +use clap::Parser; + +use pie::Pie; +use pie::tracker::writing::WritingTracker; + +use crate::editor::Editor; +use crate::task::{Outputs, Tasks}; + +pub mod parse; +pub mod task; +pub mod editor; + +#[derive(Parser)] +struct Cli { + /// Start an interactive parser development editor. + #[arg(short, long)] + edit: bool, + #[command(flatten)] + args: Args, +} + +#[derive(Parser)] +pub struct Args { + /// Path to the pest grammar file. + grammar_file_path: PathBuf, + /// Rule name (from the pest grammar file) used to parse program files. + rule_name: String, + /// Paths to program files to parse with the pest grammar. + program_file_paths: Vec, +} + +fn main() -> Result<(), io::Error> { + let cli = Cli::parse(); + if cli.edit { + let mut editor = Editor::new(cli.args)?; + editor.run() + } else { + compile_grammar_and_parse(cli.args); + Ok(()) + } +} + +fn compile_grammar_and_parse(args: Args) { + let mut pie = Pie::with_tracker(WritingTracker::with_stderr()); + + let mut session = pie.new_session(); + let mut errors = String::new(); + + let compile_grammar_task = Tasks::compile_grammar(&args.grammar_file_path); + if let Err(error) = session.require(&compile_grammar_task) { + let _ = writeln!(errors, "{}", error); // Ignore error: writing to String cannot fail. + } + + for path in args.program_file_paths { + let task = Tasks::parse(&compile_grammar_task, &path, &args.rule_name); + match session.require(&task) { + Err(error) => { let _ = writeln!(errors, "{}", error); } + Ok(Outputs::Parsed(Some(output))) => println!("Parsing '{}' succeeded: {}", path.display(), output), + _ => {} + } + } + + if !errors.is_empty() { + println!("Errors:\n{}", errors); + } +} diff --git a/src/4_example/index.md b/src/4_example/index.md index 3ef79a9..cf3b906 100644 --- a/src/4_example/index.md +++ b/src/4_example/index.md @@ -243,24 +243,76 @@ The `pie_graph` library support serialization when the `serde` feature is enable Then, see [this serialization integration test](https://github.com/Gohla/pie/blob/pre_type_refactor/pie/tests/serde.rs). ``` -Feel free to experiment with the grammar, example files, etc. before continuing! +Feel free to experiment a bit with the grammar, example files, etc. before continuing. +We will develop an interactive editor next however, which will make experimentation easier! -## Editor +## Interactive Parser Development ```admonish warning title="Under Construction" This subsection is under construction. ``` -[//]: # (add dev-deps) +Now we'll create an interactive version of this grammar compilation and parsing pipeline, using [Ratatui](https://ratatui.rs/) to create a terminal GUI. +Since we need to edit text files, we'll use [tui-textarea](https://github.com/rhysd/tui-textarea), which is a text editor widget for Ratatui. +Ratatui works with multiple [backends](https://ratatui.rs/concepts/backends/), with [crossterm](https://crates.io/crates/crossterm) being the default backend since it is cross-platform. +Add these libraries as a dependency to `pie/Cargo.toml`: -[//]: # () -[//]: # (create `pie/examples/parser_dev/editor.rs`) +```diff2html linebyline +{{#include ../gen/4_example/d_1_Cargo.toml.diff}} +``` -[//]: # () -[//]: # (`Editor` `new` `draw_and_process_event` `run`) +### Ratatui Scaffolding -[//]: # () -[//]: # (add editor arg to `Cli` and run editor instead) +We will put the editor in a separate module, and start out with the basic scaffolding of a Ratatui "Hello World" application. +Add `editor` as a public module to `pie/examples/parser_dev/main.rs`: + +```diff2html linebyline +{{#include ../gen/4_example/d_2_main_editor.rs.diff}} +``` + +Create the `pie/examples/parser_dev/editor.rs` file and add the following to it: + +```rust, +{{#include d_3_editor.rs}} +``` + +The `Editor` struct will hold the state of the editor application, which is currently empty, but we'll add fields to it later. +Likewise, the `new` function doesn't do a lot right now, but it is scaffolding for when we add state. +It returns a `Result` because it can fail in the future. + +The `run` method sets up the terminal for GUI rendering, draws the GUI and processes events in a loop until stopped, and then undoes our changes to the terminal. +It is set up in such a way that undoing our changes to the terminal happens regardless if there is an error or not (although panics would still skip that code and leave the terminal in a bad state). +This is a [standard program loop for Ratatui](https://ratatui.rs/tutorial/hello-world/index.html). + +```admonish tip title="Rust Help: Returning From Loops" collapsible=true +A [`loop` indicates an infinite loop](https://doc.rust-lang.org/book/ch03-05-control-flow.html#repeating-code-with-loop). +You can [return a value from such loops with `break`](https://doc.rust-lang.org/book/ch03-05-control-flow.html#returning-values-from-loops). +``` + +The `draw_and_process_event` method first draws the GUI, currently just a hello world message, and then processes events such as key presses. +Currently, this skips key releases because we are only interested in presses, and returns `Ok(false)` if escape is pressed, causing the `loop` to be `break`ed out. + +Now we need to go back to our command-line argument parsing and add a flag indicating that we want to start up an interactive editor. +Modify `pie/examples/parser_dev/main.rs`: + +```diff2html +{{#include ../gen/4_example/d_4_main_cli.rs.diff}} +``` + +We add a new `Cli` struct with an `edit` field that is settable by a short (`-e`) or long (`--edit`) flag, and flatten `Args` into it. +Using this new `Cli` struct here keeps `Args` clean, since the existing code does not need to know about the `edit` flag. +Instead of using a flag, you could also define a [separate command](https://docs.rs/clap/latest/clap/_derive/_tutorial/chapter_0/index.html) for editing. + +In `main`, we parse `Cli` instead, check whether `cli.edit` is set, and create and run the editor if it is. +Otherwise, we do a batch build. + +Try out the code with `cargo run --example parser_dev -- test.pest main test_1.test test_2.test -e` in a terminal, which should open up a separate screen with a hello world text. +Press escape to exit out of the application. + +If the program ever panics, your terminal will be left in a bad state. +In that case, you'll have to reset your terminal back to a good state, or restart your terminal. + +### Text Editing [//]: # () [//]: # (run) diff --git a/stepper/src/app.rs b/stepper/src/app.rs index 7e39bfc..ab35b13 100644 --- a/stepper/src/app.rs +++ b/stepper/src/app.rs @@ -440,5 +440,12 @@ pub fn step_all( add("c_4_test_1.txt", "test_1.txt"), add("c_4_test_2.txt", "test_2.txt"), ]); + + stepper.apply([ + create_diff_from_destination_file("d_1_Cargo.toml", "pie/Cargo.toml"), + create_diff_from_destination_file("d_2_main_editor.rs", "pie/examples/parser_dev/main.rs"), + add("d_3_editor.rs", "pie/examples/parser_dev/editor.rs"), + create_diff_from_destination_file("d_4_main_cli.rs", "pie/examples/parser_dev/main.rs"), + ]); }); }