#2225: copy quotes only cells with a tab or line break

Copy wrapped any cell that held a double quote in quotes (and doubled
its quotes), so copying `say "hi"` into another app gave
`"say ""hi"""`. Excel quotes only text with a tab or line break; a cell
with just quotes goes on the clipboard as it is.

Pasting text read a field that starts with a quote as quoted even when
its closing quote doesn't end the field, so `"hi" there` (which Excel
copies as it is) pasted as `hi there`. A field is now quoted only when
its closing quote is followed by a tab, a line end or the end of the
text; otherwise it is the text as written, as Excel pastes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hector Villarreal
2026-11-03 01:02:40 -06:00
co-authored by Claude Opus 5.5
parent 178f3eb745
commit fe965e7581
4 changed files with 34 additions and 2 deletions
+10
View File
@@ -228,14 +228,23 @@ pub(crate) fn parse_tsv(text: &str) -> Vec<Vec<String>> {
let mut chars = text.chars().peekable();
let mut quoted = false;
let mut at_start = true;
// A quoted field's text as it stands in the input, for a field that turns out not to be one.
let mut raw = String::new();
while let Some(c) = chars.next() {
if quoted {
raw.push(c);
if c == '"' {
if chars.peek() == Some(&'"') {
field.push('"');
raw.push('"');
chars.next();
} else {
quoted = false;
// A field is quoted only when its closing quote ends it, as Excel reads it:
// `"hi" there` is that text, quotes included.
if !matches!(chars.peek(), None | Some('\t' | '\n' | '\r')) {
field = std::mem::take(&mut raw);
}
}
} else {
field.push(c);
@@ -246,6 +255,7 @@ pub(crate) fn parse_tsv(text: &str) -> Vec<Vec<String>> {
'"' if at_start => {
quoted = true;
at_start = false;
raw = String::from('"');
}
'\t' => {
row.push(std::mem::take(&mut field));
+19
View File
@@ -307,6 +307,9 @@ fn tsv() {
assert_eq!(parse_tsv("a\tb\n1\t2\n"), vec![vec!["a", "b"], vec!["1", "2"]]);
assert_eq!(parse_tsv("\"x\ty\"\tz"), vec![vec!["x\ty", "z"]]);
assert_eq!(parse_tsv("\"he said \"\"hi\"\"\""), vec![vec!["he said \"hi\""]]);
// #2225: a quote that doesn't close the field leaves it as written, as Excel pastes it.
assert_eq!(parse_tsv("\"hi\" there\tx\n"), vec![vec!["\"hi\" there", "x"]]);
assert_eq!(parse_tsv("say \"hi\"\n"), vec![vec!["say \"hi\""]]);
}
#[test]
@@ -371,3 +374,19 @@ fn replace_case_insensitive_keeps_text_when_folding_moves_boundaries() {
let sh = s.doc().unwrap().wb.active().unwrap();
assert_eq!(sh.value(CellRef::new(0, 0)), Value::text(text));
}
#[test]
fn copy_quotes_only_cells_with_tabs_or_line_breaks() {
// #2225: a cell holding a quote was wrapped in quotes on copy; Excel quotes only text with a
// tab or line break, and pastes the result back as the same cells.
let mut s = Session::new();
s.new_workbook();
for (cell, input) in [("A1", "say \"hi\""), ("A2", "plain"), ("A3", "\"hi\" there"), ("A4", "x \"q\"\ny"), ("A5", "tab\tinside")] {
s.execute("cell.set", json!({"cell": cell, "input": input})).unwrap();
}
let text = s.execute("edit.copy", json!({"range": "A1:A5"})).unwrap()["text"].as_str().unwrap().to_string();
assert_eq!(text, "say \"hi\"\nplain\n\"hi\" there\n\"x \"\"q\"\"\ny\"\n\"tab\tinside\"\n");
let rows = parse_tsv(&text);
let cells: Vec<&str> = rows.iter().map(|r| r[0].as_str()).collect();
assert_eq!(cells, ["say \"hi\"", "plain", "\"hi\" there", "x \"q\"\ny", "tab\tinside"]);
}