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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user