diff --git a/assets/icons/table.svg b/assets/icons/table.svg
new file mode 100644
index 00000000000000..a55be5624f8d7e
--- /dev/null
+++ b/assets/icons/table.svg
@@ -0,0 +1,8 @@
+
diff --git a/crates/csv_preview/src/csv_preview.rs b/crates/csv_preview/src/csv_preview.rs
index 3f8504ceae355f..11525da35cb314 100644
--- a/crates/csv_preview/src/csv_preview.rs
+++ b/crates/csv_preview/src/csv_preview.rs
@@ -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 {
- Some(Icon::new(IconName::FileDoc))
+ Some(Icon::new(IconName::Table))
}
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
diff --git a/crates/csv_preview/src/parser.rs b/crates/csv_preview/src/parser.rs
index 116c8912a38684..8ce4c5ee13f9fc 100644
--- a/crates/csv_preview/src/parser.rs
+++ b/crates/csv_preview/src/parser.rs
@@ -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 {
+ 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,
@@ -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, ',')
}
}
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index ac48d8343ecf8c..1c9966a1e76fb7 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -249,6 +249,7 @@ pub enum IconName {
StarFilled,
Stop,
Tab,
+ Table,
Terminal,
TerminalAlt,
TextSnippet,
diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs
index 5303f2952e8a53..bdc3727a74fc6b 100644
--- a/crates/theme/src/icon_theme.rs
+++ b/crates/theme/src/icon_theme.rs
@@ -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",
],
),
(
diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs
index be34bda080e81d..7a6d93b9621fb9 100644
--- a/crates/zed/src/zed.rs
+++ b/crates/zed/src/zed.rs
@@ -5818,7 +5818,6 @@ mod tests {
"context_server",
"copilot",
"copilot_edit_predictions",
- "csv",
"debug_panel",
"debugger",
"dev",
@@ -5872,6 +5871,7 @@ mod tests {
"svg",
"syntax_tree_view",
"tab_switcher",
+ "tabular_data",
"task",
"terminal",
"terminal_panel",