From b11cce5c41ca488bcdc0bea1696ba3d86cac5f4c Mon Sep 17 00:00:00 2001
From: Oleksandr Kholiavko <43780952+HalavicH@users.noreply.github.com>
Date: Thu, 16 Jul 2026 21:28:22 +0200
Subject: [PATCH] csv_preview: Add single-line row displaying mode (#61127)
### Changes
Adds a checkbox to toggle between multiline cells (current behavior) and
single-line rows that clip overflow, so wide CSVs stay compact when full
cell content isn't needed at a glance.
#### Demo
| Before | After |
| --- | --- |
| No single line mode |
|
|
|
|
### Testing
- Toggle "Display multiline rows" off: cells clip to one line with
ellipsis instead of wrapping
- Toggle it back on: cells wrap and grow to show full content
- Resize the window / change font size: single-line clip height tracks
the actual text line height instead of a stale hardcoded value
Release Notes:
- N/A
---
.gitignore | 2 ++
crates/csv_preview/src/csv_preview.rs | 21 ++++++++-----
.../csv_preview/src/renderer/preview_view.rs | 10 +++++-
.../csv_preview/src/renderer/render_table.rs | 22 ++++++++++---
.../src/renderer/row_identifiers.rs | 22 +++++--------
crates/csv_preview/src/renderer/settings.rs | 31 ++++++++++++++++---
.../csv_preview/src/renderer/table_header.rs | 1 +
crates/csv_preview/src/settings.rs | 9 ++++++
8 files changed, 85 insertions(+), 33 deletions(-)
diff --git a/.gitignore b/.gitignore
index cf75babbcd..43a48ce78c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,3 +58,5 @@ crates/docs_preprocessor/actions.json
# NixOS integration test state
.nixos-test-history
+
+.local*
diff --git a/crates/csv_preview/src/csv_preview.rs b/crates/csv_preview/src/csv_preview.rs
index 8f8ba76875..cc86fc63dd 100644
--- a/crates/csv_preview/src/csv_preview.rs
+++ b/crates/csv_preview/src/csv_preview.rs
@@ -49,6 +49,10 @@ pub struct CsvPreviewView {
/// Performance metrics for debugging and monitoring CSV operations.
pub(crate) performance_metrics: PerformanceMetrics,
pub(crate) list_state: gpui::ListState,
+ /// Cached row height, refreshed from the actual text line height on every render.
+ /// Used to size not-yet-rendered rows for the scrollbar without a full `.measure_all()`
+ /// pass, so it tracks the real row height instead of a hardcoded guess.
+ pub(crate) row_height: Pixels,
/// Time when the last parsing operation ended, used for smart debouncing
pub(crate) last_parse_end_time: Option,
}
@@ -94,7 +98,7 @@ impl CsvPreviewView {
.and_then(|item| item.act_as::(cx))
.filter(|editor| Self::is_csv_file(editor, cx))
{
- let csv_preview = Self::new(&editor, cx);
+ let csv_preview = Self::new(&editor, window, cx);
workspace.active_pane().update(cx, |pane, cx| {
let existing = pane
.items_of_type::()
@@ -115,7 +119,7 @@ impl CsvPreviewView {
.and_then(|item| item.act_as::(cx))
.filter(|editor| Self::is_csv_file(editor, cx))
{
- let csv_preview = Self::new(&editor, cx);
+ let csv_preview = Self::new(&editor, window, cx);
let pane = workspace
.find_pane_in_direction(SplitDirection::Right, cx)
.unwrap_or_else(|| {
@@ -152,7 +156,7 @@ impl CsvPreviewView {
});
}
- fn new(editor: &Entity, cx: &mut Context) -> Entity {
+ fn new(editor: &Entity, window: &Window, cx: &mut Context) -> Entity {
let contents = TableLikeContent::default();
let table_interaction_state = cx.new(|cx| {
TableInteractionState::new(cx).with_custom_scrollbar(ui::Scrollbars::for_settings::<
@@ -173,6 +177,7 @@ impl CsvPreviewView {
},
);
+ let row_height = window.pixel_snap(window.line_height());
let mut view = CsvPreviewView {
focus_handle: cx.focus_handle(),
active_editor_state: EditorState {
@@ -186,7 +191,8 @@ impl CsvPreviewView {
filter_sort_task: None,
performance_metrics: PerformanceMetrics::default(),
list_state: gpui::ListState::new(contents.rows.len(), ListAlignment::Top, px(1.))
- .with_uniform_item_height(px(24.)),
+ .with_uniform_item_height(row_height),
+ row_height,
settings: CsvPreviewSettings::default(),
last_parse_end_time: None,
engine: TableDataEngine::default(),
@@ -239,11 +245,10 @@ impl CsvPreviewView {
this.update(cx, |view, cx| {
view.engine.set_d2d_mapping(mapping);
let visible_rows = view.engine.d2d_mapping().visible_row_count();
- // Approximation of single csv table row height. Will be re-measured on scrolling.
- // This cheap solution allow to render scrollbar with fraction of a cost compared to `.measure_all()` call
- let approximate_height = px(24.);
+ // Uses the row height measured on the last render. Cheaper than a full
+ // `.measure_all()` pass; exact row heights are re-measured on scrolling.
view.list_state
- .reset_with_uniform_height(visible_rows, approximate_height);
+ .reset_with_uniform_height(visible_rows, view.row_height);
cx.notify();
})
.ok();
diff --git a/crates/csv_preview/src/renderer/preview_view.rs b/crates/csv_preview/src/renderer/preview_view.rs
index 335633656a..f460142943 100644
--- a/crates/csv_preview/src/renderer/preview_view.rs
+++ b/crates/csv_preview/src/renderer/preview_view.rs
@@ -7,7 +7,15 @@ use crate::CsvPreviewView;
impl Render for CsvPreviewView {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
let theme = cx.theme();
-
+ let row_height = window.pixel_snap(window.line_height());
+ if row_height != self.row_height {
+ self.row_height = row_height;
+ // Font size (rem size, buffer font override, ...) changed since the list was last
+ // measured: existing rows and unmeasured-item height hints are now the wrong size.
+ // Unlike `reset_with_uniform_height`, this preserves scroll position and keeps each
+ // item's prior size as a hint rather than dropping straight to a fresh guess.
+ self.list_state.remeasure();
+ }
let render_prep_start = Instant::now();
let table_with_settings = v_flex()
.size_full()
diff --git a/crates/csv_preview/src/renderer/render_table.rs b/crates/csv_preview/src/renderer/render_table.rs
index 511e36abd1..9a38a3fedd 100644
--- a/crates/csv_preview/src/renderer/render_table.rs
+++ b/crates/csv_preview/src/renderer/render_table.rs
@@ -69,6 +69,7 @@ impl CsvPreviewView {
cols,
display_row,
row_identifier_text_color,
+ this.row_height,
cx,
)
.unwrap_or_else(|| panic!("Expected to render a table row"))
@@ -83,6 +84,7 @@ impl CsvPreviewView {
.rendered_indices
.extend(range.clone());
+ let row_height = this.row_height;
range
.filter_map(|display_index| {
Self::render_single_table_row(
@@ -90,6 +92,7 @@ impl CsvPreviewView {
cols,
DisplayRow(display_index),
row_identifier_text_color,
+ row_height,
cx,
)
})
@@ -110,6 +113,7 @@ impl CsvPreviewView {
cols: usize,
display_row: DisplayRow,
row_identifier_text_color: gpui::Hsla,
+ row_height: Pixels,
cx: &Context,
) -> Option> {
// Get the actual row index from our sorted indices
@@ -128,14 +132,23 @@ impl CsvPreviewView {
let display_cell_id = DisplayCellId::new(display_row, col);
- let cell = div().size_full().whitespace_nowrap().text_ellipsis().child(
- CsvPreviewView::create_selectable_cell(
+ let cell = div()
+ .size_full()
+ .when(
+ !this.settings.multiline_cells_effectively_enabled(),
+ |div| {
+ div.whitespace_nowrap()
+ .text_ellipsis()
+ .h(row_height)
+ .overflow_hidden()
+ },
+ )
+ .child(CsvPreviewView::create_selectable_cell(
display_cell_id,
cell_content,
this.settings.vertical_alignment,
cx,
- ),
- );
+ ));
elements.push(
div()
@@ -154,7 +167,6 @@ impl CsvPreviewView {
},
))
})
- .text_ui(cx)
.child(cell)
.into_any_element(),
);
diff --git a/crates/csv_preview/src/renderer/row_identifiers.rs b/crates/csv_preview/src/renderer/row_identifiers.rs
index 770e266186..b997f64576 100644
--- a/crates/csv_preview/src/renderer/row_identifiers.rs
+++ b/crates/csv_preview/src/renderer/row_identifiers.rs
@@ -1,12 +1,12 @@
use ui::{
ActiveTheme as _, AnyElement, Button, ButtonCommon as _, ButtonSize, ButtonStyle,
- Clickable as _, Context, ElementId, FluentBuilder as _, IntoElement as _, ParentElement as _,
- SharedString, Styled as _, StyledTypography as _, Tooltip, div,
+ Clickable as _, Context, ElementId, IntoElement as _, ParentElement as _, SharedString,
+ Styled as _, StyledTypography as _, Tooltip, div,
};
use crate::{
CsvPreviewView,
- settings::{RowIdentifiers, VerticalAlignment},
+ settings::RowIdentifiers,
types::{DataRow, DisplayRow, LineNumber},
};
@@ -34,7 +34,7 @@ impl LineNumber {
if start + 1 == end {
format!("{start}\n{end}")
} else {
- format!("{start}\n...\n{end}")
+ format!("{start}\n-\n{end}")
}
}
RowIdentDisplayMode::Horizontal => {
@@ -78,11 +78,6 @@ impl CsvPreviewView {
(max_line_number as f32).log10().floor() as usize + 1
};
- // if !self.settings.multiline_cells_enabled {
- // // Uses horizontal line numbers layout like `123-456`. Needs twice the size
- // digit_count *= 2;
- // }
-
let char_width_px = 9.0; // TODO: get real width of the characters
let base_width = (digit_count as f32) * char_width_px;
let padding = 20.0;
@@ -157,7 +152,7 @@ impl CsvPreviewView {
.contents
.line_numbers
.get(*data_row)?
- .display_string(if self.settings.multiline_cells_enabled {
+ .display_string(if self.settings.multiline_cells_effectively_enabled() {
RowIdentDisplayMode::Vertical
} else {
RowIdentDisplayMode::Horizontal
@@ -172,14 +167,11 @@ impl CsvPreviewView {
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().panel_background)
.h_full()
- .text_ui(cx)
.text_color(cx.theme().colors().text_muted)
.justify_center()
- .map(|div| match self.settings.vertical_alignment {
- VerticalAlignment::Top => div.items_start(),
- VerticalAlignment::Center => div.items_center(),
- })
+ .items_center()
.font_buffer(cx)
+ .text_ui(cx)
.child(row_identifier)
.into_any_element();
Some(value)
diff --git a/crates/csv_preview/src/renderer/settings.rs b/crates/csv_preview/src/renderer/settings.rs
index 18c185bb0f..fe8f671f92 100644
--- a/crates/csv_preview/src/renderer/settings.rs
+++ b/crates/csv_preview/src/renderer/settings.rs
@@ -1,6 +1,7 @@
use ui::{
- ActiveTheme as _, AnyElement, ButtonSize, Context, ContextMenu, DropdownMenu, ElementId,
- IntoElement as _, ParentElement as _, Styled as _, Tooltip, Window, div, h_flex,
+ ActiveTheme as _, AnyElement, ButtonSize, Checkbox, Context, ContextMenu, DropdownMenu,
+ ElementId, IntoElement as _, ParentElement as _, Styled as _, ToggleState, Tooltip, Window,
+ div, h_flex,
};
use crate::{
@@ -121,6 +122,30 @@ impl CsvPreviewView {
),
);
+ let multiline_enabled = self.settings.multiline_cells_enabled;
+ let panel = panel.child({
+ let view = view.clone();
+ Checkbox::new(
+ ElementId::Name("multiline-rows-checkbox".into()),
+ if multiline_enabled {
+ ToggleState::Selected
+ } else {
+ ToggleState::Unselected
+ },
+ )
+ .label("Display multiline rows")
+ .tooltip(Tooltip::text(
+ "When enabled, row height grows to show all content. \
+ When disabled, only the first line is visible — hover a cell to see the rest.",
+ ))
+ .on_click(move |_state, _window, cx| {
+ view.update(cx, |this, cx| {
+ this.settings.multiline_cells_enabled = !this.settings.multiline_cells_enabled;
+ cx.notify();
+ });
+ })
+ });
+
#[cfg(feature = "dev-tools")]
let panel = panel.child(
h_flex()
@@ -171,7 +196,6 @@ fn create_dev_only_popover_menu(
view_entity.update(cx, |view, cx| {
view.settings.rendering_with =
RowRenderMechanism::VariableList;
- view.settings.multiline_cells_enabled = true;
cx.notify();
})
}
@@ -188,7 +212,6 @@ fn create_dev_only_popover_menu(
view_entity.update(cx, |view, cx| {
view.settings.rendering_with =
RowRenderMechanism::UniformList;
- view.settings.multiline_cells_enabled = false;
cx.notify();
})
}
diff --git a/crates/csv_preview/src/renderer/table_header.rs b/crates/csv_preview/src/renderer/table_header.rs
index 6549cb5152..444ee09099 100644
--- a/crates/csv_preview/src/renderer/table_header.rs
+++ b/crates/csv_preview/src/renderer/table_header.rs
@@ -44,6 +44,7 @@ impl CsvPreviewView {
.w_full()
.items_center()
.font_buffer(cx)
+ .text_buffer(cx)
.child(
div()
.flex_1()
diff --git a/crates/csv_preview/src/settings.rs b/crates/csv_preview/src/settings.rs
index 2d5cc78c5f..dc871fa4b3 100644
--- a/crates/csv_preview/src/settings.rs
+++ b/crates/csv_preview/src/settings.rs
@@ -46,3 +46,12 @@ pub(crate) struct CsvPreviewSettings {
pub(crate) show_perf_metrics_overlay: bool,
pub(crate) multiline_cells_enabled: bool,
}
+
+impl CsvPreviewSettings {
+ /// `multiline_cells_enabled` only makes sense with `VariableList`, which
+ /// supports per-row heights; `UniformList` requires every row to share one
+ /// height, so multiline is never honored there regardless of the setting.
+ pub(crate) fn multiline_cells_effectively_enabled(&self) -> bool {
+ self.multiline_cells_enabled && self.rendering_with == RowRenderMechanism::VariableList
+ }
+}