diff options
author | Aram Drevekenin <aram@poor.dev> | 2022-12-29 11:38:33 +0100 |
---|---|---|
committer | Aram Drevekenin <aram@poor.dev> | 2022-12-29 11:38:33 +0100 |
commit | 843eb26d3a9d904ee825b9bdbf57b0ae494ac9d5 (patch) | |
tree | d51bd96c0453776b95bf14ca10d04b7e7d88179d /2 | |
parent | d1aae90d278ef47ae0240a2844d75883cb0ec37c (diff) |
tests passing
Diffstat (limited to '2')
-rw-r--r-- | 2 | 1838 |
1 files changed, 1838 insertions, 0 deletions
@@ -0,0 +1,1838 @@ +use crate::input::{ + command::RunCommand, + config::ConfigError, + layout::{ + FloatingPaneLayout, Layout, TiledPaneLayout, PercentOrFixed, Run, RunPlugin, RunPluginLocation, + SplitDirection, SplitSize, + }, +}; + +use kdl::*; + +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; + +use crate::{ + kdl_child_with_name, kdl_children_nodes, kdl_get_bool_property_or_child_value, + kdl_get_bool_property_or_child_value_with_error, kdl_get_child, + kdl_get_int_property_or_child_value, kdl_get_property_or_child, + kdl_get_string_property_or_child_value, kdl_get_string_property_or_child_value_with_error, + kdl_name, kdl_parsing_error, kdl_property_names, kdl_property_or_child_value_node, + kdl_string_arguments, +}; + +use std::convert::TryFrom; +use std::path::PathBuf; +use std::vec::Vec; +use url::Url; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PaneOrFloatingPane { + Pane(TiledPaneLayout), + FloatingPane(FloatingPaneLayout), + Either(TiledPaneLayout), +} + +pub struct KdlLayoutParser<'a> { + global_cwd: Option<PathBuf>, + raw_layout: &'a str, + tab_templates: HashMap<String, (TiledPaneLayout, Vec<FloatingPaneLayout>, KdlNode)>, + pane_templates: HashMap<String, (PaneOrFloatingPane, KdlNode)>, + default_tab_template: Option<(TiledPaneLayout, Vec<FloatingPaneLayout>, KdlNode)>, +} + +impl<'a> KdlLayoutParser<'a> { + pub fn new(raw_layout: &'a str, global_cwd: Option<PathBuf>) -> Self { + KdlLayoutParser { + raw_layout, + tab_templates: HashMap::new(), + pane_templates: HashMap::new(), + default_tab_template: None, + global_cwd, + } + } + fn is_a_reserved_word(&self, word: &str) -> bool { + word == "pane" + || word == "layout" + || word == "pane_template" + || word == "tab_template" + || word == "default_tab_template" + || word == "command" + || word == "edit" + || word == "plugin" + || word == "children" + || word == "tab" + || word == "args" + || word == "close_on_exit" + || word == "start_suspended" + || word == "borderless" + || word == "focus" + || word == "name" + || word == "size" + || word == "cwd" + || word == "split_direction" + } + fn is_a_valid_pane_property(&self, property_name: &str) -> bool { + property_name == "borderless" + || property_name == "focus" + || property_name == "name" + || property_name == "size" + || property_name == "plugin" + || property_name == "command" + || property_name == "edit" + || property_name == "cwd" + || property_name == "args" + || property_name == "close_on_exit" + || property_name == "start_suspended" + || property_name == "split_direction" + || property_name == "pane" + || property_name == "children" + } + fn is_a_valid_floating_pane_property(&self, property_name: &str) -> bool { + property_name == "borderless" + || property_name == "focus" + || property_name == "name" + || property_name == "plugin" + || property_name == "command" + || property_name == "edit" + || property_name == "cwd" + || property_name == "args" + || property_name == "close_on_exit" + || property_name == "start_suspended" + || property_name == "x" + || property_name == "y" + || property_name == "width" + || property_name == "height" + } + fn is_a_valid_tab_property(&self, property_name: &str) -> bool { + property_name == "focus" + || property_name == "name" + || property_name == "split_direction" + || property_name == "cwd" + || property_name == "floating_panes" + } + fn assert_legal_node_name(&self, name: &str, kdl_node: &KdlNode) -> Result<(), ConfigError> { + if name.contains(char::is_whitespace) { + Err(ConfigError::new_layout_kdl_error( + format!("Node names ({}) cannot contain whitespace.", name), + kdl_node.span().offset(), + kdl_node.span().len(), + )) + } else if self.is_a_reserved_word(&name) { + Err(ConfigError::new_layout_kdl_error( + format!("Node name '{}' is a reserved word.", name), + kdl_node.span().offset(), + kdl_node.span().len(), + )) + } else { + Ok(()) + } + } + fn assert_legal_template_name( + &self, + name: &str, + kdl_node: &KdlNode, + ) -> Result<(), ConfigError> { + if name.is_empty() { + Err(ConfigError::new_layout_kdl_error( + format!("Template names cannot be empty"), + kdl_node.span().offset(), + kdl_node.span().len(), + )) + } else if name.contains(')') || name.contains('(') { + Err(ConfigError::new_layout_kdl_error( + format!("Template names cannot contain parantheses"), + kdl_node.span().offset(), + kdl_node.span().len(), + )) + } else if name + .chars() + .next() + .map(|first_char| first_char.is_numeric()) + .unwrap_or(false) + { + Err(ConfigError::new_layout_kdl_error( + format!("Template names cannot start with numbers"), + kdl_node.span().offset(), + kdl_node.span().len(), + )) + } else { + Ok(()) + } + } + fn parse_split_size(&self, kdl_node: &KdlNode) -> Result<Option<SplitSize>, ConfigError> { + if let Some(size) = kdl_get_string_property_or_child_value!(kdl_node, "size") { + match SplitSize::from_str(size) { + Ok(size) => Ok(Some(size)), + Err(_e) => Err(kdl_parsing_error!( + format!( + "size should be a fixed number (eg. 1) or a quoted percent (eg. \"50%\")" + ), + kdl_node + )), + } + } else if let Some(size) = kdl_get_int_property_or_child_value!(kdl_node, "size") { + if size == 0 { + return Err(kdl_parsing_error!( + format!("size should be greater than 0"), + kdl_node + )); + } + Ok(Some(SplitSize::Fixed(size as usize))) + } else if let Some(node) = kdl_property_or_child_value_node!(kdl_node, "size") { + Err(kdl_parsing_error!( + format!("size should be a fixed number (eg. 1) or a quoted percent (eg. \"50%\")"), + node + )) + } else if let Some(node) = kdl_child_with_name!(kdl_node, "size") { + Err(kdl_parsing_error!( + format!( + "size cannot be bare, it should have a value (eg. 'size 1', or 'size \"50%\"')" + ), + node + )) + } else { + Ok(None) + } + } + fn parse_percent_or_fixed( + &self, + kdl_node: &KdlNode, + value_name: &str, + can_be_zero: bool, + ) -> Result<Option<PercentOrFixed>, ConfigError> { + if let Some(size) = kdl_get_string_property_or_child_value!(kdl_node, value_name) { + match PercentOrFixed::from_str(size) { + Ok(size) => { + if !can_be_zero && size.is_zero() { + Err(kdl_parsing_error!( + format!("{} should be greater than 0", value_name), + kdl_node + )) + } else { + Ok(Some(size)) + } + }, + Err(_e) => Err(kdl_parsing_error!( + format!( + "{} should be a fixed number (eg. 1) or a quoted percent (eg. \"50%\")", + value_name + ), + kdl_node + )), + } + } else if let Some(size) = kdl_get_int_property_or_child_value!(kdl_node, value_name) { + if size == 0 && !can_be_zero { + return Err(kdl_parsing_error!( + format!("{} should be greater than 0", value_name), + kdl_node + )); + } + Ok(Some(PercentOrFixed::Fixed(size as usize))) + } else if let Some(node) = kdl_property_or_child_value_node!(kdl_node, "size") { + Err(kdl_parsing_error!( + format!( + "{} should be a fixed number (eg. 1) or a quoted percent (eg. \"50%\")", + value_name + ), + node + )) + } else if let Some(node) = kdl_child_with_name!(kdl_node, "size") { + Err(kdl_parsing_error!( + format!( + "{} cannot be bare, it should have a value (eg. 'size 1', or 'size \"50%\"')", + value_name + ), + node + )) + } else { + Ok(None) + } + } + fn parse_plugin_block(&self, plugin_block: &KdlNode) -> Result<Option<Run>, ConfigError> { + let _allow_exec_host_cmd = + kdl_get_bool_property_or_child_value_with_error!(plugin_block, "_allow_exec_host_cmd") + .unwrap_or(false); + let string_url = + kdl_get_string_property_or_child_value_with_error!(plugin_block, "location").ok_or( + ConfigError::new_layout_kdl_error( + "Plugins must have a location".into(), + plugin_block.span().offset(), + plugin_block.span().len(), + ), + )?; + let url_node = kdl_get_property_or_child!(plugin_block, "location").ok_or( + ConfigError::new_layout_kdl_error( + "Plugins must have a location".into(), + plugin_block.span().offset(), + plugin_block.span().len(), + ), + )?; + let url = Url::parse(string_url).map_err(|e| { + ConfigError::new_layout_kdl_error( + format!("Failed to parse url: {:?}", e), + url_node.span().offset(), + url_node.span().len(), + ) + })?; + let location = RunPluginLocation::try_from(url)?; + Ok(Some(Run::Plugin(RunPlugin { + _allow_exec_host_cmd, + location, + }))) + } + fn parse_args(&self, pane_node: &KdlNode) -> Result<Option<Vec<String>>, ConfigError> { + match kdl_get_child!(pane_node, "args") { + Some(kdl_args) => { + if kdl_args.entries().is_empty() { + return Err(kdl_parsing_error!(format!("args cannot be empty and should contain one or more command arguments (eg. args \"-h\" \"-v\")"), kdl_args)); + } + Ok(Some( + kdl_string_arguments!(kdl_args) + .iter() + .map(|s| String::from(*s)) + .collect(), + )) + }, + None => Ok(None), + } + } + fn cwd_prefix(&self, tab_cwd: Option<&PathBuf>) -> Result<Option<PathBuf>, ConfigError> { + Ok(match (&self.global_cwd, tab_cwd) { + (Some(global_cwd), Some(tab_cwd)) => Some(global_cwd.join(tab_cwd)), + (None, Some(tab_cwd)) => Some(tab_cwd.clone()), + (Some(global_cwd), None) => Some(global_cwd.clone()), + (None, None) => None, + }) + } + fn parse_cwd(&self, kdl_node: &KdlNode) -> Result<Option<PathBuf>, ConfigError> { + Ok( + kdl_get_string_property_or_child_value_with_error!(kdl_node, "cwd") + .map(|cwd| PathBuf::from(cwd)), + ) + } + fn parse_pane_command( + &self, + pane_node: &KdlNode, + is_template: bool, + ) -> Result<Option<Run>, ConfigError> { + let command = kdl_get_string_property_or_child_value_with_error!(pane_node, "command") + .map(|c| PathBuf::from(c)); + let edit = kdl_get_string_property_or_child_value_with_error!(pane_node, "edit") + .map(|c| PathBuf::from(c)); + let cwd = self.parse_cwd(pane_node)?; + let args = self.parse_args(pane_node)?; + let close_on_exit = + kdl_get_bool_property_or_child_value_with_error!(pane_node, "close_on_exit"); + let start_suspended = + kdl_get_bool_property_or_child_value_with_error!(pane_node, "start_suspended"); + if !is_template { + self.assert_no_bare_attributes_in_pane_node( + &command, + &args, + &close_on_exit, + &start_suspended, + pane_node, + )?; + } + let hold_on_close = close_on_exit.map(|c| !c).unwrap_or(true); + let hold_on_start = start_suspended.map(|c| c).unwrap_or(false); + match (command, edit, cwd) { + (None, None, Some(cwd)) => Ok(Some(Run::Cwd(cwd))), + (Some(command), None, cwd) => Ok(Some(Run::Command(RunCommand { + command, + args: args.unwrap_or_else(|| vec![]), + cwd, + hold_on_close, + hold_on_start, + }))), + (None, Some(edit), Some(cwd)) => Ok(Some(Run::EditFile(cwd.join(edit), None))), + (None, Some(edit), None) => Ok(Some(Run::EditFile(edit, None))), + (Some(_command), Some(_edit), _) => Err(ConfigError::new_layout_kdl_error( + "cannot have both a command and an edit instruction for the same pane".into(), + pane_node.span().offset(), + pane_node.span().len(), + )), + _ => Ok(None), + } + } + fn parse_command_plugin_or_edit_block( + &self, + kdl_node: &KdlNode, + ) -> Result<Option<Run>, ConfigError> { + let mut run = self.parse_pane_command(kdl_node, false)?; + if let Some(plugin_block) = kdl_get_child!(kdl_node, "plugin") { + let has_non_cwd_run_prop = run + .map(|r| match r { + Run::Cwd(_) => false, + _ => true, + }) + .unwrap_or(false); + if has_non_cwd_run_prop { + return Err(ConfigError::new_layout_kdl_error( + "Cannot have both a command/edit and a plugin block for a single pane".into(), + plugin_block.span().offset(), + plugin_block.span().len(), + )); + } + run = self.parse_plugin_block(plugin_block)?; + } + Ok(run) + } + fn parse_command_plugin_or_edit_block_for_template( + &self, + kdl_node: &KdlNode, + ) -> Result<Option<Run>, ConfigError> { + let mut run = self.parse_pane_command(kdl_node, true)?; + if let Some(plugin_block) = kdl_get_child!(kdl_node, "plugin") { + let has_non_cwd_run_prop = run + .map(|r| match r { + Run::Cwd(_) => false, + _ => true, + }) + .unwrap_or(false); + if has_non_cwd_run_prop { + return Err(ConfigError::new_layout_kdl_error( + "Cannot have both a command/edit and a plugin block for a single pane".into(), + plugin_block.span().offset(), + plugin_block.span().len(), + )); + } + run = self.parse_plugin_block(plugin_block)?; + } + Ok(run) + } + fn parse_pane_node(&self, kdl_node: &KdlNode) -> Result<TiledPaneLayout, ConfigError> { + self.assert_valid_pane_properties(kdl_node)?; + let borderless = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless"); + let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus"); + let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name") + .map(|name| name.to_string()); + let split_size = self.parse_split_size(kdl_node)?; + let run = self.parse_command_plugin_or_edit_block(kdl_node)?; + let children_split_direction = self.parse_split_direction(kdl_node)?; + let (external_children_index, children) = match kdl_children_nodes!(kdl_node) { + Some(children) => self.parse_child_pane_nodes_for_pane(&children)?, + None => (None, vec![]), + }; + self.assert_no_mixed_children_and_properties(kdl_node)?; + Ok(TiledPaneLayout { + borderless: borderless.unwrap_or_default(), + focus, + name, + split_size, + run, + children_split_direction, + external_children_index, + children, + ..Default::default() + }) + } + fn parse_floating_pane_node( + &self, + kdl_node: &KdlNode, + ) -> Result<FloatingPaneLayout, ConfigError> { + self.assert_valid_floating_pane_properties(kdl_node)?; + let height = self.parse_percent_or_fixed(kdl_node, "height", false)?; + let width = self.parse_percent_or_fixed(kdl_node, "width", false)?; + let x = self.parse_percent_or_fixed(kdl_node, "x", true)?; + let y = self.parse_percent_or_fixed(kdl_node, "y", true)?; + let run = self.parse_command_plugin_or_edit_block(kdl_node)?; + let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus"); + let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name") + .map(|name| name.to_string()); + self.assert_no_mixed_children_and_properties(kdl_node)?; + Ok(FloatingPaneLayout { + name, + height, + width, + x, + y, + run, + focus, + ..Default::default() + }) + } + fn insert_children_to_pane_template( + &self, + kdl_node: &KdlNode, + pane_template: &mut TiledPaneLayout, + pane_template_kdl_node: &KdlNode, + ) -> Result<(), ConfigError> { + let children_split_direction = self.parse_split_direction(kdl_node)?; + let (external_children_index, pane_parts) = match kdl_children_nodes!(kdl_node) { + Some(children) => self.parse_child_pane_nodes_for_pane(&children)?, + None => (None, vec![]), + }; + if pane_parts.len() > 0 { + let child_panes_layout = TiledPaneLayout { + children_split_direction, + children: pane_parts, + external_children_index, + ..Default::default() + }; + self.assert_one_children_block(&pane_template, pane_template_kdl_node)?; + self.insert_layout_children_or_error( + pane_template, + child_panes_layout, + pane_template_kdl_node, + )?; + } + Ok(()) + } + fn parse_pane_node_with_template( + &self, + kdl_node: &KdlNode, + pane_template: PaneOrFloatingPane, + pane_template_kdl_node: &KdlNode, + ) -> Result<TiledPaneLayout, ConfigError> { + match pane_template { + PaneOrFloatingPane::Pane(mut pane_template) + | PaneOrFloatingPane::Either(mut pane_template) => { + let borderless = + kdl_get_bool_property_or_child_value_with_error!(kdl_node, "borderless"); + let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus"); + let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name") + .map(|name| name.to_string()); + let args = self.parse_args(kdl_node)?; + let close_on_exit = + kdl_get_bool_property_or_child_value_with_error!(kdl_node, "close_on_exit"); + let start_suspended = + kdl_get_bool_property_or_child_value_with_error!(kdl_node, "start_suspended"); + let split_size = self.parse_split_size(kdl_node)?; + let run = self.parse_command_plugin_or_edit_block_for_template(kdl_node)?; + self.assert_no_bare_attributes_in_pane_node_with_template( + &run, + &pane_template.run, + &args, + &close_on_exit, + &start_suspended, + kdl_node, + )?; + self.insert_children_to_pane_template( + kdl_node, + &mut pane_template, + pane_template_kdl_node, + )?; + pane_template.run = Run::merge(&pane_template.run, &run); + if let Some(pane_template_run_command) = pane_template.run.as_mut() { + // we need to do this because panes consuming a pane_template + // can have bare args without a command + pane_template_run_command.add_args(args); + pane_template_run_command.add_close_on_exit(close_on_exit); + pane_template_run_command.add_start_suspended(start_suspended); + }; + if let Some(borderless) = borderless { + pane_template.borderless = borderless; + } + if let Some(focus) = focus { + pane_template.focus = Some(focus); + } + if let Some(name) = name { + pane_template.name = Some(name); + } + if let Some(split_size) = split_size { + pane_template.split_size = Some(split_size); + } + if let Some(index_of_children) = pane_template.external_children_index { + pane_template + .children + .insert(index_of_children, TiledPaneLayout::default()); + } + pane_template.external_children_index = None; + Ok(pane_template) + }, + PaneOrFloatingPane::FloatingPane(_) => { + let pane_template_name = kdl_get_string_property_or_child_value_with_error!( + pane_template_kdl_node, + "name" + ) + .map(|name| name.to_string()); + Err(ConfigError::new_layout_kdl_error( + format!("pane_template {}, is a floating pane template (derived from its properties) and cannot be applied to a tiled pane", pane_template_name.unwrap_or("".into())), + kdl_node.span().offset(), + kdl_node.span().len(), + )) + }, + } + } + fn parse_floating_pane_node_with_template( + &self, + kdl_node: &KdlNode, + pane_template: PaneOrFloatingPane, + pane_template_kdl_node: &KdlNode, + ) -> Result<FloatingPaneLayout, ConfigError> { + match pane_template { + PaneOrFloatingPane::Pane(_) => { + let pane_template_name = kdl_get_string_property_or_child_value_with_error!( + pane_template_kdl_node, + "name" + ) + .map(|name| name.to_string()); + Err(ConfigError::new_layout_kdl_error( + format!("pane_template {}, is a non-floating pane template (derived from its properties) and cannot be applied to a floating pane", pane_template_name.unwrap_or("".into())), + kdl_node.span().offset(), + kdl_node.span().len(), + )) + }, + PaneOrFloatingPane::FloatingPane(mut pane_template) => { + let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus"); + let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name") + .map(|name| name.to_string()); + let args = self.parse_args(kdl_node)?; + let close_on_exit = + kdl_get_bool_property_or_child_value_with_error!(kdl_node, "close_on_exit"); + let start_suspended = + kdl_get_bool_property_or_child_value_with_error!(kdl_node, "start_suspended"); + let run = self.parse_command_plugin_or_edit_block_for_template(kdl_node)?; + self.assert_no_bare_attributes_in_pane_node_with_template( + &run, + &pane_template.run, + &args, + &close_on_exit, + &start_suspended, + kdl_node, + )?; + pane_template.run = Run::merge(&pane_template.run, &run); + if let Some(pane_template_run_command) = pane_template.run.as_mut() { + // we need to do this because panes consuming a pane_template + // can have bare args without a command + pane_template_run_command.add_args(args); + pane_template_run_command.add_close_on_exit(close_on_exit); + pane_template_run_command.add_start_suspended(start_suspended); + }; + if let Some(focus) = focus { + pane_template.focus = Some(focus); + } + if let Some(name) = name { + pane_template.name = Some(name); + } + let height = self.parse_percent_or_fixed(kdl_node, "height", false)?; + let width = self.parse_percent_or_fixed(kdl_node, "width", false)?; + let x = self.parse_percent_or_fixed(kdl_node, "x", true)?; + let y = self.parse_percent_or_fixed(kdl_node, "y", true)?; + // let mut floating_pane = FloatingPaneLayout::from(&pane_template); + if let Some(height) = height { + pane_template.height = Some(height); + } + if let Some(width) = width { + pane_template.width = Some(width); + } + if let Some(y) = y { + pane_template.y = Some(y); + } + if let Some(x) = x { + pane_template.x = Some(x); + } + Ok(pane_template) + }, + PaneOrFloatingPane::Either(mut pane_template) => { + let focus = kdl_get_bool_property_or_child_value_with_error!(kdl_node, "focus"); + let name = kdl_get_string_property_or_child_value_with_error!(kdl_node, "name") + .map(|name| name.to_string()); + let args = self.parse_args(kdl_node)?; + let close_on_exit = + kdl_get_bool_property_or_child_value_with_error!(kdl_node, "close_on_exit"); + let start_suspended = + kdl_get_bool_property_or_child_value_with_error!(kdl_node, "start_suspended"); + let run = self.parse_command_plugin_or_edit_block_for_template(kdl_node)?; + self.assert_no_bare_attributes_in_pane_node_with_template( + &run, + &pane_template.run, + &args, + &close_on_exit, + &start_suspended, + kdl_node, + )?; + pane_template.run = Run::merge(&pane_template.run, &run); + if let Some(pane_template_run_command) = pane_template.run.as_mut() { + // we need to do this because panes consuming a pane_template + // can |