Add in-app help, formula reference outlines, underline and auto-closing

- Help dialog (?, F1, Datei > Hilfe): user topics, all shortcuts and the
  formula functions with one search over everything. Topics and shortcuts
  are plain data in data/help.ts; the function list comes from
  codeReference.ts so new built-ins appear by themselves.
- While a formula is edited, the grid outlines each referenced cell/range
  in the colour of its position in the text, and the outline can be
  dragged to move or resize the reference, as in Excel. Point-mode picks
  (click, drag, arrow keys) are outlined too, so keyboard picking is no
  longer blind.
- Underline as a cell format, plus Mod+Shift+B/I/U toggles that switch on
  unless every selected cell already has the format.
- A formula committed with open parentheses or an open text literal gets
  the missing closers appended; anything that still doesn't parse is left
  alone so the usual error shows instead of a guess.
- Cells and tables with a fill but no text colour get dark or light text
  by WCAG contrast instead of the theme colour (no black on black).
This commit is contained in:
wkh
2026-10-24 00:38:01 +02:00
parent 0368e96e0c
commit 0fca61eb65
19 changed files with 1420 additions and 16 deletions
+96
View File
@@ -174,6 +174,102 @@ export function currentIdentifierAtCaret(
return { text: last.value, start: last.pos + 1 };
}
/** One cell/range reference found in a formula's text -- see {@link formulaRefSpans}. */
export interface RefSpan {
/** offsets into the formula (including its leading "="): `start` is where
* the qualifier (if any) begins, `refStart` where the A1 part begins */
start: number;
refStart: number;
end: number;
qualifier: string | null;
sheetQualifier?: string;
/** both corners as written (`end` equals `start` for a single cell) */
a: { col: number; row: number; colAbs: boolean; rowAbs: boolean };
b: { col: number; row: number; colAbs: boolean; rowAbs: boolean };
/** a whole-column/whole-row range (`B:B`, `2:5`); its open axis runs to
* {@link WHOLE_LINE_END} */
whole?: LineAxis;
}
/**
* Every cell/range reference in a formula, in text order, with where it sits
* in the text -- what the grid colours while a formula is edited (and lets
* the user drag to change it, Excel-style). Same token-level reading of the
* `reference` grammar rule the parser applies (optional `Blatt!Tabelle::` /
* `Tabelle!` qualifier, `A1`, `A1:B2`, `B:B`), but tolerant: an incomplete
* formula still yields the references written so far. Named ranges are left
* out (they have no position of their own to drag); a formula that doesn't
* even tokenize (open "..." literal) yields nothing.
*/
export function formulaRefSpans(formula: string): RefSpan[] {
if (!formula.startsWith('=')) return [];
let toks: Token[];
try {
toks = tokenize(formula.slice(1));
} catch {
return [];
}
const isName = (t: Token | undefined) => !!t && (t.type === 'IDENT' || t.type === 'QUOTED');
const out: RefSpan[] = [];
let i = 0;
while (i < toks.length) {
const first = toks[i];
let j = i;
let qualifier: string | null = null;
let sheetQualifier: string | undefined;
if (isName(toks[j]) && toks[j + 1]?.type === 'BANG' && isName(toks[j + 2]) && toks[j + 3]?.type === 'DBLCOLON') {
sheetQualifier = toks[j].value;
qualifier = toks[j + 2].value;
j += 4;
} else if (isName(toks[j]) && (toks[j + 1]?.type === 'BANG' || toks[j + 1]?.type === 'DBLCOLON')) {
qualifier = toks[j].value;
j += 2;
}
const line = wholeLineAt(toks, j);
const cell = toks[j];
if (line) {
const max = WHOLE_LINE_END;
const isCol = line.axis === 'col';
out.push({
start: first.pos + 1,
refStart: line.a.pos + 1,
end: line.b.pos + line.b.value.length + 1,
qualifier,
sheetQualifier,
a: { col: isCol ? line.start : 0, row: isCol ? 0 : line.start, colAbs: isCol && line.startAbs, rowAbs: !isCol && line.startAbs },
b: { col: isCol ? line.end : max, row: isCol ? max : line.end, colAbs: isCol && line.endAbs, rowAbs: !isCol && line.endAbs },
whole: line.axis,
});
i = j + 3;
continue;
}
if (cell?.type === 'IDENT' && looksLikeCell(cell.value) && toks[j + 1]?.type !== 'LPAREN') {
const a = parseA1(cell.value);
let last = cell;
let b = a;
const other = toks[j + 2];
if (toks[j + 1]?.type === 'COLON' && other?.type === 'IDENT' && looksLikeCell(other.value)) {
b = parseA1(other.value);
last = other;
j += 2;
}
out.push({
start: first.pos + 1,
refStart: cell.pos + 1,
end: last.pos + last.value.length + 1,
qualifier,
sheetQualifier,
a,
b,
});
i = j + 1;
continue;
}
i++;
}
return out;
}
// ---- whole-column / whole-row references (`B:B`, `A:C`, `1:1`, `$2:$5`) ----
/** Stand-in for "the last row/column" in a whole-column/whole-row range's