Files
zed/crates/csv_preview/src/table_data_engine.rs
T
Oleksandr Kholiavko 3f135978df csv_preview: Add row filtering feature (#60339)
# Objective

CSV feature needs row filtering feature by column values. This PR
provides base implementation of it with barebones ui.

> NOTE: Sleek UI with search & proper scrolling hanling is implemented
in next PR. It's stacked on top to reduce review scope

## Solution

- New `FilterEntry` / `FilterEntryState` model in
`table_data_engine/filtering_by_column.rs` tracking per-column
applied/candidate filter values
- Filtering runs in the background (`feat: Implement background
filtering`) so large CSVs don't block the UI thread while a filter is
applied
- Filter menu entries reflect live counts and support a configurable
sort order (`FilterSortOrder`, added in `renderer/settings.rs` /
`settings.rs`)
- Filter/sort trigger buttons on column headers are hidden until hover,
using `GradientFade` (new in `ui/src/components/gradient_fade.rs`) to
fade content behind them

## Testing

Filter chain tested on csv fixtures with multiple filters applied
sequentially columns.

## Self-Review Checklist: (todo)

- [ ] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [ ] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines) (out of scope of this pr)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

<img width="664" height="249" alt="image"
src="https://github.com/user-attachments/assets/0e9b0a91-1a27-4e0f-a8d4-fdce36735131"
/>

<img width="663" height="205" alt="image"
src="https://github.com/user-attachments/assets/0428f5c6-6aaa-4891-b010-ca79803f6613"
/>

---

Release Notes:

- Added initial row filtering UI & logic
2026-07-08 15:54:36 +00:00

121 lines
4.1 KiB
Rust

//! This module defines core operations and config of tabular data view (CSV table)
//! It operates in 2 coordinate systems:
//! - `DataCellId` - indices of src data cells
//! - `DisplayCellId` - indices of data after applied transformations like sorting/filtering, which is used to render cell on the screen
//!
//! It's designed to contain core logic of operations without relying on `CsvPreviewView`, context or window handles.
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use ui::table_row::TableRow;
use crate::{
table_data_engine::{
filtering_by_column::{FilterEntry, FilterStack, calculate_available_filters, retain_rows},
sorting_by_column::{AppliedSorting, sort_data_rows},
},
types::{AnyColumn, DataRow, DisplayRow, TableCell, TableLikeContent},
};
pub mod filtering_by_column;
pub mod sorting_by_column;
#[derive(Default)]
pub(crate) struct TableDataEngine {
pub filter_stack: FilterStack,
/// Pre-computed unique values per column, used to populate filter menus
all_filters: HashMap<AnyColumn, Vec<FilterEntry>>,
pub applied_sorting: Option<AppliedSorting>,
d2d_mapping: DisplayToDataMapping,
pub contents: Arc<TableLikeContent>,
}
impl TableDataEngine {
pub(crate) fn d2d_mapping(&self) -> &DisplayToDataMapping {
&self.d2d_mapping
}
pub(crate) fn set_d2d_mapping(&mut self, mapping: DisplayToDataMapping) {
self.d2d_mapping = mapping;
}
/// Recomputes the unique filter entries for every column from the current table data.
/// Must be called after content changes (e.g. after parsing).
pub fn calculate_available_filters(&mut self) {
self.all_filters =
calculate_available_filters(&self.contents.rows, self.contents.number_of_cols);
}
}
/// Relation of Display (rendered) rows to Data (src) rows with applied transformations
/// Transformations applied:
/// - sorting by column
/// - filtering by column values
#[derive(Debug, Default)]
pub struct DisplayToDataMapping {
/// All rows sorted, regardless of applied filtering. Recomputed every time sorting changes
pub sorted_rows: Vec<DataRow>,
/// Rows that survive the active filters. Recomputed every time filters change
pub retained_rows: HashSet<DataRow>,
/// Merged result: sorted rows intersected with retained rows
pub mapping: Arc<HashMap<DisplayRow, DataRow>>,
}
impl DisplayToDataMapping {
/// Computes the full display-to-data mapping from owned inputs.
/// Intended to be called from a background thread.
pub(crate) fn compute(
contents: &Arc<TableLikeContent>,
filter_stack: &FilterStack,
sorting: Option<AppliedSorting>,
) -> Self {
let mut mapping = Self::default();
mapping.apply_sorting(sorting, &contents.rows);
mapping.apply_filtering(filter_stack, &contents.rows);
mapping.merge_mappings();
mapping
}
/// Get the data row for a given display row
pub fn get_data_row(&self, display_row: DisplayRow) -> Option<DataRow> {
self.mapping.get(&display_row).copied()
}
/// Get the number of filtered rows
pub fn visible_row_count(&self) -> usize {
self.mapping.len()
}
/// Computes sorting
fn apply_sorting(&mut self, sorting: Option<AppliedSorting>, rows: &[TableRow<TableCell>]) {
let data_rows: Vec<DataRow> = (0..rows.len()).map(DataRow).collect();
let sorted_rows = if let Some(sorting) = sorting {
sort_data_rows(&rows, data_rows, sorting)
} else {
data_rows
};
self.sorted_rows = sorted_rows;
}
fn apply_filtering(&mut self, filter_stack: &FilterStack, rows: &[TableRow<TableCell>]) {
self.retained_rows = retain_rows(rows, filter_stack);
}
/// Merges pre-computed sorting and filtering into the final display mapping
fn merge_mappings(&mut self) {
self.mapping = Arc::new(
self.sorted_rows
.iter()
.filter(|data_row| self.retained_rows.contains(data_row))
.enumerate()
.map(|(display, data)| (DisplayRow(display), *data))
.collect(),
);
}
}