4 Commits

Author SHA1 Message Date
00499fc2bf test(cmd): update tests for layout refactor
Updated unit tests in src/command/cmd.rs to use the new GridLayout-based
CmdContext and layout accessors. Tests now construct CmdContext with a
layout argument and verify behavior of navigation and selection commands
under the refactored layout logic. No functional changes to command logic;
only test updates.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF)
2026-04-07 09:16:25 -07:00
178983bcbf feat(ui): add new edge/jump commands and keymap
Introduced new commands: JumpToEdge (first/last row/col), PageScroll, and
OpenRecordRow. Updated command registry to use these commands and unified
key handling. Added format module for formatting functions. Updated main.rs
to include format module. Updated keymap to bind new commands and page
scroll.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF)
2026-04-07 09:16:25 -07:00
e09ddf71a7 refactor!(ui): use GridLayout for layout and display
Rebuilt App to hold a GridLayout and recompute it on state changes. Updated
cmd_context to use layout and display_value. Replaced manual width
calculations with compute_col_widths and compute_visible_cols. Updated
GridWidget to use layout and drill_state. Added Panel::mode helper and
updated UI titles. Fixed display logic for records mode using
layout.display_text.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF)
2026-04-07 09:16:25 -07:00
f8f8f537c3 refactor!(cmd): move CmdContext logic to GridLayout
Refactored CmdContext to delegate row/col counts, cell_key, none_cats, view
stacks, and records handling to GridLayout. Updated all command
implementations to use layout methods. Updated tests to construct
CmdContext with layout. Changed GridLayout to store records as Rc and added
synthetic_record_info helper. Updated view/layout.rs and view/mod.rs
accordingly.

BREAKING CHANGE: CmdContext fields changed; external callers must update to use layout
methods. GridLayout records field changed to Rc.
Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF)
2026-04-07 09:16:25 -07:00
13 changed files with 492 additions and 758 deletions

View File

@ -1,44 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

View File

@ -1,50 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

4
.gitignore vendored
View File

