Skip to content

Commit

Permalink
Interactive parser development example writing.
Browse files Browse the repository at this point in the history
  • Loading branch information
Gohla committed Nov 22, 2023
1 parent 0e1d66f commit 3c07278
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/4_example/c_2_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>
program_file_paths: Vec<PathBuf>,
}

fn main() {
Expand Down
3 changes: 2 additions & 1 deletion src/4_example/c_3_compile_parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::fmt::Write;
use std::path::PathBuf;

use clap::Parser;

use pie::Pie;
use pie::tracker::writing::WritingTracker;

Expand All @@ -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<PathBuf>
program_file_paths: Vec<PathBuf>,
}

fn main() {
Expand Down
18 changes: 18 additions & 0 deletions src/4_example/d_1_Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
53 changes: 53 additions & 0 deletions src/4_example/d_2_main_editor.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf>,
}

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);
}
}
59 changes: 59 additions & 0 deletions src/4_example/d_3_editor.rs
Original file line number Diff line number Diff line change
@@ -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<Self, io::Error> {
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<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<bool, io::Error> {
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)
}
}
70 changes: 70 additions & 0 deletions src/4_example/d_4_main_cli.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf>,
}

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);
}
}
70 changes: 61 additions & 9 deletions src/4_example/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions stepper/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
]);
});
}

0 comments on commit 3c07278

Please sign in to comment.