From 28b9324722810e0318d3e9080b8a592ede846c2f Mon Sep 17 00:00:00 2001 From: Michael Benfield Date: Fri, 6 Dec 2024 09:49:41 -0800 Subject: [PATCH] preliminary ratatui interface --- Cargo.lock | 213 ++++++++++++++++++- interpreter/Cargo.toml | 9 + interpreter/src/interpreter.rs | 4 +- interpreter/src/lib.rs | 7 +- interpreter/src/ratatui_ui.rs | 372 +++++++++++++++++++++++++++++++++ 5 files changed, 596 insertions(+), 9 deletions(-) create mode 100644 interpreter/src/ratatui_ui.rs diff --git a/Cargo.lock b/Cargo.lock index 95a4f21ee3..247dd86f57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -362,12 +362,27 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.0.98" @@ -482,6 +497,20 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "console" version = "0.15.8" @@ -491,7 +520,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.12", "windows-sys 0.52.0", ] @@ -700,6 +729,41 @@ dependencies = [ "syn 2.0.82", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote 1.0.36", + "strsim", + "syn 2.0.82", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote 1.0.36", + "syn 2.0.82", +] + [[package]] name = "der" version = "0.7.9" @@ -754,6 +818,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -1079,7 +1149,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" dependencies = [ - "unicode-width", + "unicode-width 0.1.12", ] [[package]] @@ -1365,6 +1435,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1407,7 +1483,27 @@ dependencies = [ "instant", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.1.12", +] + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "instability" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b829f37dead9dc39df40c2d3376c179fdfd2ac771f53f55d3c30dc096a3c0c6e" +dependencies = [ + "darling", + "indoc", + "pretty_assertions", + "proc-macro2", + "quote 1.0.36", + "syn 2.0.82", ] [[package]] @@ -1566,6 +1662,7 @@ name = "leo-interpreter" version = "2.4.0" dependencies = [ "colored", + "crossterm", "dialoguer", "indexmap 2.6.0", "leo-ast", @@ -1577,12 +1674,14 @@ dependencies = [ "leo-test-framework", "rand", "rand_chacha", + "ratatui", "serial_test", "snarkvm", "snarkvm-circuit", "snarkvm-synthesizer-program", "tempfile", "toml 0.8.19", + "tui-input", ] [[package]] @@ -2155,11 +2254,21 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" -version = "1.0.84" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2227,6 +2336,27 @@ dependencies = [ "rand_core", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.5.0", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -2495,6 +2625,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "rusty-hook" version = "0.11.2" @@ -3741,12 +3877,40 @@ dependencies = [ "der", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote 1.0.36", + "rustversion", + "syn 2.0.82", +] + [[package]] name = "subtle" version = "2.5.0" @@ -4165,6 +4329,16 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-input" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1733c47f1a217b7deff18730ff7ca4ecafc5771368f715ab072d679a36114" +dependencies = [ + "ratatui", + "unicode-width 0.2.0", +] + [[package]] name = "typenum" version = "1.17.0" @@ -4192,12 +4366,35 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.12", +] + [[package]] name = "unicode-width" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unicode-xid" version = "0.0.4" @@ -4607,6 +4804,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.34" diff --git a/interpreter/Cargo.toml b/interpreter/Cargo.toml index 85a9b5877a..fd58c12e7d 100644 --- a/interpreter/Cargo.toml +++ b/interpreter/Cargo.toml @@ -48,6 +48,9 @@ workspace = true [dependencies.colored] workspace = true +[dependencies.crossterm] +version = "0.28.1" + [dependencies.indexmap] workspace = true @@ -61,9 +64,15 @@ workspace = true [dependencies.rand_chacha] workspace = true +[dependencies.ratatui] +version = "0.29.0" + [dependencies.toml] workspace = true +[dependencies.tui-input] +version = "0.11.1" + [dev-dependencies.leo-test-framework] path = "../tests/test-framework" diff --git a/interpreter/src/interpreter.rs b/interpreter/src/interpreter.rs index f2b1a06267..efc7f51178 100644 --- a/interpreter/src/interpreter.rs +++ b/interpreter/src/interpreter.rs @@ -269,8 +269,8 @@ impl Interpreter { s, source_file.start_pos, ) - .map_err(|_e| { - LeoError::InterpreterHalt(InterpreterHalt::new("failed to parse expression".into())) + .map_err(|e| { + LeoError::InterpreterHalt(InterpreterHalt::new(format!("Failed to parse expression: {e}"))) })?; // TODO: This leak is silly. let expr = Box::leak(Box::new(expression)); diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index aee0a59586..da5a0b426a 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -49,6 +49,8 @@ use ui::Ui as _; mod dialoguer_input; +mod ratatui_ui; + const INTRO: &str = "This is the Leo Interpreter. Try the command `#help`."; const HELP: &str = " @@ -126,7 +128,8 @@ pub fn interpret( ) -> Result<()> { let mut interpreter = Interpreter::new(leo_filenames.iter(), aleo_filenames.iter(), signer, block_height)?; - let mut user_interface = dialoguer_input::DialoguerUi::new(); + // let mut user_interface = dialoguer_input::DialoguerUi::new(); + let mut user_interface = ratatui_ui::RatatuiUi::new(); let mut code = String::new(); let mut futures = Vec::new(); @@ -152,7 +155,7 @@ pub fn interpret( interpreter.update_watchpoints()?; watchpoints.extend(interpreter.watchpoints.iter().map(|watchpoint| { - format!("{:>20} = {}", watchpoint.code, if let Some(s) = &watchpoint.last_result { &**s } else { "?" }) + format!("{:>15} = {}", watchpoint.code, if let Some(s) = &watchpoint.last_result { &**s } else { "?" }) })); let user_data = ui::UserData { diff --git a/interpreter/src/ratatui_ui.rs b/interpreter/src/ratatui_ui.rs new file mode 100644 index 0000000000..3dfd4db746 --- /dev/null +++ b/interpreter/src/ratatui_ui.rs @@ -0,0 +1,372 @@ +// Copyright (C) 2019-2024 Aleo Systems Inc. +// This file is part of the Leo library. + +// The Leo library is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The Leo library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with the Leo library. If not, see . + +use super::ui::{Ui, UserData}; + +use std::{cmp, collections::VecDeque, io::Stdout, mem}; + +use crossterm::event::{self, Event, KeyCode, KeyModifiers}; +use ratatui::{ + Frame, + Terminal, + prelude::{ + Buffer, + Constraint, + CrosstermBackend, + Direction, + Layout, + Line, + Modifier, + Rect, + Span, + Style, + Stylize as _, + }, + text::Text, + widgets::{Block, Paragraph, Widget}, +}; + +#[derive(Default)] +struct DrawData { + code: String, + highlight: Option<(usize, usize)>, + result: String, + watchpoints: Vec, + message: String, + prompt: Prompt, +} + +pub struct RatatuiUi { + terminal: Terminal>, + data: DrawData, +} + +impl Drop for RatatuiUi { + fn drop(&mut self) { + ratatui::restore(); + } +} + +impl RatatuiUi { + pub fn new() -> Self { + RatatuiUi { terminal: ratatui::init(), data: Default::default() } + } +} + +fn append_lines<'a>( + lines: &mut Vec>, + mut last_chunk: Option>, + string: &'a str, + style: Style, +) -> Option> { + let mut line_iter = string.lines().peekable(); + while let Some(line) = line_iter.next() { + let this_span = Span::styled(line, style); + let mut real_last_chunk = mem::take(&mut last_chunk).unwrap_or_else(|| Line::raw("")); + real_last_chunk.push_span(this_span); + if line_iter.peek().is_some() { + lines.push(real_last_chunk); + } else { + if string.ends_with('\n') { + lines.push(real_last_chunk); + return None; + } else { + return Some(real_last_chunk); + } + } + } + + last_chunk +} + +fn code_text(s: &str, highlight: Option<(usize, usize)>) -> (Text, usize) { + let Some((lo, hi)) = highlight else { + return (Text::from(s), 0); + }; + + let s1 = s.get(..lo).expect("should be able to split text"); + let s2 = s.get(lo..hi).expect("should be able to split text"); + let s3 = s.get(hi..).expect("should be able to split text"); + + let mut lines = Vec::new(); + + let s1_chunk = append_lines(&mut lines, None, s1, Style::default()); + let line = lines.len(); + let s2_chunk = append_lines(&mut lines, s1_chunk, s2, Style::new().red()); + let s3_chunk = append_lines(&mut lines, s2_chunk, s3, Style::default()); + + if let Some(chunk) = s3_chunk { + lines.push(chunk); + } + + (Text::from(lines), line) +} + +struct DebuggerLayout { + code: Rect, + result: Rect, + watchpoints: Rect, + user_input: Rect, + message: Rect, +} + +impl DebuggerLayout { + fn new(total: Rect) -> Self { + let overall_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), // Code + Constraint::Length(6), // Result and watchpoints + Constraint::Length(3), // Message + Constraint::Length(3), // User input + ]) + .split(total); + let code = overall_layout[0]; + let middle = overall_layout[1]; + let message = overall_layout[2]; + let user_input = overall_layout[3]; + + let middle = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(middle); + + DebuggerLayout { code, result: middle[0], watchpoints: middle[1], user_input, message } + } +} + +#[derive(Debug, Default)] +struct Prompt { + history: VecDeque, + history_index: usize, + current: String, + cursor: usize, +} + +impl<'a> Widget for &'a Prompt { + fn render(self, area: Rect, buf: &mut Buffer) { + let mut plain = || { + Text::raw(&self.current).render(area, buf); + }; + + if self.cursor >= self.current.len() { + let span1 = Span::raw(&self.current); + let span2 = Span::styled(" ", Style::new().add_modifier(Modifier::REVERSED)); + Text::from(Line::from_iter([span1, span2])).render(area, buf); + return; + } + + let Some(pre) = self.current.get(..self.cursor) else { + plain(); + return; + }; + + let Some(c) = self.current.get(self.cursor..self.cursor + 1) else { + plain(); + return; + }; + + let Some(post) = self.current.get(self.cursor + 1..) else { + plain(); + return; + }; + + Text::from(Line::from_iter([ + Span::raw(pre), + Span::styled(c, Style::new().add_modifier(Modifier::REVERSED)), + Span::raw(post), + ])) + .render(area, buf); + } +} + +impl Prompt { + fn handle_key(&mut self, key: KeyCode, control: bool) -> Option { + match (key, control) { + (KeyCode::Enter, _) => { + self.history.push_back(mem::take(&mut self.current)); + self.history_index = self.history.len(); + return self.history.back().cloned(); + } + (KeyCode::Backspace, _) => self.backspace(), + (KeyCode::Left, _) => self.left(), + (KeyCode::Right, _) => self.right(), + (KeyCode::Up, _) => self.history_prev(), + (KeyCode::Down, _) => self.history_next(), + (KeyCode::Delete, _) => self.delete(), + (KeyCode::Char(c), false) => self.new_character(c), + (KeyCode::Char('a'), true) => self.beginning_of_line(), + (KeyCode::Char('e'), true) => self.end_of_line(), + _ => {} + } + + None + } + + fn new_character(&mut self, c: char) { + if self.cursor >= self.current.len() { + self.current.push(c); + self.cursor = self.current.len(); + } else { + let Some(pre) = self.current.get(..self.cursor) else { + return; + }; + let Some(post) = self.current.get(self.cursor..) else { + return; + }; + let mut with_char = format!("{pre}{c}"); + self.cursor = with_char.len(); + with_char.push_str(post); + self.current = with_char; + } + self.check_history(); + } + + fn right(&mut self) { + self.cursor = cmp::min(self.cursor + 1, self.current.len()); + } + + fn left(&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + + fn backspace(&mut self) { + if self.cursor == 0 { + return; + } + + if self.cursor >= self.current.len() { + self.current.pop(); + self.cursor = self.current.len(); + return; + } + + let Some(pre) = self.current.get(..self.cursor - 1) else { + return; + }; + let Some(post) = self.current.get(self.cursor..) else { + return; + }; + self.cursor -= 1; + + let s = format!("{pre}{post}"); + + self.current = s; + + self.check_history(); + } + + fn delete(&mut self) { + if self.cursor + 1 >= self.current.len() { + return; + } + + let Some(pre) = self.current.get(..self.cursor) else { + return; + }; + let Some(post) = self.current.get(self.cursor + 1..) else { + return; + }; + + let s = format!("{pre}{post}"); + + self.current = s; + + self.check_history(); + } + + fn beginning_of_line(&mut self) { + self.cursor = 0; + } + + fn end_of_line(&mut self) { + self.cursor = self.current.len(); + } + + fn history_next(&mut self) { + self.history_index += 1; + if self.history_index > self.history.len() { + self.history_index = 0; + } + self.current = self.history.get(self.history_index).cloned().unwrap_or(String::new()); + } + + fn history_prev(&mut self) { + if self.history_index == 0 { + self.history_index = self.history.len(); + } else { + self.history_index -= 1; + } + self.current = self.history.get(self.history_index).cloned().unwrap_or(String::new()); + } + + fn check_history(&mut self) { + const MAX_HISTORY: usize = 50; + + while self.history.len() > MAX_HISTORY { + self.history.pop_front(); + } + + self.history_index = self.history.len(); + } +} + +fn render_titled(frame: &mut Frame, widget: W, title: &str, area: Rect) { + let block = Block::bordered().title(title); + frame.render_widget(widget, block.inner(area)); + frame.render_widget(block, area); +} + +impl DrawData { + fn draw(&mut self, frame: &mut Frame) { + let layout = DebuggerLayout::new(frame.area()); + + let (code, line) = code_text(&self.code, self.highlight); + let p = Paragraph::new(code).scroll((line.saturating_sub(4) as u16, 0)); + render_titled(frame, p, "code", layout.code); + + render_titled(frame, Text::raw(&self.result), "Result", layout.result); + + render_titled(frame, Text::from_iter(self.watchpoints.iter().map(|s| &**s)), "Watchpoints", layout.watchpoints); + + render_titled(frame, Text::raw(&self.message), "Message", layout.message); + + render_titled(frame, &self.prompt, "Command:", layout.user_input); + } +} + +impl Ui for RatatuiUi { + fn display_user_data(&mut self, data: &UserData<'_>) { + self.data.code = data.code.to_string(); + self.data.highlight = data.highlight; + self.data.result = data.result.map(|s| s.to_string()).unwrap_or(String::new()); + self.data.watchpoints.clear(); + self.data.watchpoints.extend(data.watchpoints.iter().enumerate().map(|(i, s)| format!("{i:>2} {s}"))); + self.data.message = data.message.to_string(); + } + + fn receive_user_input(&mut self) -> String { + loop { + self.terminal.draw(|frame| self.data.draw(frame)).expect("failed to draw frame"); + if let Event::Key(key_event) = event::read().expect("event") { + let control = key_event.modifiers.contains(KeyModifiers::CONTROL); + if let Some(string) = self.data.prompt.handle_key(key_event.code, control) { + return string; + } + } + } + } +}