From e552cc03b8bc6ddfee6142b94b3de1aa94ca1e7e Mon Sep 17 00:00:00 2001 From: mattwparas Date: Wed, 21 Feb 2024 22:51:44 -0800 Subject: [PATCH] Continue to expand on component API --- Cargo.lock | 9 +- helix-term/src/commands/engine/components.rs | 403 ++++++++++++++----- helix-term/src/commands/engine/steel.rs | 36 +- helix-term/src/compositor.rs | 4 +- helix-tui/src/extension.rs | 3 +- helix-view/src/extension.rs | 5 +- 6 files changed, 323 insertions(+), 137 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd5f7c0c0..c60de0093 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2309,7 +2309,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "steel-core" version = "0.6.0" -source = "git+https://github.com/mattwparas/steel.git#01e42f435de3f6ca8cf083fdfc422d2a445d5bc9" +source = "git+https://github.com/mattwparas/steel.git#a3843c2f4d20903a47d4b62162015c1e992df6ba" dependencies = [ "abi_stable", "anyhow", @@ -2317,6 +2317,7 @@ dependencies = [ "bincode", "chrono", "codespan-reporting", + "futures-executor", "futures-task", "futures-util", "fxhash", @@ -2345,7 +2346,7 @@ dependencies = [ [[package]] name = "steel-derive" version = "0.5.0" -source = "git+https://github.com/mattwparas/steel.git#01e42f435de3f6ca8cf083fdfc422d2a445d5bc9" +source = "git+https://github.com/mattwparas/steel.git#a3843c2f4d20903a47d4b62162015c1e992df6ba" dependencies = [ "proc-macro2", "quote", @@ -2355,7 +2356,7 @@ dependencies = [ [[package]] name = "steel-gen" version = "0.2.0" -source = "git+https://github.com/mattwparas/steel.git#01e42f435de3f6ca8cf083fdfc422d2a445d5bc9" +source = "git+https://github.com/mattwparas/steel.git#a3843c2f4d20903a47d4b62162015c1e992df6ba" dependencies = [ "codegen", "serde", @@ -2365,7 +2366,7 @@ dependencies = [ [[package]] name = "steel-parser" version = "0.6.0" -source = "git+https://github.com/mattwparas/steel.git#01e42f435de3f6ca8cf083fdfc422d2a445d5bc9" +source = "git+https://github.com/mattwparas/steel.git#a3843c2f4d20903a47d4b62162015c1e992df6ba" dependencies = [ "fxhash", "lasso", diff --git a/helix-term/src/commands/engine/components.rs b/helix-term/src/commands/engine/components.rs index 0e668c66e..225e4e324 100644 --- a/helix-term/src/commands/engine/components.rs +++ b/helix-term/src/commands/engine/components.rs @@ -1,30 +1,225 @@ use std::collections::HashMap; -use helix_view::Editor; +use helix_core::Position; +use helix_view::{ + graphics::{Color, CursorKind, Rect, UnderlineStyle}, + input::{Event, KeyEvent}, + keyboard::{KeyCode, KeyModifiers}, + theme::Style, + Editor, +}; use steel::{ rvals::{Custom, FromSteelVal, IntoSteelVal}, - steel_vm::{builtin::BuiltInModule, engine::Engine, register_fn::RegisterFn}, + steel_vm::{ + builtin::BuiltInModule, + engine::Engine, + register_fn::{MarkerWrapper1, RegisterFn}, + }, SteelVal, }; +use tui::{ + buffer::Buffer, + widgets::{Block, BorderType, Borders, Widget}, +}; use crate::{ - commands::{engine::steel::ENGINE, Context}, + commands::{ + engine::steel::{BoxDynComponent, ENGINE}, + Context, + }, compositor::{self, Component}, - ui::{Popup, Prompt, PromptEvent}, + ctrl, key, + ui::{overlay::overlaid, Popup, Prompt, PromptEvent}, }; +use super::steel::WrappedDynComponent; + // 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()); + module + .register_fn("frame-set-string!", buffer_set_string) + .register_fn("position", Position::new) + .register_fn("position-row", |position: &Position| position.row) + .register_fn("position-col", |position: &Position| position.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) + .register_fn("area-width", |area: &helix_view::graphics::Rect| area.width) + .register_fn("area-height", |area: &helix_view::graphics::Rect| { + area.height + }) + .register_fn("overlaid", |component: &mut WrappedDynComponent| { + let inner: Option> = component + .inner + .take() + .map(|x| Box::new(overlaid(BoxDynComponent::new(x))) as Box); + + component.inner = inner; + }) + .register_fn("block", || { + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::White)) + .border_type(BorderType::Rounded) + .style(Style::default().bg(Color::Black)) + }) + .register_fn( + "block/render", + |buf: &mut Buffer, area: Rect, block: Block| block.render(area, buf), + ) + .register_fn("buffer/clear", Buffer::clear) + .register_fn("buffer/clear-with", Buffer::clear_with) + .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()) + .register_value("Color/White", Color::White.into_steelval().unwrap()) + .register_value("Color/Green", Color::Green.into_steelval().unwrap()) + .register_value("Color/Yellow", Color::Yellow.into_steelval().unwrap()) + .register_value("Color/Blue", Color::Blue.into_steelval().unwrap()) + .register_value("Color/Magenta", Color::Magenta.into_steelval().unwrap()) + .register_value("Color/Cyan", Color::Cyan.into_steelval().unwrap()) + .register_value("Color/Gray", Color::Gray.into_steelval().unwrap()) + .register_value("Color/LightRed", Color::LightRed.into_steelval().unwrap()) + .register_value( + "Color/LightGreen", + Color::LightGreen.into_steelval().unwrap(), + ) + .register_value( + "Color/LightYellow", + Color::LightYellow.into_steelval().unwrap(), + ) + .register_value("Color/LightBlue", Color::LightBlue.into_steelval().unwrap()) + .register_value( + "Color/LightMagenta", + Color::LightMagenta.into_steelval().unwrap(), + ) + .register_value("Color/LightCyan", Color::LightCyan.into_steelval().unwrap()) + .register_value("Color/LightGray", Color::LightGray.into_steelval().unwrap()) + .register_fn("Color/rgb", Color::Rgb) + .register_fn("Color/Indexed", Color::Indexed) + .register_fn("style-fg", Style::fg) + .register_fn("style-bg", Style::bg) + .register_fn("style-underline-color", Style::underline_color) + .register_fn("style-underline-style", Style::underline_style) + .register_value( + "Underline/Reset", + UnderlineStyle::Reset.into_steelval().unwrap(), + ) + .register_value( + "Underline/Line", + UnderlineStyle::Line.into_steelval().unwrap(), + ) + .register_value( + "Underline/Curl", + UnderlineStyle::Curl.into_steelval().unwrap(), + ) + .register_value( + "Underline/Dotted", + UnderlineStyle::Dotted.into_steelval().unwrap(), + ) + .register_value( + "Underline/Dashed", + UnderlineStyle::Dashed.into_steelval().unwrap(), + ) + .register_value( + "Underline/DoubleLine", + UnderlineStyle::DoubleLine.into_steelval().unwrap(), + ) + .register_fn("style", || Style::default()) + .register_value( + "event-result/consume", + SteelEventResult::Consumed.into_steelval().unwrap(), + ) + .register_value( + "event-result/ignore", + SteelEventResult::Ignored.into_steelval().unwrap(), + ) + .register_value( + "event-result/close", + SteelEventResult::Close.into_steelval().unwrap(), + ) + // TODO: Use a reference here instead of passing by value. + .register_fn("key-event-char", |event: Event| { + if let Event::Key(event) = event { + event.char() + } else { + None + } + }) + .register_fn("key-event-modifier", |event: Event| { + if let Event::Key(KeyEvent { modifiers, .. }) = event { + Some(modifiers.bits()) + } else { + None + } + }) + .register_value( + "key-modifier-ctrl", + SteelVal::IntV(KeyModifiers::CONTROL.bits() as isize), + ) + .register_value( + "key-modifier-shift", + SteelVal::IntV(KeyModifiers::SHIFT.bits() as isize), + ) + .register_value( + "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 + } else { + false + } + }) + .register_fn("key-event-backspace?", |event: Event| { + if let Event::Key(KeyEvent { + code: KeyCode::Backspace, + .. + }) = event + { + true + } else { + false + } + }) + .register_fn("key-event-enter?", |event: Event| { + if let Event::Key(KeyEvent { + code: KeyCode::Enter, + .. + }) = event + { + true + } else { + false + } + }); + module } -/// A dynamic component, used for rendering thing +fn buffer_set_string( + buffer: &mut tui::buffer::Buffer, + x: u16, + y: u16, + string: steel::rvals::SteelString, + style: Style, +) { + buffer.set_string(x, y, string.as_str(), style) +} + +/// A dynamic component, used for rendering #[derive(Clone)] -// TODO: Implement `trace` method for objects that hold steel vals pub struct SteelDynamicComponent { - name: String, + // TODO: currently the component id requires using a &'static str, + // however in a world with dynamic components that might not be + // the case anymore + _name: String, // This _should_ be a struct, but in theory can be whatever you want. It will be the first argument // passed to the functions in the remainder of the struct. state: SteelVal, @@ -33,6 +228,11 @@ pub struct SteelDynamicComponent { render: SteelVal, cursor: Option, required_size: Option, + + // Cached key event; we keep this around so that when sending + // events to the event handler, we can reuse the heap allocation + // instead of re-allocating for every event (which might be a lot) + key_event: Option, } impl SteelDynamicComponent { @@ -42,21 +242,16 @@ impl SteelDynamicComponent { render: SteelVal, h: HashMap, ) -> Self { - // if let SteelVal::HashMapV(h) = functions { - Self { - name, + _name: name, state, render, handle_event: h.get("handle_event").cloned(), should_update: h.get("should_update").cloned(), cursor: h.get("cursor").cloned(), required_size: h.get("required_size").cloned(), + key_event: None, } - - // } else { - // panic!("Implement better error handling") - // } } pub fn new_dyn( @@ -102,67 +297,15 @@ impl Custom for SteelDynamicComponent {} impl Custom for Box {} -pub struct WrappedDynComponent { - inner: Option>, -} - -impl Custom for WrappedDynComponent {} - -struct BoxDynComponent { - inner: Box, -} - -impl BoxDynComponent { - pub fn new(inner: Box) -> Self { - Self { inner } - } +#[derive(Clone)] +enum SteelEventResult { + Consumed, + Ignored, + Close, } -impl Component for BoxDynComponent { - fn handle_event( - &mut self, - _event: &helix_view::input::Event, - _ctx: &mut compositor::Context, - ) -> compositor::EventResult { - self.inner.handle_event(_event, _ctx) - } - - fn should_update(&self) -> bool { - self.inner.should_update() - } - - fn cursor( - &self, - _area: helix_view::graphics::Rect, - _ctx: &Editor, - ) -> ( - Option, - helix_view::graphics::CursorKind, - ) { - self.inner.cursor(_area, _ctx) - } - - fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { - self.inner.required_size(_viewport) - } - - fn type_name(&self) -> &'static str { - std::any::type_name::() - } +impl Custom for SteelEventResult {} - fn id(&self) -> Option<&'static str> { - None - } - - fn render( - &mut self, - area: helix_view::graphics::Rect, - frame: &mut tui::buffer::Buffer, - ctx: &mut compositor::Context, - ) { - self.inner.render(area, frame, ctx) - } -} impl Component for SteelDynamicComponent { fn render( &mut self, @@ -181,10 +324,10 @@ 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, c| { + let thunk = |engine: &mut Engine, f| { engine.call_function_with_args( self.render.clone(), - vec![self.state.clone(), area.into_steelval().unwrap(), f, c], + vec![self.state.clone(), area.into_steelval().unwrap(), f], ) }; @@ -196,16 +339,15 @@ impl Component for SteelDynamicComponent { .consume(|engine, args| { let mut arg_iter = args.into_iter(); - (thunk)(engine, arg_iter.next().unwrap(), arg_iter.next().unwrap()) - }) + let buffer = arg_iter.next().unwrap(); + let context = arg_iter.next().unwrap(); + + engine.update_value("*helix.cx*", context); - // .run_with_references::( - // frame, &mut ctx, thunk, - // ) + (thunk)(engine, buffer) + }) }) .unwrap(); - - log::info!("Calling dynamic render!"); } // TODO: Pass in event as well? Need to have immutable reference type @@ -213,7 +355,7 @@ impl Component for SteelDynamicComponent { // Clong is _not_ ideal, but it might be all we can do for now. fn handle_event( &mut self, - event: &helix_view::input::Event, + event: &Event, ctx: &mut compositor::Context, ) -> compositor::EventResult { if let Some(handle_event) = &mut self.handle_event { @@ -226,29 +368,70 @@ 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 + if let Some(inner) = steel::rvals::as_underlying_type_mut::( + key_event.borrow_mut().as_mut(), + ) { + *inner = event.clone(); + } + } + + None => { + self.key_event = Some(event.clone().into_steelval().unwrap()); + } + _ => { + panic!("This event needs to stay as a steelval"); + } + } + // 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, c| { + let thunk = |engine: &mut Engine| { engine.call_function_with_args( handle_event.clone(), - vec![ - self.state.clone(), - // TODO: We do _not_ want to clone here, we would need to create a bunch of methods on the engine for various - // combinations of reference passing to do this safely. Right now its limited to mutable references, but we should - // expose more - investigate macros on how to do that with recursively crunching the list to generate the combinations. - // Experimentation needed. - event.clone().into_steelval().unwrap(), - c, - ], + vec![self.state.clone(), self.key_event.clone().unwrap()], ) }; + let close_fn = compositor::EventResult::Consumed(Some(Box::new( + |compositor: &mut compositor::Compositor, _| { + // remove the layer + compositor.pop(); + }, + ))); + + let event = match event { + Event::Key(event) => *event, + _ => return compositor::EventResult::Ignored(None), + }; + match ENGINE.with(|x| { x.borrow_mut() - .run_thunk_with_reference::(&mut ctx, thunk) + .with_mut_reference::(&mut ctx) + .consume(move |engine, arguments| { + let context = arguments[0].clone(); + engine.update_value("*helix.cx*", context); + + thunk(engine) + }) }) { - Ok(v) => compositor::EventResult::from_steelval(&v) - .unwrap_or_else(|_| compositor::EventResult::Ignored(None)), + Ok(v) => { + let value = SteelEventResult::from_steelval(&v); + + match value { + Ok(SteelEventResult::Close) => close_fn, + Ok(SteelEventResult::Consumed) => compositor::EventResult::Consumed(None), + Ok(SteelEventResult::Ignored) => compositor::EventResult::Ignored(None), + _ => match event { + ctrl!('c') | key!(Esc) => close_fn, + _ => compositor::EventResult::Ignored(None), + }, + } + } Err(_) => compositor::EventResult::Ignored(None), } } else { @@ -259,8 +442,11 @@ impl Component for SteelDynamicComponent { fn should_update(&self) -> bool { if let Some(should_update) = &self.should_update { match ENGINE.with(|x| { - x.borrow_mut() - .call_function_with_args(should_update.clone(), vec![self.state.clone()]) + 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, @@ -274,30 +460,29 @@ impl Component for SteelDynamicComponent { fn cursor( &self, area: helix_view::graphics::Rect, - ctx: &Editor, + _ctx: &Editor, ) -> ( 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 - let thunk = |engine: &mut Engine, e| { + let thunk = |engine: &mut Engine| { engine.call_function_with_args( cursor.clone(), - vec![self.state.clone(), area.into_steelval().unwrap(), e], + vec![self.state.clone(), area.into_steelval().unwrap()], ) }; - <( - Option, - helix_view::graphics::CursorKind, - )>::from_steelval(&ENGINE.with(|x| { - x.borrow_mut() - .run_thunk_with_ro_reference::(ctx, thunk) - .unwrap() - })) - .unwrap() + let result = Option::::from_steelval( + &ENGINE.with(|x| thunk(&mut (x.borrow_mut())).unwrap()), + ); + + log::info!("Setting cursor at position: {:?}", result); + + (result.unwrap(), CursorKind::Block) } else { (None, helix_view::graphics::CursorKind::Hidden) } diff --git a/helix-term/src/commands/engine/steel.rs b/helix-term/src/commands/engine/steel.rs index 76b9afa20..5d21b80b5 100644 --- a/helix-term/src/commands/engine/steel.rs +++ b/helix-term/src/commands/engine/steel.rs @@ -55,7 +55,10 @@ use crate::{ use components::SteelDynamicComponent; -use super::{components, Context, MappableCommand, TYPABLE_COMMAND_LIST}; +use super::{ + components::{self, helix_component_module}, + Context, MappableCommand, TYPABLE_COMMAND_LIST, +}; use insert::{insert_char, insert_string}; static ENGINE_COUNT: AtomicUsize = AtomicUsize::new(0); @@ -117,6 +120,11 @@ thread_local! { } +fn load_component_api(engine: &mut Engine) { + let module = helix_component_module(); + engine.register_module(module); +} + fn load_keymap_api(engine: &mut Engine, api: KeyMapApi) { let mut module = BuiltInModule::new("helix/core/keymaps"); @@ -1032,7 +1040,6 @@ pub fn present_error_inside_engine_context(cx: &mut Context, engine: &mut Engine format!("```\n{}\n```", backtrace), editor.syn_loader.clone(), ); - ui::Text::new(format!("```\n{}\n```", backtrace)); let popup = Popup::new("engine", contents).position(Some( helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2), )); @@ -1470,30 +1477,14 @@ fn get_themes(cx: &mut Context) -> Vec { /// A dynamic component, used for rendering thing impl Custom for compositor::EventResult {} -impl FromSteelVal for compositor::EventResult { - fn from_steelval(val: &SteelVal) -> steel::rvals::Result { - match val { - SteelVal::SymbolV(v) if v.as_str() == "EventResult::Ignored" => { - Ok(compositor::EventResult::Ignored(None)) - } - SteelVal::SymbolV(v) if v.as_str() == "EventResult::Consumed" => { - Ok(compositor::EventResult::Consumed(None)) - } - _ => Err(steel::SteelErr::new( - steel::rerrs::ErrorKind::TypeMismatch, - "Unable to convert value to event result".to_string(), - )), - } - } -} -struct WrappedDynComponent { - inner: Option>, +pub struct WrappedDynComponent { + pub(crate) inner: Option>, } impl Custom for WrappedDynComponent {} -struct BoxDynComponent { +pub struct BoxDynComponent { inner: Box, } @@ -1838,6 +1829,9 @@ pub fn configure_builtin_sources(engine: &mut Engine, generate_sources: bool) { } load_rope_api(engine); load_misc_api(engine, generate_sources); + if !generate_sources { + load_component_api(engine); + } } fn configure_engine_impl(mut engine: Engine) -> Engine { diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 3dcb5f2bf..96a2cf42d 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -175,7 +175,9 @@ impl Compositor { pub fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { for layer in &mut self.layers { - layer.render(area, surface, cx); + if layer.should_update() { + layer.render(area, surface, cx) + }; } } diff --git a/helix-tui/src/extension.rs b/helix-tui/src/extension.rs index 8b9af3779..ad14a6c2b 100644 --- a/helix-tui/src/extension.rs +++ b/helix-tui/src/extension.rs @@ -1,11 +1,12 @@ #[cfg(feature = "steel")] mod steel_implementations { - use crate::{buffer::Buffer, widgets::Widget}; + use crate::{buffer::Buffer, widgets::Block}; use steel::{gc::unsafe_erased_pointers::CustomReference, rvals::Custom}; impl CustomReference for Buffer {} + impl Custom for Block<'static> {} steel::custom_reference!(Buffer); } diff --git a/helix-view/src/extension.rs b/helix-view/src/extension.rs index b2c3dfd9d..e3680b7a7 100644 --- a/helix-view/src/extension.rs +++ b/helix-view/src/extension.rs @@ -16,7 +16,7 @@ mod steel_implementations { LineEndingConfig, LineNumber, LspConfig, SearchConfig, SmartTabConfig, StatusLineConfig, TerminalConfig, WhitespaceConfig, }, - graphics::Rect, + graphics::{Color, Rect, Style, UnderlineStyle}, input::Event, Document, DocumentId, Editor, ViewId, }; @@ -26,6 +26,9 @@ 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 UnderlineStyle {} impl CustomReference for Event {} impl Custom for Rect {}