From 99797918bc63b664ef522786065fe85ce76b2e79 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Tue, 22 Sep 2026 21:05:25 -0700 Subject: [PATCH] WIP: project the evaluated value; yank copies it Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze --- crates/improvise-command/src/ui/project.rs | 2 ++ crates/improvise-protocol/src/render.rs | 14 ++++++++++---- crates/improvise-protocol/src/resolve.rs | 9 ++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/improvise-command/src/ui/project.rs b/crates/improvise-command/src/ui/project.rs index 7d33657b..441af17e 100644 --- a/crates/improvise-command/src/ui/project.rs +++ b/crates/improvise-command/src/ui/project.rs @@ -282,6 +282,7 @@ fn cell_display( numeric: matches!(value, Some(CellValue::Number(_))), error: matches!(value, Some(CellValue::Error(_))), id: cell_id(app, layout, row, col), + value: value.clone(), }; } let key = layout.cell_key(row, col); @@ -293,6 +294,7 @@ fn cell_display( // The *unformatted* value the TUI seeds its edit buffer from. Projecting it // beats a client un-formatting `text` (gotcha 47). edit_text: key.map(|key| app.display_value_in(layout, &key)), + value: value.clone(), } } diff --git a/crates/improvise-protocol/src/render.rs b/crates/improvise-protocol/src/render.rs index dbcad0d1..57f0d0c5 100644 --- a/crates/improvise-protocol/src/render.rs +++ b/crates/improvise-protocol/src/render.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use unicode_width::UnicodeWidthStr; +use improvise_core::model::cell::CellValue; use improvise_core::model::record::{AggregateHash, RecordHash}; use improvise_core::view::Axis; use improvise_core::view::layout::PageAxis; @@ -64,9 +65,9 @@ pub enum CreateKind { Category, } -/// One cell as the client draws it. Deliberately not a `CellValue` — the +/// One cell as the client draws it. Drawn from `text`, never from `value` — the /// formatting has already happened, and re-deriving it is how ends drift. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct CellDisplay { pub text: String, /// Right-align numerics, left-align text — decided by the server, which @@ -80,6 +81,9 @@ pub struct CellDisplay { /// What an editor should start with — the value as the user last /// **typed** it, not as it is drawn. The difference corrupts data (§3). pub edit_text: Option, + /// The value the host evaluated here, unrounded and untyped-by-text: what + /// `yank` copies. Neither `text` nor `edit_text` can be re-parsed into it (§3). + pub value: Option, } impl CellDisplay { @@ -98,6 +102,7 @@ impl CellDisplay { numeric: true, error: false, id: None, + value: None, } } @@ -110,6 +115,7 @@ impl CellDisplay { error: true, id: None, edit_text: None, + value: None, } } @@ -416,7 +422,7 @@ fn shows(items: &[&Vec], idx: usize, first_rendered: usize, level: usize /// The grid widget's copy of the visible cells (improvise-n10), indexed by /// position within the **viewport** and never by `CellKey`. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct GridCache { /// The row axis's frame: levels, label tuples and groups. **Not** a list /// of strings — see [`AxisFrame`]. @@ -563,7 +569,7 @@ enum LabelAxis { /// What can change about the grid: cell values, the frames that name them, /// the widths, the totals, the page bar and the extent. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum GridUpdate { /// Display values for cells, by viewport position. Cells(Vec<((usize, usize), CellDisplay)>), diff --git a/crates/improvise-protocol/src/resolve.rs b/crates/improvise-protocol/src/resolve.rs index 0bece83a..cb517c76 100644 --- a/crates/improvise-protocol/src/resolve.rs +++ b/crates/improvise-protocol/src/resolve.rs @@ -530,12 +530,10 @@ fn composed( state, ModelCommand::ClearCell(record_cell_at_cursor(state)?), )?]), - // `YankCell::execute`: the value, then the status. `edit_text` is - // what the host evaluated, which is what the TUI yanks. + // `YankCell::execute`: the value, then the status. The projected + // value, never text re-parsed: improvise-dsxq. "yank" => Some(vec![ - Command::SetYanked(typed( - cell_at_cursor(state)?.edit_text.as_deref().unwrap_or(""), - )), + Command::SetYanked(cell_at_cursor(state)?.value.clone()), Command::SetStatus("Yanked".to_string()), ]), "paste" => Some(match state.view.yanked.clone() { @@ -3139,6 +3137,7 @@ mod tests { #[test] fn yy_yanks_the_cell_under_the_cursor_and_p_pastes_what_it_yanked() { let mut state = client_on(record_cell()); + state.grid.cells[0][0].value = Some(CellValue::Number(1200.0)); arm(&mut state, ev(KeyCode::Char('y'))); let KeyOutcome::Local(yank) = resolve(&state, ev(KeyCode::Char('y'))) else { panic!("`yy` is bound to yank");