From f90f93977dab39a3d485a339104158c80a31defa Mon Sep 17 00:00:00 2001 From: mattwparas Date: Sat, 9 Mar 2024 21:20:47 -0800 Subject: [PATCH] component api --- Cargo.lock | 58 +++- helix-term/src/commands/engine/components.rs | 341 ++++++++++++++----- helix-term/src/commands/engine/steel.rs | 10 + helix-tui/src/extension.rs | 10 +- helix-tui/src/widgets/list.rs | 16 +- helix-tui/src/widgets/mod.rs | 4 +- helix-view/src/extension.rs | 12 +- 7 files changed, 356 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c60de0093..67a4be1d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,27 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dunce" version = "1.0.4" @@ -1616,6 +1637,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1833,6 +1865,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.1" @@ -2037,6 +2075,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.3" @@ -2309,7 +2358,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "steel-core" version = "0.6.0" -source = "git+https://github.com/mattwparas/steel.git#a3843c2f4d20903a47d4b62162015c1e992df6ba" +source = "git+https://github.com/mattwparas/steel.git#4721a778ef87646333fd8d7ad37452be1d3dd988" dependencies = [ "abi_stable", "anyhow", @@ -2317,6 +2366,7 @@ dependencies = [ "bincode", "chrono", "codespan-reporting", + "dirs", "futures-executor", "futures-task", "futures-util", @@ -2346,7 +2396,7 @@ dependencies = [ [[package]] name = "steel-derive" version = "0.5.0" -source = "git+https://github.com/mattwparas/steel.git#a3843c2f4d20903a47d4b62162015c1e992df6ba" +source = "git+https://github.com/mattwparas/steel.git#4721a778ef87646333fd8d7ad37452be1d3dd988" dependencies = [ "proc-macro2", "quote", @@ -2356,7 +2406,7 @@ dependencies = [ [[package]] name = "steel-gen" version = "0.2.0" -source = "git+https://github.com/mattwparas/steel.git#a3843c2f4d20903a47d4b62162015c1e992df6ba" +source = "git+https://github.com/mattwparas/steel.git#4721a778ef87646333fd8d7ad37452be1d3dd988" dependencies = [ "codegen", "serde", @@ -2366,7 +2416,7 @@ dependencies = [ [[package]] name = "steel-parser" version = "0.6.0" -source = "git+https://github.com/mattwparas/steel.git#a3843c2f4d20903a47d4b62162015c1e992df6ba" +source = "git+https://github.com/mattwparas/steel.git#4721a778ef87646333fd8d7ad37452be1d3dd988" dependencies = [ "fxhash", "lasso", diff --git a/helix-term/src/commands/engine/components.rs b/helix-term/src/commands/engine/components.rs index 225e4e324..6f2e5e788 100644 --- a/helix-term/src/commands/engine/components.rs +++ b/helix-term/src/commands/engine/components.rs @@ -1,25 +1,23 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use helix_core::Position; use helix_view::{ graphics::{Color, CursorKind, Rect, UnderlineStyle}, - input::{Event, KeyEvent}, + input::{Event, KeyEvent, MouseEvent}, keyboard::{KeyCode, KeyModifiers}, theme::Style, Editor, }; use steel::{ - rvals::{Custom, FromSteelVal, IntoSteelVal}, - steel_vm::{ - builtin::BuiltInModule, - engine::Engine, - register_fn::{MarkerWrapper1, RegisterFn}, - }, + rvals::{Custom, FromSteelVal, IntoSteelVal, SteelString}, + steel_vm::{builtin::BuiltInModule, engine::Engine, register_fn::RegisterFn}, SteelVal, }; +use tokio::sync::Mutex; use tui::{ buffer::Buffer, - widgets::{Block, BorderType, Borders, Widget}, + text::Text, + widgets::{self, Block, BorderType, Borders, ListItem, Widget}, }; use crate::{ @@ -29,20 +27,115 @@ use crate::{ }, compositor::{self, Component}, ctrl, key, - ui::{overlay::overlaid, Popup, Prompt, PromptEvent}, + ui::overlay::overlaid, }; -use super::steel::WrappedDynComponent; +use super::steel::{present_error_inside_engine_context, WrappedDynComponent}; + +#[derive(Clone)] +struct AsyncReader { + // Take that, and write it back to a terminal session that is + // getting rendered. + channel: Arc>>, +} + +impl AsyncReader { + // TODO: Add &mut references to these async functions + // to avoid the cloning, and to ditch the arc and mutex + async fn read_line(self) -> Option { + let mut buf = String::new(); + + let mut guard = self.channel.lock().await; + + while let Ok(v) = guard.try_recv() { + buf.push_str(&v); + } + + let fut = guard.recv(); + + match tokio::time::timeout(std::time::Duration::from_millis(2), fut).await { + Ok(Some(v)) => { + buf.push_str(&v); + Some(buf) + } + Ok(None) => { + if buf.is_empty() { + None + } else { + Some(buf) + } + } + Err(_) => Some(buf), + } + } +} + +impl Custom for AsyncReader {} + +struct AsyncWriter { + channel: tokio::sync::mpsc::UnboundedSender, +} + +impl std::io::Write for AsyncWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if let Err(_) = self.channel.send(String::from_utf8_lossy(buf).to_string()) { + Ok(0) + } else { + Ok(buf.len()) + } + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} // TODO: Move the main configuration function to use this instead pub fn helix_component_module() -> BuiltInModule { - let mut module = BuiltInModule::new("helix/components".to_string()); + let mut module = BuiltInModule::new("helix/components"); module + .register_fn("async-read-line", AsyncReader::read_line) + // TODO: + .register_fn("make-async-reader-writer", || { + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); + + let writer = AsyncWriter { channel: sender }; + let reader = AsyncReader { + channel: Arc::new(Mutex::new(receiver)), + }; + + vec![ + SteelVal::new_dyn_writer_port(writer), + reader.into_steelval().unwrap(), + ] + }) + // Attempt to pop off a specific component + .register_fn( + "pop-dynamic-component-by-name", + |ctx: &mut Context, name: SteelString| { + // Removing a component by name here will be important! + todo!() + }, + ) + .register_fn("buffer-area", |buffer: &mut Buffer| buffer.area) .register_fn("frame-set-string!", buffer_set_string) + .register_fn("new-component!", SteelDynamicComponent::new_dyn) .register_fn("position", Position::new) .register_fn("position-row", |position: &Position| position.row) .register_fn("position-col", |position: &Position| position.col) + .register_fn( + "set-position-row!", + |position: &mut Position, row: usize| { + position.row = row; + }, + ) + .register_fn( + "set-position-col!", + |position: &mut Position, col: usize| { + position.col = col; + }, + ) .register_fn("area", helix_view::graphics::Rect::new) .register_fn("area-x", |area: &helix_view::graphics::Rect| area.x) .register_fn("area-y", |area: &helix_view::graphics::Rect| area.y) @@ -58,6 +151,19 @@ pub fn helix_component_module() -> BuiltInModule { component.inner = inner; }) + .register_fn("widget/list", |items: Vec| { + widgets::List::new( + items + .into_iter() + .map(|x| ListItem::new(Text::from(x))) + .collect::>(), + ) + }) + // Pass references in as well? + .register_fn( + "widget/list/render", + |buf: &mut Buffer, area: Rect, list: widgets::List| list.render(area, buf), + ) .register_fn("block", || { Block::default() .borders(Borders::ALL) @@ -71,6 +177,16 @@ pub fn helix_component_module() -> BuiltInModule { ) .register_fn("buffer/clear", Buffer::clear) .register_fn("buffer/clear-with", Buffer::clear_with) + // Mutate a color in place, to save some headache. + .register_fn( + "set-color-rgb!", + |color: &mut Color, r: u8, g: u8, b: u8| { + *color = Color::Rgb(r, g, b); + }, + ) + .register_fn("set-color-indexed!", |color: &mut Color, index: u8| { + *color = Color::Indexed(index); + }) .register_value("Color/Reset", Color::Reset.into_steelval().unwrap()) .register_value("Color/Black", Color::Black.into_steelval().unwrap()) .register_value("Color/Red", Color::Red.into_steelval().unwrap()) @@ -99,8 +215,14 @@ pub fn helix_component_module() -> BuiltInModule { .register_value("Color/LightGray", Color::LightGray.into_steelval().unwrap()) .register_fn("Color/rgb", Color::Rgb) .register_fn("Color/Indexed", Color::Indexed) + .register_fn("set-style-fg!", |style: &mut Style, color: Color| { + style.fg = Some(color); + }) .register_fn("style-fg", Style::fg) .register_fn("style-bg", Style::bg) + .register_fn("set-style-bg!", |style: &mut Style, color: Color| { + style.bg = Some(color); + }) .register_fn("style-underline-color", Style::underline_color) .register_fn("style-underline-style", Style::underline_style) .register_value( @@ -167,39 +289,91 @@ pub fn helix_component_module() -> BuiltInModule { "key-modifier-alt", SteelVal::IntV(KeyModifiers::ALT.bits() as isize), ) - .register_fn("key-event-escape?", |event: Event| { - if let Event::Key(KeyEvent { - code: KeyCode::Esc, .. - }) = event - { - true + .register_fn("key-event-F?", |event: Event, number: u8| match event { + Event::Key(KeyEvent { + code: KeyCode::F(x), + .. + }) if number == x => true, + _ => false, + }) + .register_fn("mouse-event?", |event: Event| { + matches!(event, Event::Mouse(_)) + }) + .register_fn("event-mouse-kind", |event: Event| { + if let Event::Mouse(MouseEvent { .. }) = event { + todo!() } else { - false + todo!() } }) - .register_fn("key-event-backspace?", |event: Event| { - if let Event::Key(KeyEvent { - code: KeyCode::Backspace, - .. - }) = event - { - true + .register_fn("event-mouse-row", |event: Event| { + if let Event::Mouse(MouseEvent { .. }) = event { + todo!() } else { - false + todo!() } }) - .register_fn("key-event-enter?", |event: Event| { - if let Event::Key(KeyEvent { - code: KeyCode::Enter, - .. - }) = event - { - true + .register_fn("event-mouse-col", |event: Event| { + if let Event::Mouse(MouseEvent { .. }) = event { + todo!() + } else { + todo!() + } + }) + // Is this mouse event within the area provided + .register_fn("mouse-event-within-area?", |event: Event, area: Rect| { + if let Event::Mouse(MouseEvent { row, column, .. }) = event { + column > area.x + && column < area.x + area.width + && row > area.y + && row < area.y + area.height } else { false } }); + macro_rules! register_key_events { + ($ ( $name:expr => $key:tt ) , *, ) => { + $( + module.register_fn(concat!("key-event-", $name, "?"), |event: Event| { + matches!( + event, + Event::Key( + KeyEvent { + code: KeyCode::$key, + .. + } + )) + }); + )* + }; + } + + // Key events for individual key codes + register_key_events!( + "escape" => Esc, + "backspace" => Backspace, + "enter" => Enter, + "left" => Left, + "right" => Right, + "up" => Up, + "down" => Down, + "home" => Home, + "page-up" => PageUp, + "page-down" => PageDown, + "tab" => Tab, + "delete" => Delete, + "insert" => Insert, + "null" => Null, + "caps-lock" => CapsLock, + "scroll-lock" => ScrollLock, + "num-lock" => NumLock, + "print-screen" => PrintScreen, + "pause" => Pause, + "menu" => Menu, + "keypad-begin" => KeypadBegin, + ); + module } @@ -325,29 +499,32 @@ impl Component for SteelDynamicComponent { // Pass the `state` object through - this can be used for storing the state of whatever plugin thing we're // attempting to render let thunk = |engine: &mut Engine, f| { - engine.call_function_with_args( + engine.call_function_with_args_from_mut_slice( self.render.clone(), - vec![self.state.clone(), area.into_steelval().unwrap(), f], + &mut [self.state.clone(), area.into_steelval().unwrap(), f], ) }; - ENGINE - .with(|x| { - x.borrow_mut() - .with_mut_reference::(frame) - .with_mut_reference::(&mut ctx) - .consume(|engine, args| { - let mut arg_iter = args.into_iter(); + ENGINE.with(|x| { + let mut guard = x.borrow_mut(); - let buffer = arg_iter.next().unwrap(); - let context = arg_iter.next().unwrap(); + if let Err(e) = guard + .with_mut_reference::(frame) + .with_mut_reference::(&mut ctx) + .consume(|engine, args| { + let mut arg_iter = args.into_iter(); - engine.update_value("*helix.cx*", context); + let buffer = arg_iter.next().unwrap(); + let context = arg_iter.next().unwrap(); - (thunk)(engine, buffer) - }) - }) - .unwrap(); + engine.update_value("*helix.cx*", context); + + (thunk)(engine, buffer) + }) + { + present_error_inside_engine_context(&mut ctx, &mut guard, e) + } + }) } // TODO: Pass in event as well? Need to have immutable reference type @@ -368,8 +545,6 @@ impl Component for SteelDynamicComponent { jobs: ctx.jobs, }; - log::info!("Handling custom event: {:?}", event); - match self.key_event.as_mut() { Some(SteelVal::Custom(key_event)) => { // Save the headache, reuse the allocation @@ -391,9 +566,9 @@ impl Component for SteelDynamicComponent { // Pass the `state` object through - this can be used for storing the state of whatever plugin thing we're // attempting to render let thunk = |engine: &mut Engine| { - engine.call_function_with_args( + engine.call_function_with_args_from_mut_slice( handle_event.clone(), - vec![self.state.clone(), self.key_event.clone().unwrap()], + &mut [self.state.clone(), self.key_event.clone().unwrap()], ) }; @@ -432,7 +607,14 @@ impl Component for SteelDynamicComponent { }, } } - Err(_) => compositor::EventResult::Ignored(None), + Err(e) => { + // Present the error + ENGINE.with(|x| { + present_error_inside_engine_context(&mut ctx, &mut x.borrow_mut(), e) + }); + + compositor::EventResult::Ignored(None) + } } } else { compositor::EventResult::Ignored(None) @@ -440,20 +622,22 @@ impl Component for SteelDynamicComponent { } fn should_update(&self) -> bool { - if let Some(should_update) = &self.should_update { - match ENGINE.with(|x| { - let res = x - .borrow_mut() - .call_function_with_args(should_update.clone(), vec![self.state.clone()]); - - res - }) { - Ok(v) => bool::from_steelval(&v).unwrap_or(true), - Err(_) => true, - } - } else { - true - } + true + + // if let Some(should_update) = &self.should_update { + // match ENGINE.with(|x| { + // let res = x + // .borrow_mut() + // .call_function_with_args(should_update.clone(), vec![self.state.clone()]); + + // res + // }) { + // Ok(v) => bool::from_steelval(&v).unwrap_or(true), + // Err(_) => true, + // } + // } else { + // true + // } } // TODO: Implement immutable references. Right now I'm only supporting mutable references. @@ -465,7 +649,6 @@ impl Component for SteelDynamicComponent { Option, helix_view::graphics::CursorKind, ) { - log::info!("Calling cursor with area: {:?}", area); if let Some(cursor) = &self.cursor { // Pass the `state` object through - this can be used for storing the state of whatever plugin thing we're // attempting to render @@ -480,9 +663,11 @@ impl Component for SteelDynamicComponent { &ENGINE.with(|x| thunk(&mut (x.borrow_mut())).unwrap()), ); - log::info!("Setting cursor at position: {:?}", result); - - (result.unwrap(), CursorKind::Block) + match result { + Ok(v) => (v, CursorKind::Block), + // TODO: Figure out how to pop up an error message + Err(_e) => (None, CursorKind::Block), + } } else { (None, helix_view::graphics::CursorKind::Hidden) } @@ -504,7 +689,7 @@ impl Component for SteelDynamicComponent { // re-entrant attempting to grab the ENGINE instead mutably, since we have to break the recursion // somehow. By putting it at the edge, we then say - hey for these functions on this interface, // call the engine instance. Otherwise, all computation happens inside the engine. - let res = ENGINE + match ENGINE .with(|x| { x.borrow_mut().call_function_with_args( required_size.clone(), @@ -512,9 +697,11 @@ impl Component for SteelDynamicComponent { ) }) .and_then(|x| Option::<(u16, u16)>::from_steelval(&x)) - .unwrap(); - - res + { + Ok(v) => v, + // TODO: Figure out how to present an error + Err(_e) => None, + } } else { None } diff --git a/helix-term/src/commands/engine/steel.rs b/helix-term/src/commands/engine/steel.rs index 5d21b80b5..c5cce26fb 100644 --- a/helix-term/src/commands/engine/steel.rs +++ b/helix-term/src/commands/engine/steel.rs @@ -2286,6 +2286,16 @@ fn push_component(cx: &mut Context, component: &mut WrappedDynComponent) { cx.jobs.local_callback(callback); } +fn render(cx: &mut Context) { + let callback = async move { + let call: Box = Box::new( + move |_editor: &mut Editor, _compositor: &mut Compositor, _jobs: &mut job::Jobs| {}, + ); + Ok(call) + }; + cx.jobs.local_callback(callback); +} + fn enqueue_command(cx: &mut Context, callback_fn: SteelVal) { let rooted = callback_fn.as_rooted(); diff --git a/helix-tui/src/extension.rs b/helix-tui/src/extension.rs index ad14a6c2b..c046137ce 100644 --- a/helix-tui/src/extension.rs +++ b/helix-tui/src/extension.rs @@ -1,12 +1,20 @@ #[cfg(feature = "steel")] mod steel_implementations { - use crate::{buffer::Buffer, widgets::Block}; + use crate::{ + buffer::Buffer, + text::Text, + widgets::{Block, List, Paragraph, Table}, + }; use steel::{gc::unsafe_erased_pointers::CustomReference, rvals::Custom}; impl CustomReference for Buffer {} impl Custom for Block<'static> {} + impl Custom for List<'static> {} + impl Custom for Paragraph<'static> {} + impl Custom for Table<'static> {} + impl Custom for Text<'static> {} steel::custom_reference!(Buffer); } diff --git a/helix-tui/src/widgets/list.rs b/helix-tui/src/widgets/list.rs index e913ce788..590530793 100644 --- a/helix-tui/src/widgets/list.rs +++ b/helix-tui/src/widgets/list.rs @@ -1,12 +1,12 @@ use crate::{ buffer::Buffer, - layout::{Corner, Rect}, - style::Style, + layout::Corner, text::Text, - widgets::{Block, StatefulWidget, Widget}, + widgets::{Block, Widget}, }; +use helix_core::unicode::width::UnicodeWidthStr; +use helix_view::graphics::{Rect, Style}; use std::iter::{self, Iterator}; -use unicode_width::UnicodeWidthStr; #[derive(Debug, Clone)] pub struct ListState { @@ -131,10 +131,8 @@ impl<'a> List<'a> { } } -impl<'a> StatefulWidget for List<'a> { - type State = ListState; - - fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { +impl<'a> List<'a> { + fn render_list(mut self, area: Rect, buf: &mut Buffer, state: &mut ListState) { buf.set_style(area, self.style); let list_area = match self.block.take() { Some(b) => { @@ -244,6 +242,6 @@ impl<'a> StatefulWidget for List<'a> { impl<'a> Widget for List<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let mut state = ListState::default(); - StatefulWidget::render(self, area, buf, &mut state); + Self::render_list(self, area, buf, &mut state); } } diff --git a/helix-tui/src/widgets/mod.rs b/helix-tui/src/widgets/mod.rs index 3a0dfc5d8..7145ec678 100644 --- a/helix-tui/src/widgets/mod.rs +++ b/helix-tui/src/widgets/mod.rs @@ -10,13 +10,13 @@ //! - [`Paragraph`] mod block; -// mod list; +mod list; mod paragraph; mod reflow; mod table; pub use self::block::{Block, BorderType}; -// pub use self::list::{List, ListItem, ListState}; +pub use self::list::{List, ListItem, ListState}; pub use self::paragraph::{Paragraph, Wrap}; pub use self::table::{Cell, Row, Table, TableState}; diff --git a/helix-view/src/extension.rs b/helix-view/src/extension.rs index e3680b7a7..20393fcd5 100644 --- a/helix-view/src/extension.rs +++ b/helix-view/src/extension.rs @@ -26,8 +26,16 @@ mod steel_implementations { impl steel::rvals::Custom for Mode {} impl steel::rvals::Custom for Event {} - impl Custom for Style {} - impl Custom for Color {} + impl Custom for Style { + fn fmt(&self) -> Option> { + Some(Ok(format!("{:?}", self))) + } + } + impl Custom for Color { + fn fmt(&self) -> Option> { + Some(Ok(format!("{:?}", self))) + } + } impl Custom for UnderlineStyle {} impl CustomReference for Event {}