From 15cd1afbdcaf4aea4301515e7c9063802a8a1399 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Wed, 18 Jan 2023 19:34:49 -0700 Subject: [PATCH] lua: add some pane methods for working with zones refs: https://github.com/wez/wezterm/issues/2968 --- docs/changelog.md | 4 + docs/config/lua/pane/get_semantic_zone_at.md | 30 ++++ docs/config/lua/pane/get_semantic_zones.md | 17 ++ docs/config/lua/pane/get_text_from_region.md | 16 ++ .../lua/pane/get_text_from_semantic_zone.md | 12 ++ lua-api-crates/mux/src/pane.rs | 146 +++++++++++++++++- luahelper/src/lib.rs | 43 +++--- term/src/lib.rs | 3 +- 8 files changed, 250 insertions(+), 21 deletions(-) create mode 100644 docs/config/lua/pane/get_semantic_zone_at.md create mode 100644 docs/config/lua/pane/get_semantic_zones.md create mode 100644 docs/config/lua/pane/get_text_from_region.md create mode 100644 docs/config/lua/pane/get_text_from_semantic_zone.md diff --git a/docs/changelog.md b/docs/changelog.md index 21db4fad7..0fc81d67d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -29,6 +29,10 @@ As features stabilize some brief notes about them will accumulate here. option to control whether the mouse cursor is hidden when typing. Thanks to [@ProspectPyxis](https://github.com/ProspectPyxis)! [#2946](https://github.com/wez/wezterm/pull/2946) +* [pane:get_text_from_region()](config/lua/pane/get_text_from_region.md), + [pane:get_text_from_semantic_zone()](config/lua/pane/get_text_from_semantic_zone.md), + [pane:get_semantic_zones()](config/lua/pane/get_semantic_zones.md), + [pane:get_semantic_zone_at()](config/lua/pane/get_semantic_zone_at.md) * Color schemes: [Apple Classic](colorschemes/a/index.md#apple-classic), [\_bash (Gogh)](colorschemes/b/index.md#bash-gogh), [Breath (Gogh)](colorschemes/b/index.md#breath-gogh), diff --git a/docs/config/lua/pane/get_semantic_zone_at.md b/docs/config/lua/pane/get_semantic_zone_at.md new file mode 100644 index 000000000..2478fd3a6 --- /dev/null +++ b/docs/config/lua/pane/get_semantic_zone_at.md @@ -0,0 +1,30 @@ +# `pane:get_semantic_zone_at(x, y)` + +*Since: nightly builds only* + +Resolves the semantic zone that encapsulates the supplied *x* and *y* coordinates. + +*x* is the cell column index, where 0 is the left-most column. +*y* is the stable row index. + +Use [pane:get_dimensions()](get_dimensions.md) to +retrieve the currently valid stable index values for the top of scrollback and +top of viewport. + +```lua +-- If you have shell integration configured, returns the zone around +-- the current cursor position +function get_zone_around_cursor(pane) + local cursor = pane:get_cursor_position() + -- using x-1 here because the cursor may be one cell outside the zone + local zone = pane:get_semantic_zone_at(cursor.x - 1, cursor.y) + if zone then + return pane:get_text_from_semantic_zone(zone) + end + return nil +end +``` + +See [Shell Integration](../../../shell-integration.md) for more information +about semantic zones. + diff --git a/docs/config/lua/pane/get_semantic_zones.md b/docs/config/lua/pane/get_semantic_zones.md new file mode 100644 index 000000000..81b489405 --- /dev/null +++ b/docs/config/lua/pane/get_semantic_zones.md @@ -0,0 +1,17 @@ +# `pane:get_semantic_zones([zone_type])` + +*Since: nightly builds only* + +When *zone_type* is omitted, returns the list of all semantic zones defined in the pane. + +When *zone_type* is supplied, returns the list of all semantic zones of the matching type. + +Value values for *zone_type* are: + +* `"Prompt"` +* `"Input"` +* `"Output"` + +See [Shell Integration](../../../shell-integration.md) for more information +about semantic zones. + diff --git a/docs/config/lua/pane/get_text_from_region.md b/docs/config/lua/pane/get_text_from_region.md new file mode 100644 index 000000000..6addd931a --- /dev/null +++ b/docs/config/lua/pane/get_text_from_region.md @@ -0,0 +1,16 @@ +# `pane:get_text_from_region(start_x, start_y, end_x, end_y)` + +*Since: nightly builds only* + +Returns the text from the specified region. + +* `start_x` and `end_x` are the starting and ending cell column, where 0 is the + left-most cell +* `start_y` and `end_y` are the starting and ending row, expressed as a stable + row index. Use [pane:get_dimensions()](get_dimensions.md) to retrieve the + currently valid stable index values for the top of scrollback and top of + viewport. + +The text within the region is unwrapped to its logical line representation, +rather than the wrapped-to-physical-display-width. + diff --git a/docs/config/lua/pane/get_text_from_semantic_zone.md b/docs/config/lua/pane/get_text_from_semantic_zone.md new file mode 100644 index 000000000..da7a60bd6 --- /dev/null +++ b/docs/config/lua/pane/get_text_from_semantic_zone.md @@ -0,0 +1,12 @@ +# `pane:get_text_from_semantic_zone(zone)` + +*Since: nightly builds only* + +This is a convenience method that calls [pane:get_text_from_region()](get_text_from_region.md) on the supplied *zone* parameter. + +Use [pane:get_semantic_zone_at()](get_semantic_zone_at.md) or +[pane:get_semantic_zones()](get_semantic_zones.md) to obtain a *zone*. + +See [Shell Integration](../../../shell-integration.md) for more information +about semantic zones. + diff --git a/lua-api-crates/mux/src/pane.rs b/lua-api-crates/mux/src/pane.rs index 8702daebf..b4f0ddd0e 100644 --- a/lua-api-crates/mux/src/pane.rs +++ b/lua-api-crates/mux/src/pane.rs @@ -1,6 +1,10 @@ use super::*; -use luahelper::dynamic_to_lua_value; +use luahelper::{dynamic_to_lua_value, from_lua, to_lua}; +use mlua::Value; +use std::cmp::Ordering; use std::sync::Arc; +use termwiz::cell::SemanticType; +use wezterm_term::{SemanticZone, StableRowIndex}; #[derive(Clone, Copy, Debug)] pub struct MuxPane(pub PaneId); @@ -10,6 +14,70 @@ impl MuxPane { mux.get_pane(self.0) .ok_or_else(|| mlua::Error::external(format!("pane id {} not found in mux", self.0))) } + + fn get_text_from_semantic_zone(&self, zone: SemanticZone) -> mlua::Result { + let mux = get_mux()?; + let pane = self.resolve(&mux)?; + + let mut last_was_wrapped = false; + let first_row = zone.start_y; + let last_row = zone.end_y; + + fn cols_for_row(zone: &SemanticZone, row: StableRowIndex) -> std::ops::Range { + if row < zone.start_y || row > zone.end_y { + 0..0 + } else if zone.start_y == zone.end_y { + // A single line zone + if zone.start_x <= zone.end_x { + zone.start_x..zone.end_x.saturating_add(1) + } else { + zone.end_x..zone.start_x.saturating_add(1) + } + } else if row == zone.end_y { + // last line of multi-line + 0..zone.end_x.saturating_add(1) + } else if row == zone.start_y { + // first line of multi-line + zone.start_x..usize::max_value() + } else { + // some "middle" line of multi-line + 0..usize::max_value() + } + } + + let mut s = String::new(); + for line in pane.get_logical_lines(zone.start_y..zone.end_y + 1) { + if !s.is_empty() && !last_was_wrapped { + s.push('\n'); + } + let last_idx = line.physical_lines.len().saturating_sub(1); + for (idx, phys) in line.physical_lines.iter().enumerate() { + let this_row = line.first_row + idx as StableRowIndex; + if this_row >= first_row && this_row < last_row { + let last_phys_idx = phys.len().saturating_sub(1); + + let cols = cols_for_row(&zone, this_row); + let last_col_idx = cols.end.saturating_sub(1).min(last_phys_idx); + let col_span = phys.columns_as_str(cols); + // Only trim trailing whitespace if we are the last line + // in a wrapped sequence + if idx == last_idx { + s.push_str(col_span.trim_end()); + } else { + s.push_str(&col_span); + } + + last_was_wrapped = last_col_idx == last_phys_idx + && phys + .get_cell(last_col_idx) + .map(|c| c.attrs().wrapped()) + .unwrap_or(false); + } + } + } + + Ok(s) + } } impl UserData for MuxPane { @@ -209,6 +277,82 @@ impl UserData for MuxPane { pane.perform_actions(actions); Ok(()) }); + + methods.add_method("get_semantic_zones", |lua, this, of_type: Value| { + let mux = get_mux()?; + let pane = this.resolve(&mux)?; + + let of_type: Option = from_lua(of_type)?; + + let mut zones = pane + .get_semantic_zones() + .map_err(|e| mlua::Error::external(format!("{:#}", e)))?; + + if let Some(of_type) = of_type { + zones.retain(|zone| zone.semantic_type == of_type); + } + + let zones = to_lua(lua, zones)?; + Ok(zones) + }); + + methods.add_method( + "get_semantic_zone_at", + |lua, this, (x, y): (usize, StableRowIndex)| { + let mux = get_mux()?; + let pane = this.resolve(&mux)?; + + let zones = pane.get_semantic_zones().unwrap_or_else(|_| vec![]); + + fn find_zone(x: usize, y: StableRowIndex, zone: &SemanticZone) -> Ordering { + match zone.start_y.cmp(&y) { + Ordering::Greater => return Ordering::Greater, + // If the zone starts on the same line then check that the + // x position is within bounds + Ordering::Equal => match zone.start_x.cmp(&x) { + Ordering::Greater => return Ordering::Greater, + Ordering::Equal | Ordering::Less => {} + }, + Ordering::Less => {} + } + match zone.end_y.cmp(&y) { + Ordering::Less => Ordering::Less, + // If the zone ends on the same line then check that the + // x position is within bounds + Ordering::Equal => match zone.end_x.cmp(&x) { + Ordering::Less => Ordering::Less, + Ordering::Equal | Ordering::Greater => Ordering::Equal, + }, + Ordering::Greater => Ordering::Equal, + } + } + + match zones.binary_search_by(|zone| find_zone(x, y, zone)) { + Ok(idx) => { + let zone = to_lua(lua, zones[idx])?; + Ok(Some(zone)) + } + Err(_) => Ok(None), + } + }, + ); + + methods.add_method("get_text_from_semantic_zone", |_lua, this, zone: Value| { + let zone: SemanticZone = from_lua(zone)?; + this.get_text_from_semantic_zone(zone) + }); + + methods.add_method("get_text_from_region", |_lua, this, (start_x, start_y, end_x, end_y): (usize, StableRowIndex, usize, StableRowIndex)| { + let zone = SemanticZone { + start_x, + start_y, + end_x, + end_y, + // semantic_type is not used by get_text_from_semantic_zone + semantic_type: SemanticType::Output, + }; + this.get_text_from_semantic_zone(zone) + }); } } diff --git a/luahelper/src/lib.rs b/luahelper/src/lib.rs index 4001dce9b..e2d58d7ba 100644 --- a/luahelper/src/lib.rs +++ b/luahelper/src/lib.rs @@ -9,6 +9,28 @@ use wezterm_dynamic::{FromDynamic, ToDynamic, Value as DynValue}; pub mod enumctor; +pub fn to_lua<'lua, T: ToDynamic>( + lua: &'lua mlua::Lua, + value: T, +) -> Result, mlua::Error> { + let value = value.to_dynamic(); + dynamic_to_lua_value(lua, value) +} + +pub fn from_lua<'lua, T: FromDynamic>(value: mlua::Value<'lua>) -> Result { + let lua_type = value.type_name(); + let value = lua_value_to_dynamic(value).map_err(|e| mlua::Error::FromLuaConversionError { + from: lua_type, + to: std::any::type_name::(), + message: Some(e.to_string()), + })?; + T::from_dynamic(&value, Default::default()).map_err(|e| mlua::Error::FromLuaConversionError { + from: lua_type, + to: std::any::type_name::(), + message: Some(e.to_string()), + }) +} + /// Implement lua conversion traits for a type. /// This implementation requires that the type implement /// FromDynamic and ToDynamic. @@ -23,9 +45,7 @@ macro_rules! impl_lua_conversion_dynamic { self, lua: &'lua $crate::mlua::Lua, ) -> Result<$crate::mlua::Value<'lua>, $crate::mlua::Error> { - use wezterm_dynamic::ToDynamic; - let value = self.to_dynamic(); - $crate::dynamic_to_lua_value(lua, value) + $crate::to_lua(lua, self) } } @@ -34,22 +54,7 @@ macro_rules! impl_lua_conversion_dynamic { value: $crate::mlua::Value<'lua>, _lua: &'lua $crate::mlua::Lua, ) -> Result { - use wezterm_dynamic::FromDynamic; - let lua_type = value.type_name(); - let value = $crate::lua_value_to_dynamic(value).map_err(|e| { - $crate::mlua::Error::FromLuaConversionError { - from: lua_type, - to: stringify!($struct), - message: Some(e.to_string()), - } - })?; - $struct::from_dynamic(&value, Default::default()).map_err(|e| { - $crate::mlua::Error::FromLuaConversionError { - from: lua_type, - to: stringify!($struct), - message: Some(e.to_string()), - } - }) + $crate::from_lua(value) } } }; diff --git a/term/src/lib.rs b/term/src/lib.rs index 0b5fdc929..a19da005a 100644 --- a/term/src/lib.rs +++ b/term/src/lib.rs @@ -21,6 +21,7 @@ use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut, Range}; use std::str; use termwiz::surface::SequenceNo; +use wezterm_dynamic::{FromDynamic, ToDynamic}; pub mod config; pub use config::TerminalConfiguration; @@ -115,7 +116,7 @@ pub struct CursorPosition { } #[cfg_attr(feature = "use_serde", derive(Deserialize, Serialize))] -#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, FromDynamic, ToDynamic)] pub struct SemanticZone { pub start_y: StableRowIndex, pub start_x: usize,