Avoid slicing text inside multi-byte characters

Right-to-left line reordering walked each atom's byte range and sliced the
line at every byte, panicking when a neutral multi-byte character such as a
guillemet preceded the first strong character. Walk characters instead.

FIND/FINDALL located the REPLACE keyword in a Unicode-uppercased copy and
sliced the original with that offset; uppercasing can change byte length
(dotless i, ligatures). Uppercase ASCII only, which keeps offsets aligned.
FILLET and CHAMFER inline R/D shorthands now test the original text before
slicing past the first character.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christopher OverbeckandClaude Opus 5 committed 2026-09-17 10:33:18 -05:00
1 parent ef0c6bf403
commit 38aa2ee34d
3 files changed
+32 -6

No files matched your search

+20 -2
View File
@@ -1302,8 +1302,9 @@ impl OpenCADStudio {
let all_mode = cmd.starts_with("FINDALL");
let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
// Split at " REPLACE " keyword (case-insensitive)
let (search, replacement) = if let Some(pos) = rest.to_uppercase().find(" REPLACE ")
// Split at " REPLACE " keyword (case-insensitive). ASCII case
// mapping keeps byte offsets valid for slicing `rest`.
let (search, replacement) = if let Some(pos) = rest.to_ascii_uppercase().find(" REPLACE ")
{
(&rest[..pos], Some(rest[pos + 9..].trim()))
} else {
@@ -2386,3 +2387,20 @@ mod align_selected_bounds_tests {
assert_eq!(line_start_x(&app, b), 2.0, "b must keep its offset from a, not collapse onto it");
}
}
#[cfg(test)]
mod find_replace_command_tests {
use super::*;
/// Unicode case mapping can change a string's byte length (`ı` uppercases
/// to `I`), so the REPLACE keyword split must not slice with offsets
/// taken from a case-mapped copy.
#[test]
fn find_replace_accepts_search_text_whose_case_mapping_changes_length() {
for cmd in ["FIND ı REPLACE x", "FINDALL fi REPLACE fi", "FIND ʼn replace n"] {
let mut app = OpenCADStudio::new_for_test();
let i = app.active_tab;
let _ = app.dispatch_inquiry(cmd, i);
}
}
}