fix: quote CSV fields only when required
This commit is contained in:
@@ -9,3 +9,4 @@
|
|||||||
- should be able to copy without row names (`Y` keybinding)
|
- should be able to copy without row names (`Y` keybinding)
|
||||||
- `Ym` should copy to markdown format
|
- `Ym` should copy to markdown format
|
||||||
- `Yc` should copy to csv format
|
- `Yc` should copy to csv format
|
||||||
|
- [ ] add ability to update values from the row view
|
||||||
+21
-6
@@ -677,6 +677,10 @@ fn append_record<'a>(
|
|||||||
if index > 0 {
|
if index > 0 {
|
||||||
output.push(',');
|
output.push(',');
|
||||||
}
|
}
|
||||||
|
if !value.contains([',', '"', '\r', '\n']) {
|
||||||
|
output.push_str(value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
output.push('"');
|
output.push('"');
|
||||||
for character in value.chars() {
|
for character in value.chars() {
|
||||||
if character == '"' {
|
if character == '"' {
|
||||||
@@ -755,6 +759,17 @@ fn moved_index(selected: Option<usize>, count: usize, delta: isize) -> Option<us
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::moved_index;
|
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]
|
#[test]
|
||||||
fn rectangular_selection_copies_full_cells_and_resets_on_data_changes() -> crate::Result<()> {
|
fn rectangular_selection_copies_full_cells_and_resets_on_data_changes() -> crate::Result<()> {
|
||||||
use super::{App, Focus};
|
use super::{App, Focus};
|
||||||
@@ -799,14 +814,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.selection_text(true, false).as_deref(),
|
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!(
|
assert_eq!(
|
||||||
app.selection_text(false, false).as_deref(),
|
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!(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.
|
// A prefix cancels on focus changes rather than capturing a later key.
|
||||||
app.handle_rows_command(key('y'));
|
app.handle_rows_command(key('y'));
|
||||||
assert!(!app.handle_rows_command(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)));
|
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_rows_command(key('v'));
|
||||||
app.handle_key(key('n'));
|
app.handle_key(key('n'));
|
||||||
assert!(app.selection_anchor.is_none());
|
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_rows_command(key('v'));
|
||||||
app.handle_key(key('s'));
|
app.handle_key(key('s'));
|
||||||
assert!(app.selection_anchor.is_none());
|
assert!(app.selection_anchor.is_none());
|
||||||
@@ -824,11 +839,11 @@ mod tests {
|
|||||||
app.query.value = "SELECT 42 AS answer".into();
|
app.query.value = "SELECT 42 AS answer".into();
|
||||||
app.execute_query();
|
app.execute_query();
|
||||||
assert!(app.selection_anchor.is_none());
|
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.handle_rows_command(key('v'));
|
||||||
app.query.value = "SELECT missing FROM nowhere".into();
|
app.query.value = "SELECT missing FROM nowhere".into();
|
||||||
app.execute_query();
|
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());
|
assert!(app.selection_anchor.is_some());
|
||||||
app.open_relation(Some(0));
|
app.open_relation(Some(0));
|
||||||
assert!(app.selection_anchor.is_none());
|
assert!(app.selection_anchor.is_none());
|
||||||
|
|||||||
+2
-1
@@ -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\
|
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\
|
Paging, refresh, schema/relation changes, and successful queries clear blocks.\n\
|
||||||
Copy uses full displayed values; NULL/control escapes are preserved.\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\
|
Clipboard: macOS pbcopy; Windows PowerShell; Wayland wl-copy; X11 xclip.\n\n\
|
||||||
Query editor\n\
|
Query editor\n\
|
||||||
i/a/I/A Enter Insert mode (at/after/start/end)\n\
|
i/a/I/A Enter Insert mode (at/after/start/end)\n\
|
||||||
|
|||||||
+2
-1
@@ -79,7 +79,8 @@ Navigation:
|
|||||||
Blocks cover the current loaded page, including cells scrolled out of view.
|
Blocks cover the current loaded page, including cells scrolled out of view.
|
||||||
Paging, refreshing, opening a relation, schema changes, and successful queries clear blocks.
|
Paging, refreshing, opening a relation, schema changes, and successful queries clear blocks.
|
||||||
Copies use full displayed cell values (NULL and escaped control characters included).
|
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,
|
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.
|
or xclip on X11. A missing/failing tool reports an error without clearing the block.
|
||||||
c Add a configured database in a floating window
|
c Add a configured database in a floating window
|
||||||
|
|||||||
Reference in New Issue
Block a user