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 | <img width="662" height="316" alt="image"
src="https://github.com/user-attachments/assets/3f2a9673-557c-4539-b755-9ff2633de2d7"
/> |
| <img width="666" height="287" alt="image"
src="https://github.com/user-attachments/assets/703f7214-a722-4fa5-9125-f043439e21b2"
/> | <img width="660" height="317" alt="image"
src="https://github.com/user-attachments/assets/be6ca912-e686-44da-90eb-58f09fab51f1"
/> |

### 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
This commit is contained in:
Oleksandr Kholiavko authored and GitHub committed 2026-07-16 19:28:22 +00:00
1 parent b6a1c2706e
commit b11cce5c41
8 files changed
+85 -33

No files matched your search

+2
View File
@@ -58,3 +58,5 @@ crates/docs_preprocessor/actions.json
# NixOS integration test state
.nixos-test-history
.local*
+13 -8
View File
@@ -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<std::time::Instant>,
}
@@ -94,7 +98,7 @@ impl CsvPreviewView {
.and_then(|item| item.act_as::<Editor>(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::<CsvPreviewView>()
@@ -115,7 +119,7 @@ impl CsvPreviewView {
.and_then(|item| item.act_as::<Editor>(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<Editor>, cx: &mut Context<Workspace>) -> Entity<Self> {
fn new(editor: &Entity<Editor>, window: &Window, cx: &mut Context<Workspace>) -> Entity<Self> {
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();
@@ -7,7 +7,15 @@ use crate::CsvPreviewView;
impl Render for CsvPreviewView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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()
@@ -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<CsvPreviewView>,
) -> Option<UncheckedTableRow<AnyElement>> {
// 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(),
);
@@ -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)
+27 -4
View File
@@ -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();
})
}
@@ -44,6 +44,7 @@ impl CsvPreviewView {
.w_full()
.items_center()
.font_buffer(cx)
.text_buffer(cx)
.child(
div()
.flex_1()
+9
View File
@@ -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
}
}