csv_preview: Evolve into tabular data preview (#60768)
# Objective The MVP version of CSV preview is ready. However it's trivial to extend it to TSV & other formats. ## Solution This PR does exactly that - switching from CSV only to any tabular data (TSV/CSV/PSV/SSV). 1. added `tsv`/`psv`/`ssv` support 2. renamed action/eye button & crate 3. added `.psv`/`.ssv` types to use database icon (similar to `csv`/`tsv`) 2. Added `Table` svg icon (was previously missing) so preview is not longer using book icon (from markdown preview). > NOTE: Crate renaming will be done separately (as it's mechanical work & I don't want to mix it with logic changes) ## Testing Opened csv/tsv/psv/ssv files in peview ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] 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) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase PSV/SSV file formats icon | Before | After | | --- | --- | | <img width="141" height="136" alt="image" src="https://github.com/user-attachments/assets/ff5ec1a2-10e3-4363-a467-d535c7f621c0" /> | <img width="143" height="135" alt="image" src="https://github.com/user-attachments/assets/f4a85276-beee-4d3e-bc59-319c4fc8812c" /> | Change namespace for actions (`csv` -> `tabular data`) | Before | After | | --- | --- | | <img width="551" height="150" alt="image" src="https://github.com/user-attachments/assets/f7b79a7f-b300-4d14-8565-b42b3bcb11c4" /> | <img width="558" height="152" alt="image" src="https://github.com/user-attachments/assets/be9dcc54-51a4-4736-9539-172fecabdf6d" /> | Update parser & detection logic to support preview for TSV/PSV/SSV | Before | After | | --- | --- | | <img width="470" height="287" alt="image" src="https://github.com/user-attachments/assets/d0222bd1-91a7-46e0-b4da-76be0642bf51" /> | <img width="541" height="275" alt="image" src="https://github.com/user-attachments/assets/3c4d237a-4956-46ad-b7d6-3a8782b41888" /> | Introduce table svg icon | Before | After | | --- | --- | | <img width="349" height="66" alt="image" src="https://github.com/user-attachments/assets/bd7999ef-516f-43d1-a0e6-50055c1c3bed" /> | <img width="331" height="50" alt="image" src="https://github.com/user-attachments/assets/fc534921-0c15-421c-b046-8c5967178d24" /> | --- Release Notes: - N/A or Added/Fixed/Improved ...
This commit is contained in:
1 parent
a5234566b4
commit
f65b8464f6
6 files changed
+130
-23
No files matched your search
@@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="2" width="12" height="12" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.2" />
|
||||
<rect x="2" y="2" width="12" height="4" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="2" y1="6" x2="14" y2="6" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="6" y1="6" x2="6" y2="14" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="10" y1="6" x2="10" y2="14" stroke="currentColor" stroke-width="1.2" />
|
||||
<line x1="2" y1="10" x2="14" y2="10" stroke="currentColor" stroke-width="1.2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 634 B |
@@ -23,7 +23,7 @@ mod settings;
|
||||
mod table_data_engine;
|
||||
mod types;
|
||||
|
||||
actions!(csv, [OpenPreview, OpenPreviewToTheSide]);
|
||||
actions!(tabular_data, [OpenPreview, OpenPreviewToTheSide]);
|
||||
|
||||
pub struct TabularDataPreviewFeatureFlag;
|
||||
|
||||
@@ -279,14 +279,10 @@ impl CsvPreviewView {
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| {
|
||||
buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| file.path().extension())
|
||||
.map(|ext| ext.eq_ignore_ascii_case("csv"))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.and_then(|file| file.path().extension())
|
||||
.and_then(parser::TabularFormat::from_extension)
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +298,7 @@ impl Item for CsvPreviewView {
|
||||
type Event = ();
|
||||
|
||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||
Some(Icon::new(IconName::FileDoc))
|
||||
Some(Icon::new(IconName::Table))
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||
|
||||
@@ -18,6 +18,40 @@ pub(crate) struct EditorState {
|
||||
pub _subscription: Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum TabularFormat {
|
||||
Csv,
|
||||
Tsv,
|
||||
Psv,
|
||||
Ssv,
|
||||
}
|
||||
|
||||
const TABULAR_FORMATS: &[(&str, TabularFormat)] = &[
|
||||
("csv", TabularFormat::Csv),
|
||||
("tsv", TabularFormat::Tsv),
|
||||
("psv", TabularFormat::Psv),
|
||||
("ssv", TabularFormat::Ssv),
|
||||
];
|
||||
|
||||
impl TabularFormat {
|
||||
pub(crate) fn from_extension(ext: &str) -> Option<Self> {
|
||||
let lower = ext.to_lowercase();
|
||||
TABULAR_FORMATS
|
||||
.iter()
|
||||
.find(|(name, _)| *name == lower)
|
||||
.map(|(_, format)| *format)
|
||||
}
|
||||
|
||||
fn delimiter(self) -> char {
|
||||
match self {
|
||||
TabularFormat::Csv => ',',
|
||||
TabularFormat::Tsv => '\t',
|
||||
TabularFormat::Psv => '|',
|
||||
TabularFormat::Ssv => ';',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CsvPreviewView {
|
||||
pub(crate) fn parse_csv_from_active_editor(
|
||||
&mut self,
|
||||
@@ -57,13 +91,35 @@ impl CsvPreviewView {
|
||||
}
|
||||
}
|
||||
|
||||
let buffer_snapshot = view.update(cx, |_, cx| {
|
||||
editor
|
||||
let (buffer_snapshot, delimiter) = view.update(cx, |_, cx| {
|
||||
let buffer_ref = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.map(|b| b.read(cx).text_snapshot())
|
||||
.map(|b| b.read(cx).text_snapshot());
|
||||
|
||||
let extension = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.and_then(|file| file.path().extension().map(ToOwned::to_owned));
|
||||
|
||||
let delimiter = extension
|
||||
.as_deref()
|
||||
.and_then(TabularFormat::from_extension)
|
||||
.map(TabularFormat::delimiter)
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!(
|
||||
"unrecognized tabular data extension {:?}, defaulting to comma delimiter",
|
||||
extension
|
||||
);
|
||||
','
|
||||
});
|
||||
|
||||
(buffer_ref, delimiter)
|
||||
})?;
|
||||
|
||||
let Some(buffer_snapshot) = buffer_snapshot else {
|
||||
@@ -72,7 +128,7 @@ impl CsvPreviewView {
|
||||
|
||||
let instant = Instant::now();
|
||||
let parsed_csv = cx
|
||||
.background_spawn(async move { from_buffer(&buffer_snapshot) })
|
||||
.background_spawn(async move { from_buffer_with_delimiter(&buffer_snapshot, delimiter) })
|
||||
.await;
|
||||
let parse_duration = instant.elapsed();
|
||||
let parse_end_time: Instant = Instant::now();
|
||||
@@ -96,14 +152,17 @@ impl CsvPreviewView {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_buffer(buffer_snapshot: &BufferSnapshot) -> TableLikeContent {
|
||||
pub fn from_buffer_with_delimiter(
|
||||
buffer_snapshot: &BufferSnapshot,
|
||||
delimiter: char,
|
||||
) -> TableLikeContent {
|
||||
let text = buffer_snapshot.text();
|
||||
|
||||
if text.trim().is_empty() {
|
||||
return TableLikeContent::default();
|
||||
}
|
||||
|
||||
let (parsed_cells_with_positions, line_numbers) = parse_csv_with_positions(&text);
|
||||
let (parsed_cells_with_positions, line_numbers) = parse_csv_with_positions(&text, delimiter);
|
||||
if parsed_cells_with_positions.is_empty() {
|
||||
return TableLikeContent::default();
|
||||
}
|
||||
@@ -136,6 +195,7 @@ pub fn from_buffer(buffer_snapshot: &BufferSnapshot) -> TableLikeContent {
|
||||
/// Parse CSV and track byte positions for each cell
|
||||
fn parse_csv_with_positions(
|
||||
text: &str,
|
||||
delimiter: char,
|
||||
) -> (
|
||||
Vec<Vec<(SharedString, std::ops::Range<usize>)>>,
|
||||
Vec<LineNumber>,
|
||||
@@ -175,7 +235,7 @@ fn parse_csv_with_positions(
|
||||
}
|
||||
}
|
||||
}
|
||||
',' if !in_quotes => {
|
||||
c if c == delimiter && !in_quotes => {
|
||||
// Field separator
|
||||
let field_end_offset = current_offset;
|
||||
if current_field.is_empty() && !in_quotes {
|
||||
@@ -423,10 +483,52 @@ Jane,"Simple name""#;
|
||||
assert!(parsed.rows.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tsv_parsing() {
|
||||
let tsv_data = "Name\tAge\tCity\nJohn\t30\tNew York\nJane\t25\tLos Angeles";
|
||||
let (parsed_cells, _) = parse_csv_with_positions(tsv_data, '\t');
|
||||
|
||||
assert_eq!(parsed_cells.len(), 3);
|
||||
assert_eq!(parsed_cells[0].len(), 3);
|
||||
assert_eq!(parsed_cells[0][0].0.as_ref(), "Name");
|
||||
assert_eq!(parsed_cells[0][1].0.as_ref(), "Age");
|
||||
assert_eq!(parsed_cells[0][2].0.as_ref(), "City");
|
||||
assert_eq!(parsed_cells[1][0].0.as_ref(), "John");
|
||||
assert_eq!(parsed_cells[1][1].0.as_ref(), "30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psv_parsing() {
|
||||
let psv_data = "Name|Age|City\nJohn|30|New York\nJane|25|Los Angeles";
|
||||
let (parsed_cells, _) = parse_csv_with_positions(psv_data, '|');
|
||||
|
||||
assert_eq!(parsed_cells.len(), 3);
|
||||
assert_eq!(parsed_cells[0].len(), 3);
|
||||
assert_eq!(parsed_cells[0][0].0.as_ref(), "Name");
|
||||
assert_eq!(parsed_cells[0][1].0.as_ref(), "Age");
|
||||
assert_eq!(parsed_cells[0][2].0.as_ref(), "City");
|
||||
assert_eq!(parsed_cells[1][0].0.as_ref(), "John");
|
||||
assert_eq!(parsed_cells[1][1].0.as_ref(), "30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ssv_parsing() {
|
||||
let ssv_data = "Name;Age;City\nJohn;30;New York\nJane;25;Los Angeles";
|
||||
let (parsed_cells, _) = parse_csv_with_positions(ssv_data, ';');
|
||||
|
||||
assert_eq!(parsed_cells.len(), 3);
|
||||
assert_eq!(parsed_cells[0].len(), 3);
|
||||
assert_eq!(parsed_cells[0][0].0.as_ref(), "Name");
|
||||
assert_eq!(parsed_cells[0][1].0.as_ref(), "Age");
|
||||
assert_eq!(parsed_cells[0][2].0.as_ref(), "City");
|
||||
assert_eq!(parsed_cells[1][0].0.as_ref(), "John");
|
||||
assert_eq!(parsed_cells[1][1].0.as_ref(), "30");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_csv_parsing_quote_offset_handling() {
|
||||
let csv_data = r#"first,"se,cond",third"#;
|
||||
let (parsed_cells, _) = parse_csv_with_positions(csv_data);
|
||||
let (parsed_cells, _) = parse_csv_with_positions(csv_data, ',');
|
||||
|
||||
assert_eq!(parsed_cells.len(), 1); // One row
|
||||
assert_eq!(parsed_cells[0].len(), 3); // Three cells
|
||||
@@ -452,7 +554,7 @@ Jane,"Simple name""#;
|
||||
let csv_data = r#"id,"name with spaces","description, with commas",status
|
||||
1,"John Doe","A person with ""quotes"" and, commas",active
|
||||
2,"Jane Smith","Simple description",inactive"#;
|
||||
let (parsed_cells, _) = parse_csv_with_positions(csv_data);
|
||||
let (parsed_cells, _) = parse_csv_with_positions(csv_data, ',');
|
||||
|
||||
assert_eq!(parsed_cells.len(), 3); // header + 2 rows
|
||||
|
||||
@@ -510,6 +612,6 @@ impl TableLikeContent {
|
||||
let buffer_id = BufferId::new(1).unwrap();
|
||||
let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, text);
|
||||
let snapshot = buffer.snapshot();
|
||||
from_buffer(snapshot)
|
||||
from_buffer_with_delimiter(&snapshot, ',')
|
||||
}
|
||||
}
|
||||
@@ -249,6 +249,7 @@ pub enum IconName {
|
||||
StarFilled,
|
||||
Stop,
|
||||
Tab,
|
||||
Table,
|
||||
Terminal,
|
||||
TerminalAlt,
|
||||
TextSnippet,
|
||||
|
||||
@@ -230,8 +230,8 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[
|
||||
"storage",
|
||||
&[
|
||||
"accdb", "csv", "dat", "db", "dbf", "dll", "fmp", "fp7", "frm", "gdb", "ib", "ldf",
|
||||
"mdb", "mdf", "myd", "myi", "pdb", "RData", "rdata", "sav", "sdf", "sql", "sqlite",
|
||||
"tsv",
|
||||
"mdb", "mdf", "myd", "myi", "pdb", "psv", "RData", "rdata", "sav", "sdf", "sql",
|
||||
"sqlite", "ssv", "tsv",
|
||||
],
|
||||
),
|
||||
(
|
||||
|
||||
@@ -5899,7 +5899,6 @@ mod tests {
|
||||
"context_server",
|
||||
"copilot",
|
||||
"copilot_edit_predictions",
|
||||
"csv",
|
||||
"debug_panel",
|
||||
"debugger",
|
||||
"dev",
|
||||
@@ -5953,6 +5952,7 @@ mod tests {
|
||||
"svg",
|
||||
"syntax_tree_view",
|
||||
"tab_switcher",
|
||||
"tabular_data",
|
||||
"task",
|
||||
"terminal",
|
||||
"terminal_panel",
|
||||
|
||||
Reference in new issue
Block a user