fix: quote CSV fields only when required

This commit is contained in:
Valdemar Lorenzen
2026-10-21 10:35:43 +02:00
parent c533e71ecc
commit 138490eecd
4 changed files with 27 additions and 9 deletions
+1
View File
@@ -9,3 +9,4 @@
- should be able to copy without row names (`Y` keybinding)
- `Ym` should copy to markdown format
- `Yc` should copy to csv format
- [ ] add ability to update values from the row view
+21 -6
View File
@@ -677,6 +677,10 @@ fn append_record<'a>(
if index > 0 {
output.push(',');
}
if !value.contains([',', '"', '\r', '\n']) {
output.push_str(value);
continue;
}
output.push('"');
for character in value.chars() {
if character == '"' {
@@ -755,6 +759,17 @@ fn moved_index(selected: Option<usize>, count: usize, delta: isize) -> Option<us
mod tests {
use super::moved_index;
#[test]
fn csv_quotes_only_fields_with_delimiters_quotes_or_line_breaks() {
let mut output = String::new();
super::append_record(
&mut output,
["42", "00123", "界", "", "a,b", "a\"b", "a\rb", "a\nb"].into_iter(),
false,
);
assert_eq!(output, "42,00123,界,,\"a,b\",\"a\"\"b\",\"a\rb\",\"a\nb\"\r\n");
}
#[test]
fn rectangular_selection_copies_full_cells_and_resets_on_data_changes() -> crate::Result<()> {
use super::{App, Focus};
@@ -799,14 +814,14 @@ mod tests {
);
assert_eq!(
app.selection_text(true, false).as_deref(),
Some("\"b\",\"c\"\r\n\"a|b\",\"say \"\"hi\"\",界\"\r\n\"NULL\",\"<&>\\tail\"\r\n")
Some("b,c\r\na|b,\"say \"\"hi\"\",界\"\r\nNULL,<&>\\tail\r\n")
);
assert_eq!(
app.selection_text(false, false).as_deref(),
Some("\"a|b\",\"say \"\"hi\"\",界\"\r\n\"NULL\",\"<&>\\tail\"\r\n")
Some("a|b,\"say \"\"hi\"\",界\"\r\nNULL,<&>\\tail\r\n")
);
assert!(app.handle_rows_command(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)));
assert_eq!(app.selection_text(false, false).as_deref(), Some("\"a|b\"\r\n"));
assert_eq!(app.selection_text(false, false).as_deref(), Some("a|b\r\n"));
// A prefix cancels on focus changes rather than capturing a later key.
app.handle_rows_command(key('y'));
assert!(!app.handle_rows_command(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)));
@@ -814,7 +829,7 @@ mod tests {
app.handle_rows_command(key('v'));
app.handle_key(key('n'));
assert!(app.selection_anchor.is_none());
assert_eq!(app.selection_text(false, false).as_deref(), Some("\"101\"\r\n"));
assert_eq!(app.selection_text(false, false).as_deref(), Some("101\r\n"));
app.handle_rows_command(key('v'));
app.handle_key(key('s'));
assert!(app.selection_anchor.is_none());
@@ -824,11 +839,11 @@ mod tests {
app.query.value = "SELECT 42 AS answer".into();
app.execute_query();
assert!(app.selection_anchor.is_none());
assert_eq!(app.selection_text(true, false).as_deref(), Some("\"answer\"\r\n\"42\"\r\n"));
assert_eq!(app.selection_text(true, false).as_deref(), Some("answer\r\n42\r\n"));
app.handle_rows_command(key('v'));
app.query.value = "SELECT missing FROM nowhere".into();
app.execute_query();
assert_eq!(app.selection_text(false, false).as_deref(), Some("\"42\"\r\n"));
assert_eq!(app.selection_text(false, false).as_deref(), Some("42\r\n"));
assert!(app.selection_anchor.is_some());
app.open_relation(Some(0));
assert!(app.selection_anchor.is_none());
+2 -1
View File
@@ -433,7 +433,8 @@ fn draw_help(frame: &mut Frame, screen: Rect, offset: &mut u16) {
With no block, copy uses the current cell. Blocks stay within the loaded page.\n\
Paging, refresh, schema/relation changes, and successful queries clear blocks.\n\
Copy uses full displayed values; NULL/control escapes are preserved.\n\
Headerless Markdown has blank headings. CSV quotes fields with CRLF endings.\n\
Headerless Markdown has blank headings. CSV uses CRLF and quotes only fields\n\
containing commas, double quotes, or line breaks.\n\
Clipboard: macOS pbcopy; Windows PowerShell; Wayland wl-copy; X11 xclip.\n\n\
Query editor\n\
i/a/I/A Enter Insert mode (at/after/start/end)\n\
+2 -1
View File
@@ -79,7 +79,8 @@ Navigation:
Blocks cover the current loaded page, including cells scrolled out of view.
Paging, refreshing, opening a relation, schema changes, and successful queries clear blocks.
Copies use full displayed cell values (NULL and escaped control characters included).
Headerless Markdown uses blank headings; CSV quotes fields and uses CRLF line endings.
Headerless Markdown uses blank headings; CSV quotes only fields containing commas,
double quotes, or line breaks, and uses CRLF line endings.
Clipboard tools: pbcopy on macOS, PowerShell on Windows, wl-copy on Wayland,
or xclip on X11. A missing/failing tool reports an error without clearing the block.
c Add a configured database in a floating window