UI: AutoComplete folds case beyond ASCII and cuts the proposed rest on characters (#842)

This commit is contained in:
Marco Arnulfo 2026-10-13 00:18:55 +02:00
parent 7a8235a586
commit 9c3db37ca2
3 changed files with 30 additions and 4 deletions

View File

@ -251,9 +251,10 @@ pub fn column_completion(sheet: &gridcraft_engine::model::Sheet, at: CellRef, ty
match sheet.value(CellRef::new(row, at.col)) {
gridcraft_engine::core::Value::Empty => false,
gridcraft_engine::core::Value::Text(t) => {
if t.to_lowercase().starts_with(&lower) && t.len() > typed.len() {
let folded = t.to_lowercase();
if folded.starts_with(&lower) && folded.len() > lower.len() {
match &found {
Some(f) if !f.eq_ignore_ascii_case(&t) => ambiguous = true,
Some(f) if f.to_lowercase() != folded => ambiguous = true,
None => found = Some(t.to_string()),
_ => {}
}
@ -281,6 +282,13 @@ pub fn column_completion(sheet: &gridcraft_engine::model::Sheet, at: CellRef, ty
if ambiguous { None } else { found }
}
/// The part of `full` after the `typed` prefix, counted in characters: case folding can change
/// a character's byte length ("ß" is two bytes, "ẞ" three).
pub fn completion_rest<'a>(full: &'a str, typed: &str) -> Option<&'a str> {
let start = full.char_indices().nth(typed.chars().count())?.0;
full.get(start..)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -336,8 +336,7 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font:
}
// Column AutoComplete: the rest of the suggested entry, shown selected after the caret.
if let Some(full) = &ed.completion
&& full.len() > ed.text.len()
&& let Some(rest) = full.get(ed.text.len()..)
&& let Some(rest) = crate::editor::completion_rest(full, &ed.text)
{
let pos = resp.rect.left_top() + egui::vec2(output.galley.rect.width(), 0.0);
let g = ui.painter().layout_no_wrap(

View File

@ -537,6 +537,25 @@ fn column_autocomplete_skips_ambiguous_prefixes_and_numbers() {
assert_eq!(completion(&h), ("12".into(), None), "a number is not completed");
}
#[test]
fn column_autocomplete_folds_case_beyond_ascii() {
let mut s = blank();
s.execute("range.setValues", json!({"range": "A1", "values": [["Ébène"], ["Ébano"], ["ébano"], ["STRAẞENBAHN"]]})).unwrap();
s.execute("selection.set", json!({"cell": "A5"})).unwrap();
let mut h = harness(s);
text(&mut h, "éba");
assert_eq!(completion(&h), ("éba".into(), Some("ébano".into())), "Ébano and ébano are one entry, Ébène doesn't match");
h.state_mut().cancel_edit();
text(&mut h, "éb");
assert_eq!(completion(&h).1, None, "Ébène and Ébano differ past the prefix");
h.state_mut().cancel_edit();
// "ß" is two bytes, "ẞ" three: the proposed rest starts after the fifth character, not the sixth byte.
text(&mut h, "straß");
assert_eq!(completion(&h), ("straß".into(), Some("STRAẞENBAHN".into())));
let painted = h.output().shapes.iter().any(|c| matches!(&c.shape, egui::Shape::Text(t) if t.galley.job.text == "ENBAHN"));
assert!(painted, "the rest \"ENBAHN\" is drawn after the caret");
}
#[test]
fn column_autocomplete_can_be_turned_off() {
assert!(gridcraft_ui_egui::UiState::default().autocomplete);