M3: column AutoComplete, spelling dialog, Page Break Preview, icons gallery
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
168b8ec8b7
commit
433726773b
|
|
@ -35,6 +35,8 @@ pub struct EditState {
|
|||
pub request_focus: bool,
|
||||
/// The text changed programmatically: push `caret` into the widget on the next frame.
|
||||
pub sync_caret: bool,
|
||||
/// Column AutoComplete: the full entry the typed text completes to.
|
||||
pub completion: Option<String>,
|
||||
/// Move the caret to the end on the next frame.
|
||||
pub caret_to_end: bool,
|
||||
}
|
||||
|
|
@ -56,6 +58,7 @@ impl EditState {
|
|||
request_focus: true,
|
||||
sync_caret: true,
|
||||
caret_to_end: true,
|
||||
completion: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -272,3 +275,46 @@ mod tests {
|
|||
assert_eq!(r[3].3.as_deref(), Some("Sheet2"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Excel-style AutoComplete: the one distinct text entry in the same column (contiguous data
|
||||
/// above and below) that starts with `typed` (case-insensitive). `None` when ambiguous.
|
||||
pub fn column_completion(sheet: &sheetcraft_engine::model::Sheet, at: CellRef, typed: &str) -> Option<String> {
|
||||
if typed.is_empty() || typed.starts_with('=') || typed.chars().all(|c| c.is_ascii_digit() || c == '.' || c == '-') {
|
||||
return None;
|
||||
}
|
||||
let lower = typed.to_lowercase();
|
||||
let mut found: Option<String> = None;
|
||||
let mut ambiguous = false;
|
||||
let mut scan = |row: u32| -> bool {
|
||||
match sheet.value(CellRef::new(row, at.col)) {
|
||||
sheetcraft_engine::core::Value::Empty => false,
|
||||
sheetcraft_engine::core::Value::Text(t) => {
|
||||
if t.to_lowercase().starts_with(&lower) && t.len() > typed.len() {
|
||||
match &found {
|
||||
Some(f) if !f.eq_ignore_ascii_case(&t) => ambiguous = true,
|
||||
None => found = Some(t.to_string()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
};
|
||||
let mut r = at.row;
|
||||
let mut n = 0;
|
||||
while r > 0 && n < 5000 {
|
||||
r -= 1;
|
||||
n += 1;
|
||||
if !scan(r) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut r = at.row + 1;
|
||||
n = 0;
|
||||
while n < 5000 && scan(r) {
|
||||
r += 1;
|
||||
n += 1;
|
||||
}
|
||||
if ambiguous { None } else { found }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,6 +285,13 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font:
|
|||
ed.caret = r.primary.index.into();
|
||||
}
|
||||
if resp.changed() {
|
||||
ed.completion = None;
|
||||
if !ed.is_formula()
|
||||
&& ed.caret == ed.text.chars().count()
|
||||
&& let Some(sh) = app.session.active().and_then(|d| d.wb.sheet(ed.sheet))
|
||||
{
|
||||
ed.completion = crate::editor::column_completion(sh, ed.cell, &ed.text);
|
||||
}
|
||||
ed.point = None;
|
||||
let names: Vec<String> = function_names();
|
||||
ed.update_autocomplete(&names);
|
||||
|
|
@ -296,6 +303,21 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font:
|
|||
sheetcraft_engine::Mode::Edit
|
||||
};
|
||||
}
|
||||
// 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 pos = resp.rect.left_top() + egui::vec2(output.galley.rect.width(), 0.0);
|
||||
let g = ui.painter().layout_no_wrap(
|
||||
rest.to_string(),
|
||||
output.galley.job.sections.first().map(|s| s.format.font_id.clone()).unwrap_or_default(),
|
||||
Color32::WHITE,
|
||||
);
|
||||
let r = egui::Rect::from_min_size(pos, g.size());
|
||||
ui.painter().rect_filled(r, 0.0, Color32::from_rgb(0x2E, 0x6F, 0xD8));
|
||||
ui.painter().galley(pos, g, Color32::WHITE);
|
||||
}
|
||||
// Autocomplete list and argument hint under the editor.
|
||||
if mine && (!ed.autocomplete.is_empty() || ed.current_function().is_some()) {
|
||||
let below = resp.rect.left_bottom() + vec2(0.0, 4.0);
|
||||
|
|
|
|||
|
|
@ -280,7 +280,11 @@ impl SheetApp {
|
|||
Some(t) => (t, true),
|
||||
None => (current.clone(), false),
|
||||
};
|
||||
self.editor = Some(editor::EditState::new(d.wb.active_sheet, at, text, replace, from_formula_bar));
|
||||
let mut ed = editor::EditState::new(d.wb.active_sheet, at, text, replace, from_formula_bar);
|
||||
if replace && !ed.is_formula() {
|
||||
ed.completion = editor::column_completion(sh, at, &ed.text);
|
||||
}
|
||||
self.editor = Some(ed);
|
||||
self.session.mode = if replace { sheetcraft_engine::Mode::Enter } else { sheetcraft_engine::Mode::Edit };
|
||||
}
|
||||
|
||||
|
|
@ -288,7 +292,10 @@ impl SheetApp {
|
|||
pub fn commit_edit(&mut self, dr: i64, dc: i64, array: bool, fill_selection: bool) -> bool {
|
||||
let Some(ed) = self.editor.take() else { return true };
|
||||
self.session.mode = sheetcraft_engine::Mode::Ready;
|
||||
let text = ed.text.clone();
|
||||
let text = match &ed.completion {
|
||||
Some(full) if full.to_lowercase().starts_with(&ed.text.to_lowercase()) => full.clone(),
|
||||
_ => ed.text.clone(),
|
||||
};
|
||||
// Data validation.
|
||||
if let Some(d) = self.session.active()
|
||||
&& let Some((dv, msg)) = sheetcraft_engine::cmd::data::check_validation(&d.wb, ed.sheet, ed.cell, &text)
|
||||
|
|
|
|||
|
|
@ -111,3 +111,17 @@ fn dialogs_open_and_close() {
|
|||
h.state_mut().message = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_autocomplete_completes_on_enter() {
|
||||
let mut s = blank();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [["North"], ["East"], ["Eastern"]]})).unwrap();
|
||||
s.execute("selection.set", json!({"cell": "A4"})).unwrap();
|
||||
let mut h = harness(s);
|
||||
text(&mut h, "No");
|
||||
key(&mut h, Key::Enter, Modifiers::NONE);
|
||||
assert_eq!(value(&h, "A4"), Value::from("North"));
|
||||
text(&mut h, "Ea"); // ambiguous: East / Eastern
|
||||
key(&mut h, Key::Enter, Modifiers::NONE);
|
||||
assert_eq!(value(&h, "A5"), Value::from("Ea"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue