-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Interactive parser development example writing.
- Loading branch information
Showing
6 changed files
with
344 additions
and
9 deletions.
There are no files selected for viewing
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
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; | ||
|
||
mod buffer; | ||
|
||
/// 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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
#![allow(dead_code)] | ||
|
||
use std::fs::{File, read_to_string}; | ||
use std::io::{self, Write}; | ||
use std::path::PathBuf; | ||
|
||
use crossterm::event::Event; | ||
use ratatui::Frame; | ||
use ratatui::layout::{Constraint, Direction, Layout, Rect}; | ||
use ratatui::style::{Color, Modifier, Style}; | ||
use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; | ||
use tui_textarea::TextArea; | ||
|
||
/// Editable text buffer for a file. | ||
pub struct Buffer { | ||
path: PathBuf, | ||
editor: TextArea<'static>, | ||
feedback: String, | ||
modified: bool, | ||
} | ||
|
||
impl Buffer { | ||
/// Create a new [`Buffer`] for file at `path`. | ||
/// | ||
/// # Errors | ||
/// | ||
/// Returns an error when reading file at `path` fails. | ||
pub fn new(path: PathBuf) -> Result<Self, io::Error> { | ||
let text = read_to_string(&path)?; | ||
let mut editor = TextArea::from(text.lines()); | ||
|
||
// Enable line numbers. Default style = no additional styling (inherit). | ||
editor.set_line_number_style(Style::default()); | ||
|
||
Ok(Self { path, editor, feedback: String::default(), modified: false }) | ||
} | ||
|
||
/// Draws this buffer with `frame` into `area`, highlighting it if it is `active`. | ||
pub fn draw(&mut self, frame: &mut Frame, area: Rect, active: bool) { | ||
// Determine and set styles based on whether this buffer is active. Default style = no additional styling (inherit). | ||
let mut cursor_line_style = Style::default(); | ||
let mut cursor_style = Style::default(); | ||
let mut block_style = Style::default(); | ||
if active { // Highlight active editor. | ||
cursor_line_style = cursor_line_style.add_modifier(Modifier::UNDERLINED); | ||
cursor_style = cursor_style.add_modifier(Modifier::REVERSED); | ||
block_style = block_style.fg(Color::Gray); | ||
} | ||
self.editor.set_cursor_line_style(cursor_line_style); | ||
self.editor.set_cursor_style(cursor_style); | ||
|
||
// Create and set the block for the text editor, bordering it and providing a title. | ||
let mut block = Block::default().borders(Borders::ALL).style(block_style); | ||
if let Some(file_name) = self.path.file_name() { // Add file name as title. | ||
block = block.title(format!("{}", file_name.to_string_lossy())) | ||
} | ||
if self.modified { // Add modified to title. | ||
block = block.title("[modified]"); | ||
} | ||
self.editor.set_block(block); | ||
|
||
// Split area up into a text editor (80% of available space), and feedback text (minimum of 7 lines). | ||
let areas = Layout::default() | ||
.direction(Direction::Vertical) | ||
.constraints(vec![Constraint::Percentage(80), Constraint::Min(7)]) | ||
.split(area); | ||
// Render text editor into first area (`areas[0]`). | ||
frame.render_widget(self.editor.widget(), areas[0]); | ||
// Render feedback text into second area (`areas[1]`). | ||
let feedback = Paragraph::new(self.feedback.clone()) | ||
.wrap(Wrap::default()) | ||
.block(Block::default().style(block_style).borders(Borders::ALL - Borders::TOP)); | ||
frame.render_widget(feedback, areas[1]); | ||
} | ||
|
||
/// Process `event`, updating whether this buffer is modified. | ||
pub fn process_event(&mut self, event: Event) { | ||
self.modified |= self.editor.input(event); | ||
} | ||
|
||
/// Save this buffer to its file if it is modified. Does nothing if not modified. Sets as unmodified when successful. | ||
/// | ||
/// # Errors | ||
/// | ||
/// Returns an error if writing buffer text to the file fails. | ||
pub fn save_if_modified(&mut self) -> Result<(), io::Error> { | ||
if !self.modified { | ||
return Ok(()); | ||
} | ||
let mut file = io::BufWriter::new(File::create(&self.path)?); | ||
for line in self.editor.lines() { | ||
file.write_all(line.as_bytes())?; | ||
file.write_all(b"\n")?; | ||
} | ||
file.flush()?; | ||
self.modified = false; | ||
Ok(()) | ||
} | ||
|
||
/// Gets the file path of this buffer. | ||
pub fn path(&self) -> &PathBuf { &self.path } | ||
|
||
/// Gets the mutable feedback text of this buffer. | ||
pub fn feedback_mut(&mut self) -> &mut String { &mut self.feedback } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
use std::io; | ||
|
||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers}; | ||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; | ||
use ratatui::backend::{Backend, CrosstermBackend}; | ||
use ratatui::layout::{Constraint, Direction, Layout}; | ||
use ratatui::Terminal; | ||
use ratatui::widgets::Paragraph; | ||
|
||
use crate::Args; | ||
use crate::editor::buffer::Buffer; | ||
|
||
mod buffer; | ||
|
||
/// Live parser development editor. | ||
pub struct Editor { | ||
buffers: Vec<Buffer>, | ||
active_buffer: usize, | ||
} | ||
|
||
impl Editor { | ||
/// Create a new editor from `args`. | ||
/// | ||
/// # Errors | ||
/// | ||
/// Returns an error when creating a buffer fails. | ||
pub fn new(args: Args) -> Result<Self, io::Error> { | ||
let mut buffers = Vec::with_capacity(1 + args.program_file_paths.len()); | ||
buffers.push(Buffer::new(args.grammar_file_path)?); // First buffer is always the grammar buffer. | ||
for path in args.program_file_paths { | ||
buffers.push(Buffer::new(path)?); // Subsequent buffers are always example program buffers. | ||
} | ||
|
||
let editor = Self { buffers, active_buffer: 0, }; | ||
Ok(editor) | ||
} | ||
|
||
/// 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| { | ||
let root_areas = Layout::default() | ||
.direction(Direction::Vertical) | ||
.constraints(vec![Constraint::Percentage(100), Constraint::Min(1)]) | ||
.split(frame.size()); | ||
let buffer_areas = Layout::default() | ||
.direction(Direction::Horizontal) | ||
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)]) | ||
.split(root_areas[0]); | ||
|
||
// Draw grammar buffer on the left (`buffer_areas[0]`). | ||
self.buffers[0].draw(frame, buffer_areas[0], self.active_buffer == 0); | ||
|
||
{ // Draw example program buffers on the right (`buffer_areas[1]`). | ||
let num_program_buffers = self.buffers.len() - 1; | ||
// Split vertical space between example program buffers. | ||
let program_buffer_areas = Layout::default() | ||
.direction(Direction::Vertical) | ||
.constraints(vec![Constraint::Ratio(1, num_program_buffers as u32); num_program_buffers]) | ||
.split(buffer_areas[1]); | ||
for ((buffer, area), i) in self.buffers[1..].iter_mut().zip(program_buffer_areas.iter()).zip(1..) { | ||
buffer.draw(frame, *area, self.active_buffer == i); | ||
} | ||
} | ||
|
||
// Draw help line on the last line (`root_areas[1]`). | ||
let help = Paragraph::new("Interactive Parser Development. Press Esc to quit, ^T to switch active \ | ||
buffer."); | ||
frame.render_widget(help, root_areas[1]); | ||
})?; | ||
|
||
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), | ||
Event::Key(key) if key.code == KeyCode::Char('t') && key.modifiers.contains(KeyModifiers::CONTROL) => { | ||
self.active_buffer = (self.active_buffer + 1) % self.buffers.len(); | ||
} | ||
event => self.buffers[self.active_buffer].process_event(event), // Otherwise: forward to current buffer. | ||
}; | ||
|
||
Ok(true) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters