Keyboard: Ctrl+Shift+A after a function name inserts its argument names

This commit is contained in:
Marco Arnulfo
2026-10-16 06:24:36 +02:00
parent f1894c6da6
commit 0033dfcdeb
4 changed files with 148 additions and 2 deletions
+38
View File
@@ -144,6 +144,44 @@ impl EditState {
true
}
/// Ctrl+Shift+A, as in Excel: right after a function name, or after its `(`, inserts the
/// argument names (optional ones without their brackets) and the closing parenthesis, and
/// the `(` too when missing. Anywhere else nothing changes and it returns false.
pub fn insert_argument_names(&mut self) -> bool {
if !self.is_formula() {
return false;
}
let caret_b = self.byte_caret();
let (Some(before), Some(after)) = (self.text.get(..caret_b), self.text.get(caret_b..)) else { return false };
// An odd number of quotes before the caret: it is inside a text literal.
if before.matches('"').count() % 2 == 1 {
return false;
}
let open = before.ends_with('(');
if !open && after.starts_with('(') {
return false;
}
let head = before.strip_suffix('(').unwrap_or(before);
let name: String =
head.chars().rev().take_while(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '_').collect::<String>().chars().rev().collect();
if name.is_empty() || name.starts_with(|c: char| c.is_ascii_digit()) {
return false;
}
let Some(signature) = crate::formula_locale::signature(&name, self.locale) else { return false };
let sep = self.locale.list_separator();
let inner = signature.split_once('(').map(|(_, rest)| rest.trim_end().trim_end_matches(')')).unwrap_or("");
let args: Vec<&str> = inner.split(sep).map(|a| a.trim().trim_start_matches('[').trim_end_matches(']')).filter(|a| !a.is_empty()).collect();
let close = if open && after.starts_with(')') { "" } else { ")" };
let inserted = format!("{}{}{close}", if open { "" } else { "(" }, args.join(&sep.to_string()));
self.text = format!("{before}{inserted}{after}");
self.caret += inserted.chars().count();
self.autocomplete.clear();
self.completion = None;
self.caret_to_end = false;
self.sync_caret = true;
true
}
/// The function call the caret is inside and the argument index (for the hint tooltip).
pub fn current_function(&self) -> Option<(String, usize)> {
if !self.is_formula() {
+16
View File
@@ -255,6 +255,22 @@ pub fn editor_widget(app: &mut SheetApp, ui: &mut egui::Ui, id: egui::Id, font:
if ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, Key::F4)) {
cycle_anchor(&mut ed);
}
// Ctrl+Shift+A (Control+Shift+A on a Mac) inserts the argument names. Consumed anywhere,
// as in Excel, so it never falls through to the text's select-all.
if ui.input_mut(|i| i.consume_key(egui::Modifiers::CTRL | egui::Modifiers::SHIFT, Key::A)) {
let (text, caret) = (ed.text.clone(), ed.caret);
if ed.insert_argument_names()
&& let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), id)
{
// The text edit records an undo point only once typing pauses: mark the text
// before the shortcut so one Undo takes back just the inserted names.
let mut undoer = state.undoer();
let cursor = state.cursor.char_range().unwrap_or(egui::text::CCursorRange::one(egui::text::CCursor::new(caret)));
undoer.add_undo(&(cursor, text));
state.set_undoer(undoer);
state.store(ui.ctx(), id);
}
}
}
// The text widget.
let refs = formula_refs(&ed.text);
+90
View File
@@ -0,0 +1,90 @@
//! Ctrl+Shift+A (Control+Shift+A on a Mac) after a function name inserts its argument names, as
//! in Excel.
use egui::{Context, Event, Key, Modifiers, RawInput, Rect, pos2, vec2};
use gridcraft_engine::Session;
use gridcraft_ui_egui::SheetApp;
fn frame(ctx: &Context, app: &mut SheetApp, time: f64, events: Vec<Event>) {
let mut output = ctx.run_ui(
RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(1200.0, 800.0))), time: Some(time), events, ..Default::default() },
|ui| {
let ctx = ui.ctx().clone();
app.logic(&ctx);
app.ui(ui);
},
);
output.textures_delta.clear();
}
fn key(key: Key, modifiers: Modifiers) -> Event {
Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }
}
fn ctrl_shift_a() -> Event {
key(Key::A, Modifiers::CTRL | Modifiers::SHIFT)
}
/// A cell editor holding `text`, caret at the end.
fn editing(text: &str) -> (Context, SheetApp) {
let ctx = Context::default();
let mut session = Session::new();
session.new_workbook();
let mut app = SheetApp::new(session, Default::default());
for t in 0..4 {
frame(&ctx, &mut app, f64::from(t) * 0.01, vec![]);
}
app.begin_edit(Some(text.into()), false);
for t in 4..8 {
frame(&ctx, &mut app, f64::from(t) * 0.01, vec![]);
}
(ctx, app)
}
fn text(app: &SheetApp) -> &str {
app.editor.as_ref().map(|e| e.text.as_str()).unwrap_or("<no editor>")
}
#[test]
fn after_the_opening_parenthesis_inserts_the_argument_names() {
let (ctx, mut app) = editing("=SUMIF(");
frame(&ctx, &mut app, 0.1, vec![ctrl_shift_a()]);
frame(&ctx, &mut app, 0.11, vec![]);
assert_eq!(text(&app), "=SUMIF(range,criteria,sum_range)");
}
#[test]
fn after_a_bare_function_name_also_adds_the_parentheses() {
for (typed, expected) in
[("=SUMIF", "=SUMIF(range,criteria,sum_range)"), ("=1+sumif", "=1+sumif(range,criteria,sum_range)"), ("=TODAY", "=TODAY()")]
{
let (ctx, mut app) = editing(typed);
frame(&ctx, &mut app, 0.1, vec![ctrl_shift_a()]);
frame(&ctx, &mut app, 0.11, vec![]);
assert_eq!(text(&app), expected, "after {typed}");
}
}
#[test]
fn anywhere_else_the_shortcut_changes_nothing() {
for typed in ["=SUMIF(A1", "=SUMIF(A1,", "=NOTAFUNCTION", "=A1+", "=\"SUMIF(", "SUMIF("] {
let (ctx, mut app) = editing(typed);
frame(&ctx, &mut app, 0.1, vec![ctrl_shift_a()]);
frame(&ctx, &mut app, 0.11, vec![]);
assert_eq!(text(&app), typed, "after {typed}");
}
}
#[test]
fn one_undo_takes_back_the_inserted_names_only() {
let (ctx, mut app) = editing("=SUMI");
// Typed just before the shortcut: the text edit's own undo has not settled on it yet.
frame(&ctx, &mut app, 0.1, vec![Event::Text("F(".into())]);
frame(&ctx, &mut app, 0.11, vec![]);
assert_eq!(text(&app), "=SUMIF(");
frame(&ctx, &mut app, 0.12, vec![ctrl_shift_a()]);
frame(&ctx, &mut app, 0.13, vec![]);
assert_eq!(text(&app), "=SUMIF(range,criteria,sum_range)");
frame(&ctx, &mut app, 0.14, vec![key(Key::Z, Modifiers::COMMAND)]);
frame(&ctx, &mut app, 0.15, vec![]);
assert_eq!(text(&app), "=SUMIF(");
}
+4 -2
View File
@@ -1,6 +1,6 @@
# UI parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (horizontal scrollbar in the sheet tab bar, #840) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Ctrl+Shift+A inserts a function's argument names) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
@@ -124,7 +124,8 @@ otherwise. Number-format and border keys match the physical key (Ctrl+Shift+4, w
| F5, Shift+F5 (Mac), F7, Shift+F9, Ctrl+Alt+F9, F12, Ctrl+F4 | same | Go To, Find, Spelling, calculate sheet / all, Save As, close | **added** |
| Shift+F10, Alt+Shift+F10, Ctrl+F1 | Shift+F10, Cmd+Option+R | context menu, Error Checking, ribbon | **added** |
| F1, F3, F4 (repeat), F6 / Shift+F6, F8 / Shift+F8, F11, End mode | same | help, Paste Name, Repeat, pane cycling, extend/add mode, chart sheet | missing (no such feature yet) |
| Ctrl+6, Ctrl+8, Ctrl+Q, Ctrl+Shift+A, Ctrl+A after a function name, Ctrl+Shift+U, Ctrl+Alt+P | same | objects, outline symbols, Quick Analysis, argument names, Function Arguments, formula bar height, tooltips | missing |
| Ctrl+Shift+A after a function name | Control+Shift+A | argument names: `=SUMIF(` becomes `=SUMIF(range,criteria,sum_range)`, the `(` added when missing; one Undo takes them back | **added** |
| Ctrl+6, Ctrl+8, Ctrl+Q, Ctrl+A after a function name, Ctrl+Shift+U, Ctrl+Alt+P | same | objects, outline symbols, Quick Analysis, Function Arguments, formula bar height, tooltips | missing |
| Ctrl+Shift+Page Up / Down, Ctrl+Alt+5, Ctrl+Shift+F1, Ctrl+F5–F10, Alt+F8 / F11 / F12 | same | group sheets, shape cycling, full screen, window keys, macros, VBA, Power Query | missing |
| — | Cmd+= (calculate), Cmd+T (references while editing; F4 works), Shift+F7, Cmd+Shift+L | Mac-only extras | missing (Cmd+= is egui's zoom key; no thesaurus or Modify Cell Style) |
@@ -161,6 +162,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| Date | Change | Summary |
|---|---|---|
| 2026-10-16 | minor | Ctrl+Shift+A (Control+Shift+A on a Mac) after a function name, or after its `(`, inserts the argument names and the closing parenthesis, as in Excel; one Undo in the editor takes them back |
| 2026-10-12 | minor | The File view's recent list tells when each workbook was last changed, read off the UI thread |
| 2026-10-13 | minor | Horizontal scrollbar in the sheet tab bar: drag, track click pages, frozen columns excluded from the page (#840) |
| 2026-10-12 | minor | Ctrl+wheel zoom steps along a ladder of percentages about 20% apart that never skips 100%, gliding to each step with animations on, so a notch out undoes a notch in (it drifted: 100 → 122 → 101); Excel steps 15 points, see decisions.md |