From 69371d6470f794c065296af117bbc528a313161f Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sun, 18 May 2025 18:02:38 +0200 Subject: [PATCH] xkb: add ComposeTable::feed2 API --- compose-tests/src/main.rs | 28 +- .../testcases/t00/t005/t0050/XCompose | 3 + .../testcases/t00/t005/t0050/expected.txt | 7 + .../testcases/t00/t005/t0050/input.txt | 3 + .../testcases/t00/t005/t0051/XCompose | 3 + .../testcases/t00/t005/t0051/expected.txt | 7 + .../testcases/t00/t005/t0051/input.txt | 3 + .../testcases/t00/t005/t0052/XCompose | 3 + .../testcases/t00/t005/t0052/expected.txt | 9 + .../testcases/t00/t005/t0052/input.txt | 4 + .../testcases/t00/t005/t0053/XCompose | 3 + .../testcases/t00/t005/t0053/expected.txt | 5 + .../testcases/t00/t005/t0053/input.txt | 2 + .../testcases/t00/t005/t0054/XCompose | 3 + .../testcases/t00/t005/t0054/expected.txt | 6 + .../testcases/t00/t005/t0054/input.txt | 2 + .../testcases/t00/t005/t0055/XCompose | 1 + .../testcases/t00/t005/t0055/expected.txt | 3 + .../testcases/t00/t005/t0055/input.txt | 1 + kbvm/release-notes.md | 6 +- kbvm/src/xkb/compose/table.rs | 559 ++++++++++++++---- kbvm/src/xkb/compose/table/tests.rs | 44 +- 22 files changed, 593 insertions(+), 112 deletions(-) create mode 100644 compose-tests/testcases/t00/t005/t0050/XCompose create mode 100644 compose-tests/testcases/t00/t005/t0050/expected.txt create mode 100644 compose-tests/testcases/t00/t005/t0050/input.txt create mode 100644 compose-tests/testcases/t00/t005/t0051/XCompose create mode 100644 compose-tests/testcases/t00/t005/t0051/expected.txt create mode 100644 compose-tests/testcases/t00/t005/t0051/input.txt create mode 100644 compose-tests/testcases/t00/t005/t0052/XCompose create mode 100644 compose-tests/testcases/t00/t005/t0052/expected.txt create mode 100644 compose-tests/testcases/t00/t005/t0052/input.txt create mode 100644 compose-tests/testcases/t00/t005/t0053/XCompose create mode 100644 compose-tests/testcases/t00/t005/t0053/expected.txt create mode 100644 compose-tests/testcases/t00/t005/t0053/input.txt create mode 100644 compose-tests/testcases/t00/t005/t0054/XCompose create mode 100644 compose-tests/testcases/t00/t005/t0054/expected.txt create mode 100644 compose-tests/testcases/t00/t005/t0054/input.txt create mode 100644 compose-tests/testcases/t00/t005/t0055/XCompose create mode 100644 compose-tests/testcases/t00/t005/t0055/expected.txt create mode 100644 compose-tests/testcases/t00/t005/t0055/input.txt diff --git a/compose-tests/src/main.rs b/compose-tests/src/main.rs index d17b6e78..7eea137f 100644 --- a/compose-tests/src/main.rs +++ b/compose-tests/src/main.rs @@ -16,7 +16,7 @@ use { thiserror::Error, }; -// const SINGLE: Option<&str> = Some("t0019"); +// const SINGLE: Option<&str> = Some("t0054"); const SINGLE: Option<&str> = None; const WRITE_MISSING: bool = true; const WRITE_FAILED: bool = false; @@ -76,6 +76,8 @@ enum ResultError { WriteActualFailed(#[source] io::Error), #[error("could not create compose table")] CreateComposeTable, + #[error("unknown modifier `{0}`")] + UnknownModifier(String), } fn test_case2(diagnostics: &mut Vec, case: &Path) -> Result<(), ResultError> { @@ -115,16 +117,32 @@ fn test_case2(diagnostics: &mut Vec, case: &Path) -> Result<(), Resu if let Some((pre, _)) = line.split_once("#") { line = pre; } - line = line.trim(); - if line.is_empty() { - continue; + let mut iter = line.split_whitespace(); + match iter.next() { + Some(l) => line = l, + _ => continue, + } + let mut accept = None; + for modifier in iter { + match modifier { + "use" => accept = Some(true), + "skip" => accept = Some(false), + _ => return Err(ResultError::UnknownModifier(modifier.to_string())), + } } let Some(keysym) = Keysym::from_str(line) else { return Err(ResultError::UnknownKeysym(line.to_string())); }; - let Some(res) = table.feed(&mut state, keysym) else { + let Some(mut out) = table.feed2(&mut state, keysym) else { continue; }; + if let Some(accept) = accept { + match accept { + true => out.use_intermediate_composed(), + false => out.skip_intermediate_composed(), + } + } + let res = out.apply(); match res { FeedResult::Pending => actual.push_str(" pending\n"), FeedResult::Aborted => actual.push_str(" aborted\n"), diff --git a/compose-tests/testcases/t00/t005/t0050/XCompose b/compose-tests/testcases/t00/t005/t0050/XCompose new file mode 100644 index 00000000..387cea0e --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0050/XCompose @@ -0,0 +1,3 @@ + : A +: B + : C diff --git a/compose-tests/testcases/t00/t005/t0050/expected.txt b/compose-tests/testcases/t00/t005/t0050/expected.txt new file mode 100644 index 00000000..21d5c73a --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0050/expected.txt @@ -0,0 +1,7 @@ +a + pending +a + pending +a + composed + C diff --git a/compose-tests/testcases/t00/t005/t0050/input.txt b/compose-tests/testcases/t00/t005/t0050/input.txt new file mode 100644 index 00000000..16f18f3a --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0050/input.txt @@ -0,0 +1,3 @@ +a +a +a diff --git a/compose-tests/testcases/t00/t005/t0051/XCompose b/compose-tests/testcases/t00/t005/t0051/XCompose new file mode 100644 index 00000000..4a051275 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0051/XCompose @@ -0,0 +1,3 @@ + : A + : C +: B diff --git a/compose-tests/testcases/t00/t005/t0051/expected.txt b/compose-tests/testcases/t00/t005/t0051/expected.txt new file mode 100644 index 00000000..0a022209 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0051/expected.txt @@ -0,0 +1,7 @@ +a skip + pending +a + pending +a + composed + C diff --git a/compose-tests/testcases/t00/t005/t0051/input.txt b/compose-tests/testcases/t00/t005/t0051/input.txt new file mode 100644 index 00000000..4bf8e7db --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0051/input.txt @@ -0,0 +1,3 @@ +a skip +a +a diff --git a/compose-tests/testcases/t00/t005/t0052/XCompose b/compose-tests/testcases/t00/t005/t0052/XCompose new file mode 100644 index 00000000..78b256d9 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0052/XCompose @@ -0,0 +1,3 @@ + : C + : A +: B diff --git a/compose-tests/testcases/t00/t005/t0052/expected.txt b/compose-tests/testcases/t00/t005/t0052/expected.txt new file mode 100644 index 00000000..0f0374e6 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0052/expected.txt @@ -0,0 +1,9 @@ +a + composed + B + +a skip + pending +a + composed + A diff --git a/compose-tests/testcases/t00/t005/t0052/input.txt b/compose-tests/testcases/t00/t005/t0052/input.txt new file mode 100644 index 00000000..78714652 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0052/input.txt @@ -0,0 +1,4 @@ +a + +a skip +a diff --git a/compose-tests/testcases/t00/t005/t0053/XCompose b/compose-tests/testcases/t00/t005/t0053/XCompose new file mode 100644 index 00000000..d978cfbf --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0053/XCompose @@ -0,0 +1,3 @@ +: B + : A + : C diff --git a/compose-tests/testcases/t00/t005/t0053/expected.txt b/compose-tests/testcases/t00/t005/t0053/expected.txt new file mode 100644 index 00000000..25daed9a --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0053/expected.txt @@ -0,0 +1,5 @@ +a + pending +a use + composed + A diff --git a/compose-tests/testcases/t00/t005/t0053/input.txt b/compose-tests/testcases/t00/t005/t0053/input.txt new file mode 100644 index 00000000..b528c63f --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0053/input.txt @@ -0,0 +1,2 @@ +a +a use diff --git a/compose-tests/testcases/t00/t005/t0054/XCompose b/compose-tests/testcases/t00/t005/t0054/XCompose new file mode 100644 index 00000000..78b256d9 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0054/XCompose @@ -0,0 +1,3 @@ + : C + : A +: B diff --git a/compose-tests/testcases/t00/t005/t0054/expected.txt b/compose-tests/testcases/t00/t005/t0054/expected.txt new file mode 100644 index 00000000..bec74964 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0054/expected.txt @@ -0,0 +1,6 @@ +a + composed + B +a + composed + B diff --git a/compose-tests/testcases/t00/t005/t0054/input.txt b/compose-tests/testcases/t00/t005/t0054/input.txt new file mode 100644 index 00000000..7e8a1653 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0054/input.txt @@ -0,0 +1,2 @@ +a +a diff --git a/compose-tests/testcases/t00/t005/t0055/XCompose b/compose-tests/testcases/t00/t005/t0055/XCompose new file mode 100644 index 00000000..5bf3e4e5 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0055/XCompose @@ -0,0 +1 @@ +: X diff --git a/compose-tests/testcases/t00/t005/t0055/expected.txt b/compose-tests/testcases/t00/t005/t0055/expected.txt new file mode 100644 index 00000000..3ef260df --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0055/expected.txt @@ -0,0 +1,3 @@ +a skip + composed + X diff --git a/compose-tests/testcases/t00/t005/t0055/input.txt b/compose-tests/testcases/t00/t005/t0055/input.txt new file mode 100644 index 00000000..2e93a8e0 --- /dev/null +++ b/compose-tests/testcases/t00/t005/t0055/input.txt @@ -0,0 +1 @@ +a skip diff --git a/kbvm/release-notes.md b/kbvm/release-notes.md index e707f1ac..aac2034e 100644 --- a/kbvm/release-notes.md +++ b/kbvm/release-notes.md @@ -74,7 +74,7 @@ - Added support for `VoidAction()`. This is an alias for `LockControls(controls=none, affect=neither)`. - In compose files, later productions now always override earlier productions. - That is, all of the following are the same: + That is, all of the following are the same in the `ComposeTable::feed` API. ```compose : X @@ -90,6 +90,10 @@ : Y : X ``` +- A new function `ComposeTable::feed2` has been added to give more control over + situations such as the above. In the last example above, after inputting + ` `, the application can choose to either use the `X` output or proceed + to the next possible sequence. # 0.1.4 (2025-04-21) diff --git a/kbvm/src/xkb/compose/table.rs b/kbvm/src/xkb/compose/table.rs index fe159e49..2480a4cb 100644 --- a/kbvm/src/xkb/compose/table.rs +++ b/kbvm/src/xkb/compose/table.rs @@ -39,15 +39,31 @@ pub(crate) struct Payload { #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Node { pub(crate) keysym: Keysym, - data: [u32; 2], + data: [u32; 4], } -#[derive(Clone, PartialEq)] -enum NodeType { - /// The range is the range of the children of this node in the table. - Intermediate { range: Range }, - /// The payload is the index of the payload in the table. - Leaf { payload: usize }, +#[derive(Debug, Clone, PartialEq)] +struct NodeData { + children: Range, + payload: Option, + is_old_terminal: bool, +} + +#[derive(Debug)] +struct NodePayload<'a> { + payload: &'a Payload, + is_old_terminal: bool, +} + +#[derive(Debug)] +enum NodeType<'a> { + Intermediate { + children: Range, + payload: Option>, + }, + Leaf { + payload: &'a Payload, + }, } /// A compose table. @@ -76,6 +92,60 @@ enum NodeType { /// Some(Pending) /// Some(Composed { string: Some("´"), keysym: Some(acute) }) /// ``` +/// +/// # Data Structure +/// +/// This type represents a tree parsed from one or more compose files. For example, the compose file +/// +/// ```compose +/// : "foo" +/// : "bar" +/// : "baz" +/// : "yolo" +/// ``` +/// +/// would produce the following tree (with an implicit root node): +/// +/// ```txt +/// ┌────────┐ ┌───┐ +/// │ │ ││ +/// │ │ └─┬─┘ +/// │-> "foo"│ │ +/// └────┬───┘ │ +/// │ │ +/// ┌─────────┴───────┐ │ +/// ▼ ▼ ▼ +/// ┌────────┐ ┌───┐ ┌─────────┐ +/// │ │ ││ │ │ +/// │ │ └─┬─┘ │ │ +/// │-> "bar"│ │ │-> "yolo"│ +/// └────────┘ │ └─────────┘ +/// │ +/// │ +/// ▼ +/// ┌────────┐ +/// │ │ +/// │ │ +/// │-> "baz"│ +/// └────────┘ +/// ``` +/// +/// A [`State`] object is a pointer into this tree, encoding the current position. Feeding a +/// [`Keysym`] into the [`State`] advances the [`State`] to the next child or resets it to the root +/// node if no continuation exists. +/// +/// # Intermediate Nodes with Data +/// +/// Classic compose APIs do not support non-leaf nodes producing an output. In the example above, +/// the top-left `` node is such a node. Those APIs behave as if the `` node did not produce +/// an output. +/// +/// This is the behavior of the [`ComposeTable::feed`] function. If more control is desired, then +/// the [`ComposeTable::feed2`] function should be used instead. It allows the application decide +/// if the intermediate node should +/// +/// - be treated as a leaf node, and the state be reset to the root node, or +/// - be treated as an intermediate node without an output. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ComposeTable { nodes: Box<[Node]>, @@ -110,28 +180,251 @@ pub enum FeedResult<'a> { }, } -impl NodeType { - fn serialize(self) -> [u32; 2] { - // This is fine because we ensure that there are no more than u32::MAX nodes in - // the table and we generally assume that usize >= u32. - match self { - NodeType::Intermediate { range } => [range.start as u32, range.end as u32], - NodeType::Leaf { payload } => [!0, payload as u32], +/// The output of a [`ComposeTable::feed2`] call. +/// +/// Dropping this object without calling [`Self::apply`] does not update the state. +#[derive(Debug)] +pub struct Feed2Result<'a, 'b> { + state: &'b mut State, + table: &'a ComposeTable, + node: Option>, + accept: Option, +} + +impl<'a> Feed2Result<'a, '_> { + /// Returns the continuation-state of the node. + /// + /// This function returns `None` if no node was found or the node is not an intermediate node. + /// + /// # Example + /// + /// ```compose + /// : X + /// : Y + /// ``` + /// + /// After feeding ``, this function returns `Some`. + /// + /// After feeding ``, this function returns `None`. + /// + /// ```compose + /// : Y + /// ``` + /// + /// After feeding ``, this function returns `None`. + pub fn continuation(&self) -> Option { + let node = self.node.as_ref()?; + match node { + NodeType::Intermediate { children, .. } => Some(State { + range: children.clone(), + }), + NodeType::Leaf { .. } => None, } } -} -impl Node { - fn deserialize(&self) -> NodeType { - if self.data[0] == !0 { - NodeType::Leaf { - payload: self.data[1] as usize, + /// Returns the output produced by the node. + /// + /// If no node was found, [`FeedResult::Aborted`] is returned. If the node produces no output + /// [`FeedResult::Pending`] is returned. Otherwise this function returns + /// [`FeedResult::Composed`]. + /// + /// If this function return [`FeedResult::Composed`], then [`Feed2Result::continuation`] can be + /// used to determine if this is an intermediate node or a leaf node. + /// + /// # Example + /// + /// ```compose + /// : X + /// : Y + /// ``` + /// + /// After feeding ``, this function returns `X`. + /// + /// ```compose + /// : Y + /// ``` + /// + /// After feeding ``, this function returns [`FeedResult::Pending`]. + pub fn output(&self) -> FeedResult<'a> { + let Some(node) = &self.node else { + return FeedResult::Aborted; + }; + match node { + NodeType::Intermediate { + payload: Some(NodePayload { payload, .. }), + .. } - } else { + | NodeType::Leaf { payload } => payload.to_composed(), + NodeType::Intermediate { payload: None, .. } => FeedResult::Pending, + } + } + + /// Returns whether the selected node is a terminal in the xkbcommon API. + /// + /// This function returns true if the node is a leaf node or it is an intermediate node and its + /// production was declared after all productions of all child nodes. + /// + /// # Examples + /// + /// ```compose + /// : X + /// : Y + /// ``` + /// + /// After feeding ``, this function returns `false`. + /// + /// ```compose + /// : Y + /// : X + /// ``` + /// + /// After feeding ``, this function returns `true`. + pub fn is_classic_terminal(&self) -> bool { + let Some(node) = &self.node else { + return false; + }; + matches!( + node, NodeType::Intermediate { - range: (self.data[0] as usize)..(self.data[1] as usize), + payload: Some(NodePayload { + is_old_terminal: true, + .. + }), + .. + } | NodeType::Leaf { .. } + ) + } + + /// Uses the output of an intermediate node and resets the state to the initial state. + /// + /// This function has no effect if the selected node is not an intermediate node with output. + /// + /// This function only configures this object. The effect is not applied until + /// [`Feed2Result::apply`] is called. + /// + /// # Example + /// + /// ```compose + /// : X + /// : Y + /// ``` + /// + /// After feeding ``, calling this function causes the result to be [`FeedResult::Composed`]. + pub fn use_intermediate_composed(&mut self) { + self.accept = Some(true); + } + + /// Skips the output of an intermediate node and proceeds to its children. + /// + /// This function has no effect if the selected node is not an intermediate node with output. + /// + /// This function only configures this object. The effect is not applied until + /// [`Feed2Result::apply`] is called. + /// + /// # Example + /// + /// ```compose + /// : X + /// : Y + /// ``` + /// + /// After feeding ``, calling this function causes the result to be [`FeedResult::Pending`] + /// and the state to accept `` as the next input. + pub fn skip_intermediate_composed(&mut self) { + self.accept = Some(false); + } + + /// Updates the state and returns its result. + /// + /// If no matching node was found, the state is reset to the initial state and the result is + /// [`FeedResult::Aborted`]. + /// + /// Otherwise, if the node is a leaf node, the state is reset to the initial state and the + /// result is [`FeedResult::Composed`]. + /// + /// Otherwise, if the node is an intermediate node without output, the state is set to the node + /// and the result is [`FeedResult::Pending`]. + /// + /// Otherwise the node is an intermediate node with output and the behavior is as follows: + /// + /// - If [`Feed2Result::use_intermediate_composed`] was called, the state is reset to the + /// initial state and the result is [`FeedResult::Composed`]. + /// - If [`Feed2Result::skip_intermediate_composed`] was called, the state is set to the node + /// and the result is [`FeedResult::Pending`]. + /// - If neither function was called: + /// - If the production for the output was declared after all productions for any of the child + /// nodes, the state is reset to the initial state and the result is + /// [`FeedResult::Composed`]. + /// - Otherwise, the state is set to the node and the result is [`FeedResult::Pending`]. + /// + /// If [`Feed2Result::use_intermediate_composed`] and [`Feed2Result::skip_intermediate_composed`] + /// are both called, only the last function call is considered. + pub fn apply(self) -> FeedResult<'a> { + let Some(node) = &self.node else { + *self.state = self.table.create_state(); + return FeedResult::Aborted; + }; + match node { + NodeType::Leaf { payload } => { + *self.state = self.table.create_state(); + payload.to_composed() + } + NodeType::Intermediate { + children, + payload: None, + } => { + self.state.range = children.clone(); + FeedResult::Pending + } + NodeType::Intermediate { + children, + payload: Some(pl), + } => { + let accept = self.accept.unwrap_or(pl.is_old_terminal); + match accept { + true => { + *self.state = self.table.create_state(); + pl.payload.to_composed() + } + false => { + self.state.range = children.clone(); + FeedResult::Pending + } + } + } + } + } +} + +const HAS_PAYLOAD: u32 = 1 << 0; +const IS_OLD_TERMINAL: u32 = 1 << 1; + +impl NodeData { + fn serialize(self) -> [u32; 4] { + let mut res = [0; 4]; + res[0] = self.children.start as u32; + res[1] = self.children.end as u32; + if let Some(payload) = self.payload { + res[2] = payload as u32; + res[3] |= HAS_PAYLOAD; + if self.is_old_terminal { + res[3] |= IS_OLD_TERMINAL; } } + res + } +} + +impl Node { + fn deserialize(&self) -> NodeData { + let flags = self.data[3]; + let has_payload = flags & HAS_PAYLOAD != 0; + let is_old_terminal = flags & IS_OLD_TERMINAL != 0; + NodeData { + children: (self.data[0] as usize)..(self.data[1] as usize), + payload: has_payload.then_some(self.data[2] as usize), + is_old_terminal, + } } } @@ -144,6 +437,7 @@ impl ComposeTable { struct PreData { step_range: Range, production: Option, + is_old_terminal: bool, } // the next step will add all prefixes of all productions to these vectors. @@ -191,11 +485,14 @@ impl ComposeTable { pre_datas.push(PreData { step_range: start..steps.len(), production: None, + is_old_terminal: false, }); } // unwrap is fine because the parser guarantees that each production has at // least 1 step. - pre_datas.last_mut().unwrap().production = Some(idx); + let last = pre_datas.last_mut().unwrap(); + last.production = Some(idx); + last.is_old_terminal = true; } // this sort is stable and therefore preserves the order of compose rules that @@ -224,17 +521,6 @@ impl ComposeTable { // the duplicates are removed by the next step pre_datas.sort_by_key(|k| &steps[k.step_range.clone()]); - let mut warn_duplicate = |pre_data: &PreData| { - if let Some(pl) = pre_data.production { - let pl = &productions[pl]; - diagnostics.push( - map, - DiagnosticKind::ComposeProductionOverwritten, - ad_hoc_display!("compose production has been overwritten").spanned2(pl.span), - ); - } - }; - // this step deduplicates events. there are two scenarios: // // 1. for each list of steps, we choose the last PreData with this list @@ -253,29 +539,30 @@ impl ComposeTable { // : V // : (no production) // : J - let mut pre_datas_dedup = Vec::<&PreData>::new(); + let mut pre_datas_dedup = Vec::<&mut PreData>::new(); let mut prev_step = None; let mut prev_len = 0; - let mut prev_production = false; - for k in &pre_datas { + for k in &mut pre_datas { let len = k.step_range.len(); - // NOTE: This is untested because the lookup logic always terminates anyway - // if it finds a production. So this is just an optimization. - if prev_production && len > prev_len { - warn_duplicate(k); - continue; - } let step = steps[k.step_range.end - 1]; if (Some(step), len) == (prev_step, prev_len) { - // NOTE: This is untested because the stdlib binary search always chooses - // the last matching entry. - if let Some(prev) = pre_datas_dedup.pop() { - warn_duplicate(prev); + let prev = pre_datas_dedup.pop().unwrap(); + if k.production.is_some() { + if let Some(pl) = prev.production { + let pl = &productions[pl]; + diagnostics.push( + map, + DiagnosticKind::ComposeProductionOverwritten, + ad_hoc_display!("compose production has been overwritten") + .spanned2(pl.span), + ); + } + } else { + k.production = prev.production; } } prev_step = Some(step); prev_len = len; - prev_production = k.production.is_some(); pre_datas_dedup.push(k); } @@ -287,6 +574,7 @@ impl ComposeTable { heap_pos: Cell, children_heap_pos: Cell, next_child_pos: Cell, + is_old_terminal: bool, } // the next steps deduplicates the contents of pre_datas and counts for each entry @@ -336,6 +624,7 @@ impl ComposeTable { heap_pos: Cell::new(0), children_heap_pos: Cell::new(0), next_child_pos: Cell::new(0), + is_old_terminal: pre_data.is_old_terminal, }); } @@ -384,17 +673,20 @@ impl ComposeTable { // the next step converts the datas to the final node structure let mut payloads = vec![]; let mut nodes = Vec::with_capacity(datas.len() + 1); + let root_children = 1..1 + num_root; // the root node nodes.push(Node { keysym: Default::default(), - data: NodeType::Intermediate { - range: 1..1 + num_root, + data: NodeData { + children: root_children.clone(), + payload: None, + is_old_terminal: false, } .serialize(), }); for data in datas { - let ty = match data.production { + let payload = match data.production { Some(idx) => { let production = &productions[idx]; let pos = payloads.len(); @@ -406,13 +698,21 @@ impl ComposeTable { .map(|s| s.as_bstr().to_string().into_boxed_str()), keysym: production.val.keysym, }); - NodeType::Leaf { payload: pos } - } - _ => { - let lo = data.children_heap_pos.get() as usize; - let hi = lo + data.num_children as usize; - NodeType::Intermediate { range: lo..hi } + Some(pos) } + _ => None, + }; + let children = if data.num_children > 0 { + let lo = data.children_heap_pos.get() as usize; + let hi = lo + data.num_children as usize; + lo..hi + } else { + root_children.clone() + }; + let ty = NodeData { + children, + payload, + is_old_terminal: data.is_old_terminal, }; nodes.push(Node { keysym: data.step.keysym, @@ -430,17 +730,51 @@ impl ComposeTable { /// /// The returned state is in the initial state. pub fn create_state(&self) -> State { - let NodeType::Intermediate { range } = self.nodes[0].deserialize() else { - unreachable!(); - }; - State { range } + State { + range: self.nodes[0].deserialize().children, + } } - /// Advance the compose state. + /// Advances the compose state. /// /// The `state` should have been created by [`Self::create_state`]. Otherwise this function /// might panic. /// + /// This function is the same as calling [`Self::feed2`] immediately followed by + /// [`Feed2Result::apply`]. This corresponds to the behavior of xkbcommon. + /// + /// This function does not support productions were one is a prefix of another. For example, the + /// following compose files behave the same with this function: + /// + /// ```compose + /// : X + /// ``` + /// + /// ```compose + /// : Y + /// : X + /// ``` + /// + /// ```compose + /// : Y + /// : Y + /// : X + /// ``` + /// + /// If more control is needed over the behavior of overlapping productions, [`Self::feed2`] + /// should be used instead. + pub fn feed(&self, state: &mut State, sym: Keysym) -> Option> { + self.feed2(state, sym).map(|o| o.apply()) + } + + /// Finds the next step in a compose sequence. + /// + /// The `state` should have been created by [`Self::create_state`]. Otherwise this function + /// might panic. + /// + /// This function does not update the `state`. The state can be updated by calling + /// [`Feed2Result::apply`]. + /// /// This function returns `None` if the call had no effect. This happens in two /// situations: /// @@ -448,43 +782,48 @@ impl ComposeTable { /// - The state is in the initial state (that is, there is no ongoing compose /// sequence) and the keysym does not start a compose sequence. /// - /// Otherwise, this function returns a [`FeedResult`] as follows: - /// - /// - If the keysym/modifiers combination matches no candidate, - /// [`FeedResult::Aborted`] is returned and the state is reset to the initial state. - /// - Otherwise, if the candidate completes the compose sequence, - /// [`FeedResult::Composed`] is returned with the output and `state` is reset to the - /// initial state. - /// - Otherwise, [`FeedResult::Pending`] is returned and `state` is updated to match - /// the new pending state. - pub fn feed(&self, state: &mut State, sym: Keysym) -> Option> { + /// Otherwise, the [`Feed2Result`] can be used to inspect the selected node. See the + /// documentation of [`ComposeTable`] for a description of nodes. + pub fn feed2<'a, 'b>( + &'a self, + state: &'b mut State, + sym: Keysym, + ) -> Option> { if sym.is_modifier() { return None; } let range = &self.nodes[state.range.clone()]; - if let Ok(node) = range.binary_search_by_key(&sym, |n| n.keysym) { - let node = &range[node]; - let res = match node.deserialize() { - NodeType::Intermediate { range } => { - state.range = range; - FeedResult::Pending - } - NodeType::Leaf { payload } => { - *state = self.create_state(); - let payload = &self.payloads[payload]; - FeedResult::Composed { - string: payload.string.as_deref(), - keysym: payload.keysym, + let node = match range.binary_search_by_key(&sym, |n| n.keysym) { + Ok(node) => { + let node = range[node].deserialize(); + let ty = if node.children.start > 1 { + NodeType::Intermediate { + children: node.children.clone(), + payload: node.payload.map(|pl| NodePayload { + payload: &self.payloads[pl], + is_old_terminal: node.is_old_terminal, + }), + } + } else { + NodeType::Leaf { + payload: &self.payloads[node.payload.unwrap()], } + }; + Some(ty) + } + Err(..) => { + if state.range.start == 1 { + return None; } - }; - return Some(res); - } - if state.range.start == 1 { - return None; - } - *state = self.create_state(); - Some(FeedResult::Aborted) + None + } + }; + Some(Feed2Result { + state, + table: self, + node, + accept: None, + }) } /// Creates an iterator over the rules in this table. @@ -525,6 +864,15 @@ impl ComposeTable { } } +impl Payload { + fn to_composed(&self) -> FeedResult<'_> { + FeedResult::Composed { + string: self.string.as_deref(), + keysym: self.keysym, + } + } +} + /// A matching step in a [`MatchRule`]. /// /// # Example @@ -655,7 +1003,6 @@ pub struct Iter<'a> { impl<'a> Iter<'a> { /// Returns the next element of the iterator. pub fn next(&mut self) -> Option> { - self.stack.pop(); loop { let mut range = self.child_range.pop()?; let Some(idx) = range.next() else { @@ -665,17 +1012,19 @@ impl<'a> Iter<'a> { self.child_range.push(range); let node = &self.table.nodes[idx]; self.stack.push(MatchStep { node }); - match node.deserialize() { - NodeType::Intermediate { range } => { - self.child_range.push(range); - } - NodeType::Leaf { payload } => { - let payload = &self.table.payloads[payload]; - return Some(MatchRule { - steps: &self.stack, - payload, - }); - } + let data = node.deserialize(); + let child_range = if data.children.start > 1 { + data.children + } else { + 0..0 + }; + self.child_range.push(child_range); + if let Some(payload) = data.payload { + let payload = &self.table.payloads[payload]; + return Some(MatchRule { + steps: &self.stack, + payload, + }); } } } diff --git a/kbvm/src/xkb/compose/table/tests.rs b/kbvm/src/xkb/compose/table/tests.rs index d892383b..196e787a 100644 --- a/kbvm/src/xkb/compose/table/tests.rs +++ b/kbvm/src/xkb/compose/table/tests.rs @@ -1,6 +1,6 @@ use crate::{ syms, - xkb::{diagnostic::WriteToStderr, Context}, + xkb::{compose::FeedResult, diagnostic::WriteToStderr, Context}, }; #[test] @@ -54,6 +54,15 @@ fn iter() { assert_eq!(r.string(), None); assert_eq!(r.keysym(), Some(syms::b)); } + { + let r = iter.next().unwrap(); + assert_eq!( + r.steps().iter().map(|s| s.keysym()).collect::>(), + [syms::q], + ); + assert_eq!(r.string(), None); + assert_eq!(r.keysym(), Some(syms::q)); + } { let r = iter.next().unwrap(); assert_eq!( @@ -65,3 +74,36 @@ fn iter() { } assert!(iter.next().is_none()); } + +#[test] +fn old_feed() { + const COMPOSE: &str = r#" + : x + : y + + : y + : x + "#; + let context = Context::default(); + let mut builder = context.compose_table_builder(); + builder.buffer(COMPOSE); + let table = builder.build(WriteToStderr).unwrap(); + let mut state = table.create_state(); + + assert_eq!(table.feed(&mut state, syms::a), Some(FeedResult::Pending)); + assert_eq!( + table.feed(&mut state, syms::a), + Some(FeedResult::Composed { + string: None, + keysym: Some(syms::y) + }) + ); + + assert_eq!( + table.feed(&mut state, syms::b), + Some(FeedResult::Composed { + string: None, + keysym: Some(syms::x) + }) + ); +}