@ -8,7 +8,3 @@ symbols.json
profile.json
profile.json.gz
bench/*.txt
# Added by git-smart-commit
*.patch
*.improv

File diff suppressed because it is too large Load Diff

View File

@ -72,8 +72,6 @@ pub enum Binding {
},
/// A prefix sub-keymap (Emacs-style).
Prefix(Arc<Keymap>),
/// A sequence of commands executed in order, concatenating their effects.
Sequence(Vec<(&'static str, Vec<String>)>),
}
/// A keymap maps key patterns to bindings (command names or prefix sub-keymaps).
@ -123,17 +121,6 @@ impl Keymap {
.insert(KeyPattern::Key(key, mods), Binding::Prefix(sub));
}
/// Bind a key to a sequence of commands (executed in order).
pub fn bind_seq(
&mut self,
key: KeyCode,
mods: KeyModifiers,
steps: Vec<(&'static str, Vec<String>)>,
) {
self.bindings
.insert(KeyPattern::Key(key, mods), Binding::Sequence(steps));
}
/// Bind a catch-all for any Char key.
pub fn bind_any_char(&mut self, name: &'static str, args: Vec<String>) {
self.bindings
@ -156,7 +143,8 @@ impl Keymap {
.or_else(|| {
// Retry Char keys without modifiers (shift is implicit in the char)
if matches!(key, KeyCode::Char(_)) && mods != KeyModifiers::NONE {
self.bindings.get(&KeyPattern::Key(key, KeyModifiers::NONE))
self.bindings
.get(&KeyPattern::Key(key, KeyModifiers::NONE))
} else {
None
}
@ -186,14 +174,6 @@ impl Keymap {
Some(cmd.execute(ctx))
}
Binding::Prefix(sub) => Some(vec![Box::new(SetTransientKeymap(sub.clone()))]),
Binding::Sequence(steps) => {
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
for (name, args) in steps {
let cmd = registry.interactive(name, args, ctx).ok()?;
effects.extend(cmd.execute(ctx));
}
Some(effects)
}
}
}
}
@ -381,11 +361,7 @@ impl KeymapSet {
// Drill into aggregated cell / view history / add row
normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
normal.bind(KeyCode::Char('<'), none, "view-back");
normal.bind_seq(
KeyCode::Char('o'),
none,
vec![("add-record-row", vec![]), ("enter-edit-at-cursor", vec![])],
);
normal.bind(KeyCode::Char('o'), none, "open-record-row");
// Records mode toggle and prune toggle
normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
@ -409,11 +385,7 @@ impl KeymapSet {
normal.bind_prefix(KeyCode::Char('y'), none, Arc::new(y_map));
let mut z_map = Keymap::new();
z_map.bind_seq(
KeyCode::Char('Z'),
none,
vec![("save", vec![]), ("force-quit", vec![])],
);
z_map.bind(KeyCode::Char('Z'), none, "save-and-quit");
normal.bind_prefix(KeyCode::Char('Z'), none, Arc::new(z_map));
set.insert(ModeKey::Normal, Arc::new(normal));
@ -454,24 +426,9 @@ impl KeymapSet {
fp.bind(KeyCode::Char('o'), none, "enter-formula-edit");
fp.bind(KeyCode::Char('d'), none, "delete-formula-at-cursor");
fp.bind(KeyCode::Delete, none, "delete-formula-at-cursor");
fp.bind_args(
KeyCode::Char('F'),
none,
"toggle-panel-and-focus",
vec!["formula".into()],
);
fp.bind_args(
KeyCode::Char('C'),
none,
"toggle-panel-and-focus",
vec!["category".into()],
);
fp.bind_args(
KeyCode::Char('V'),
none,
"toggle-panel-and-focus",
vec!["view".into()],
);
fp.bind_args(KeyCode::Char('F'), none, "toggle-panel-and-focus", vec!["formula".into()]);
fp.bind_args(KeyCode::Char('C'), none, "toggle-panel-and-focus", vec!["category".into()]);
fp.bind_args(KeyCode::Char('V'), none, "toggle-panel-and-focus", vec!["view".into()]);
set.insert(ModeKey::FormulaPanel, Arc::new(fp));
// ── Category panel ───────────────────────────────────────────────
@ -552,24 +509,9 @@ impl KeymapSet {
vp.bind(KeyCode::Char('o'), none, "create-and-switch-view");
vp.bind(KeyCode::Char('d'), none, "delete-view-at-cursor");
vp.bind(KeyCode::Delete, none, "delete-view-at-cursor");
vp.bind_args(
KeyCode::Char('V'),
none,
"toggle-panel-and-focus",
vec!["view".into()],
);
vp.bind_args(
KeyCode::Char('C'),
none,
"toggle-panel-and-focus",
vec!["category".into()],
);
vp.bind_args(
KeyCode::Char('F'),
none,
"toggle-panel-and-focus",
vec!["formula".into()],
);
vp.bind_args(KeyCode::Char('V'), none, "toggle-panel-and-focus", vec!["view".into()]);
vp.bind_args(KeyCode::Char('C'), none, "toggle-panel-and-focus", vec!["category".into()]);
vp.bind_args(KeyCode::Char('F'), none, "toggle-panel-and-focus", vec!["formula".into()]);
set.insert(ModeKey::ViewPanel, Arc::new(vp));
// ── Tile select ──────────────────────────────────────────────────
@ -693,8 +635,8 @@ impl KeymapSet {
let mut sm = Keymap::new();
sm.bind(KeyCode::Esc, none, "exit-search-mode");
sm.bind(KeyCode::Enter, none, "exit-search-mode");
sm.bind_args(KeyCode::Backspace, none, "pop-char", vec!["search".into()]);
sm.bind_any_char("append-char", vec!["search".into()]);
sm.bind(KeyCode::Backspace, none, "search-pop-char");
sm.bind_any_char("search-append-char", vec![]);
set.insert(ModeKey::SearchMode, Arc::new(sm));
// ── Import wizard ────────────────────────────────────────────────

View File

@ -257,7 +257,6 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
f.render_widget(
GridWidget::new(
&app.model,
&app.layout,
&app.mode,
&app.search_query,
&app.buffers,
@ -283,7 +282,11 @@ fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
Some((format!("edit: {buf}"), Color::Green))
}
AppMode::FormulaEdit { .. } => {
let buf = app.buffers.get("formula").map(|s| s.as_str()).unwrap_or("");
let buf = app
.buffers
.get("formula")
.map(|s| s.as_str())
.unwrap_or("");
Some((format!("formula: {buf}"), Color::Cyan))
}
AppMode::CategoryAdd { .. } => {
@ -299,7 +302,11 @@ fn draw_bottom_bar(f: &mut Frame, area: Rect, app: &App) {
Some((format!("add item to {category}: {buf}"), Color::Green))
}
AppMode::ExportPrompt { .. } => {
let buf = app.buffers.get("export").map(|s| s.as_str()).unwrap_or("");
let buf = app
.buffers
.get("export")
.map(|s| s.as_str())
.unwrap_or("");
Some((format!("export path: {buf}"), Color::Yellow))
}
_ => None,
@ -342,6 +349,7 @@ fn draw_status(f: &mut Frame, area: Rect, app: &App) {
f.render_widget(Paragraph::new(line).style(mode_style(&app.mode)), area);
}
fn draw_welcome(f: &mut Frame, area: Rect) {
let popup = centered_popup(area, 58, 20);
let inner = draw_popup_frame(f, popup, " Welcome to improvise ", Color::Blue);

View File

@ -93,6 +93,7 @@ impl std::fmt::Display for CellValue {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct InternedKey(pub Vec<(Symbol, Symbol)>);
/// Serialized as a list of (key, value) pairs so CellKey doesn't need
/// to implement the `Serialize`-as-string requirement for JSON object keys.
#[derive(Debug, Clone, Default)]
@ -179,7 +180,9 @@ impl DataStore {
/// Iterate over all cells, yielding (CellKey, &CellValue) pairs.
pub fn iter_cells(&self) -> impl Iterator<Item = (CellKey, &CellValue)> {
self.cells.iter().map(|(k, v)| (self.to_cell_key(k), v))
self.cells
.iter()
.map(|(k, v)| (self.to_cell_key(k), v))
}
pub fn remove(&mut self, key: &CellKey) {

View File

@ -132,7 +132,8 @@ impl Model {
self.data.remove(&k);
}
// Remove formulas targeting this category
self.formulas.retain(|f| f.target_category != name);
self.formulas
.retain(|f| f.target_category != name);
}
/// Remove an item from a category and all cells that reference it.

View File

@ -506,10 +506,7 @@ mod tests {
let mut app = two_col_model();
// Total rows: A, B, C + R0..R9 = 13 rows. Last row = 12.
for i in 0..10 {
app.model
.category_mut("Row")
.unwrap()
.add_item(&format!("R{i}"));
app.model.category_mut("Row").unwrap().add_item(&format!("R{i}"));
}
app.term_height = 13; // ~5 visible rows
app.model.active_view_mut().selected = (0, 0);

View File

@ -96,11 +96,7 @@ impl Effect for RemoveFormula {
pub struct EnterEditAtCursor;
impl Effect for EnterEditAtCursor {
fn apply(&self, app: &mut App) {
app.rebuild_layout();
let ctx = app.cmd_context(
crossterm::event::KeyCode::Null,
crossterm::event::KeyModifiers::NONE,
);
let ctx = app.cmd_context(crossterm::event::KeyCode::Null, crossterm::event::KeyModifiers::NONE);
let value = ctx.display_value.clone();
drop(ctx);
app.buffers.insert("edit".to_string(), value);
@ -476,10 +472,9 @@ pub struct SetDrillPendingEdit {
impl Effect for SetDrillPendingEdit {
fn apply(&self, app: &mut App) {
if let Some(drill) = &mut app.drill_state {
drill.pending_edits.insert(
(self.record_idx, self.col_name.clone()),
self.new_value.clone(),
);
drill
.pending_edits
.insert((self.record_idx, self.col_name.clone()), self.new_value.clone());
}
}
}

View File

@ -59,7 +59,7 @@ impl<'a> GridWidget<'a> {
let n_col_levels = layout.col_cats.len().max(1);
let n_row_levels = layout.row_cats.len().max(1);
let col_widths = compute_col_widths(self.model, layout, fmt_comma, fmt_decimals);
let col_widths = compute_col_widths(self.model, &layout, fmt_comma, fmt_decimals);
// ── Adaptive row header widths ───────────────────────────────
let data_row_items: Vec<&Vec<String>> = layout
@ -128,7 +128,9 @@ impl<'a> GridWidget<'a> {
v
};
let col_x_at = |ci: usize| -> u16 {
area.x + row_header_width + col_x[ci].saturating_sub(col_x[col_offset])
area.x
+ row_header_width
+ col_x[ci].saturating_sub(col_x[col_offset])
};
let col_w_at = |ci: usize| -> u16 { *col_widths.get(ci).unwrap_or(&MIN_COL_WIDTH) };
@ -179,11 +181,7 @@ impl<'a> GridWidget<'a> {
buf.set_string(
x,
y,
format!(
"{:<width$}",
truncate(&label, cw.saturating_sub(1)),
width = cw
),
format!("{:<width$}", truncate(&label, cw.saturating_sub(1)), width = cw),
group_style,
);
}
@ -235,11 +233,7 @@ impl<'a> GridWidget<'a> {
buf.set_string(
x,
y,
format!(
"{:>width$}",
truncate(&label, cw.saturating_sub(1)),
width = cw
),
format!("{:>width$}", truncate(&label, cw.saturating_sub(1)), width = cw),
styled,
);
}
@ -363,9 +357,7 @@ impl<'a> GridWidget<'a> {
ds.pending_edits
.get(&(ri, col_name))
.cloned()
.unwrap_or_else(|| {
layout.display_text(self.model, ri, ci, fmt_comma, fmt_decimals)
})
.unwrap_or_else(|| layout.display_text(self.model, ri, ci, fmt_comma, fmt_decimals))
} else {
layout.display_text(self.model, ri, ci, fmt_comma, fmt_decimals)
};
@ -502,9 +494,9 @@ impl<'a> Widget for GridWidget<'a> {
block.render(area, buf);
// Page axis bar
if !self.layout.page_coords.is_empty() && inner.height > 0 {
let page_info: Vec<String> = self
.layout
let layout = GridLayout::new(self.model, self.model.active_view());
if !layout.page_coords.is_empty() && inner.height > 0 {
let page_info: Vec<String> = layout
.page_coords
.iter()
.map(|(cat, sel)| format!("{cat} = {sel}"))
@ -533,12 +525,7 @@ impl<'a> Widget for GridWidget<'a> {
/// Header widths use the widest *individual* level label (not the joined
/// multi-level string), matching how the grid renderer draws each level on
/// its own row with repeat-suppression.
pub fn compute_col_widths(
model: &Model,
layout: &GridLayout,
fmt_comma: bool,
fmt_decimals: u8,
) -> Vec<u16> {
pub fn compute_col_widths(model: &Model, layout: &GridLayout, fmt_comma: bool, fmt_decimals: u8) -> Vec<u16> {
let n = layout.col_count();
let mut widths = vec![0u16; n];
// Measure individual header level labels
@ -620,16 +607,9 @@ pub fn compute_row_header_width(layout: &GridLayout) -> u16 {
}
/// Count how many columns fit starting from `col_offset` given the available width.
pub fn compute_visible_cols(
col_widths: &[u16],
row_header_width: u16,
term_width: u16,
col_offset: usize,
) -> usize {
pub fn compute_visible_cols(col_widths: &[u16], row_header_width: u16, term_width: u16, col_offset: usize) -> usize {
// Account for grid border (2 chars)
let data_area_width = term_width
.saturating_sub(2)
.saturating_sub(row_header_width);
let data_area_width = term_width.saturating_sub(2).saturating_sub(row_header_width);
let mut acc = 0u16;
let mut count = 0usize;
for ci in col_offset..col_widths.len() {
@ -677,7 +657,6 @@ mod tests {
use crate::model::cell::{CellKey, CellValue};
use crate::model::Model;
use crate::ui::app::AppMode;
use crate::view::GridLayout;
// ── Helpers ───────────────────────────────────────────────────────────────
@ -733,7 +712,10 @@ mod tests {
// Fill every cell so nothing is pruned as empty.
for t in ["Food", "Clothing"] {
for mo in ["Jan", "Feb"] {
m.set_cell(coord(&[("Type", t), ("Month", mo)]), CellValue::Number(1.0));
m.set_cell(
coord(&[("Type", t), ("Month", mo)]),
CellValue::Number(1.0),
);
}
}
m

View File

@ -4,12 +4,20 @@ use ratatui::{
style::{Color, Modifier, Style},
widgets::Widget,
};
use unicode_width::UnicodeWidthStr;
use crate::model::Model;
use crate::ui::app::AppMode;
use crate::view::Axis;
fn axis_display(axis: Axis) -> (&'static str, Color) {
match axis {
Axis::Row => ("", Color::Green),
Axis::Column => ("", Color::Blue),
Axis::Page => ("", Color::Magenta),
Axis::None => ("", Color::DarkGray),
}
}
pub struct TileBar<'a> {
pub model: &'a Model,
pub mode: &'a AppMode,
@ -24,26 +32,10 @@ impl<'a> TileBar<'a> {
tile_cat_idx,
}
}
fn axis_display(axis: Axis) -> (&'static str, Color) {
match axis {
Axis::Row => ("|", Color::Green),
Axis::Column => ("-", Color::Blue),
Axis::Page => ("=", Color::Magenta),
Axis::None => (".", Color::DarkGray),
}
}
}
impl<'a> Widget for TileBar<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
// Clear the line to avoid stale characters from previous renders
buf.set_string(
area.x,
area.y,
" ".repeat(area.width as usize),
Style::default(),
);
let view = self.model.active_view();
let selected_cat_idx = if matches!(self.mode, AppMode::TileSelect) {
@ -58,7 +50,7 @@ impl<'a> Widget for TileBar<'a> {
let cat_names: Vec<&str> = self.model.category_names();
for (i, cat_name) in cat_names.iter().enumerate() {
let (axis_symbol, axis_color) = TileBar::axis_display(view.axis_of(cat_name));
let (axis_symbol, axis_color) = axis_display(view.axis_of(cat_name));
let label = format!(" [{cat_name} {axis_symbol}] ");
let is_selected = selected_cat_idx == Some(i);
@ -71,23 +63,22 @@ impl<'a> Widget for TileBar<'a> {
Style::default().fg(axis_color)
};
let label_w = label.width() as u16;
if x + label_w > area.x + area.width {
if x + label.len() as u16 > area.x + area.width {
break;
}
buf.set_string(x, area.y, &label, style);
x += label_w;
x += label.len() as u16;
}
// Hint
if matches!(self.mode, AppMode::TileSelect) {
let hint = " [Enter] cycle axis [r/c/p] set axis [←→] select [Esc] cancel";
if x + hint.width() as u16 <= area.x + area.width {
if x + hint.len() as u16 <= area.x + area.width {
buf.set_string(x, area.y, hint, Style::default().fg(Color::DarkGray));
}
} else {
let hint = " Ctrl+↑↓←→ to move tiles";
if x + hint.width() as u16 <= area.x + area.width {
if x + hint.len() as u16 <= area.x + area.width {
buf.set_string(x, area.y, hint, Style::default().fg(Color::DarkGray));
}
}

View File

@ -157,7 +157,7 @@ impl GridLayout {
.collect()
};
// Sort for deterministic ordering
records.sort_by(|a, b| a.0 .0.cmp(&b.0 .0));
records.sort_by(|a, b| a.0.0.cmp(&b.0.0));
// Synthesize row items: one per record, labeled with its index
let row_items: Vec<AxisEntry> = (0..records.len())
@ -610,7 +610,10 @@ mod tests {
m.category_mut("Col").unwrap().add_item("Y");
// Only X has data; Y is entirely empty
m.set_cell(
CellKey::new(vec![("Row".into(), "A".into()), ("Col".into(), "X".into())]),
CellKey::new(vec![
("Row".into(), "A".into()),
("Col".into(), "X".into()),
]),
CellValue::Number(1.0),
);
@ -640,9 +643,7 @@ mod tests {
v.set_axis("_Dim", Axis::Column);
let layout = GridLayout::new(&m, m.active_view());
assert!(layout.is_records_mode());
let cols: Vec<String> = (0..layout.col_count())
.map(|i| layout.col_label(i))
.collect();
let cols: Vec<String> = (0..layout.col_count()).map(|i| layout.col_label(i)).collect();
// All columns return synthetic keys
let value_col = cols.iter().position(|c| c == "Value").unwrap();
let key = layout.cell_key(0, value_col).unwrap();
@ -662,9 +663,7 @@ mod tests {
v.set_axis("_Index", Axis::Row);
v.set_axis("_Dim", Axis::Column);
let layout = GridLayout::new(&m, m.active_view());
let cols: Vec<String> = (0..layout.col_count())
.map(|i| layout.col_label(i))
.collect();
let cols: Vec<String> = (0..layout.col_count()).map(|i| layout.col_label(i)).collect();
// Value column resolves to the cell value
let value_col = cols.iter().position(|c| c == "Value").unwrap();
@ -676,10 +675,7 @@ mod tests {
let region_col = cols.iter().position(|c| c == "Region").unwrap();
let key = layout.cell_key(0, region_col).unwrap();
let display = layout.resolve_display(&key).unwrap();
assert!(
!display.is_empty(),
"Region column should resolve to a value"
);
assert!(!display.is_empty(), "Region column should resolve to a value");
}
#[test]