···253253/**
254254 * Render highlighted formula tokens as an HTML string.
255255 * Each token is wrapped in a <span> with a class based on its type.
256256+ *
257257+ * When a colorMap is provided, cell_ref tokens whose text appears as a key
258258+ * in the map receive an inline `style="color: <value>"` matching the grid
259259+ * range-highlight color. Tokens not in the map keep class-only styling.
256260 */
257257-export function renderHighlightedFormula(tokens: HighlightToken[]): string {
261261+export function renderHighlightedFormula(
262262+ tokens: HighlightToken[],
263263+ colorMap?: Map<string, string>,
264264+): string {
258265 return tokens.map(t => {
259266 const escaped = escapeHtml(t.text);
267267+ if (colorMap && t.type === 'cell_ref') {
268268+ const color = colorMap.get(t.text);
269269+ if (color) {
270270+ return `<span class="formula-token-${t.type}" style="color: ${color}">${escaped}</span>`;
271271+ }
272272+ }
260273 return `<span class="formula-token-${t.type}">${escaped}</span>`;
261274 }).join('');
262275}
+42-7
src/sheets/main.ts
···13451345 attachCellEditorAutocomplete(input);
13461346 // Attach formula UX enhancements: range highlights + tooltip
13471347 attachCellEditorFormulaUX(input, td);
13481348- // Initial highlight/range update
13491349- updateFormulaHighlight(value);
13481348+ // Initial highlight/range update (with range colors — editing active)
13491349+ updateFormulaHighlight(value, true);
13501350 updateFormulaRangeHighlights(value);
13511351}
13521352···19371937// --- Formula syntax highlighting helpers ---
19381938const formulaHighlightLayer = document.getElementById('formula-highlight-layer');
1939193919401940-function updateFormulaHighlight(text) {
19401940+function updateFormulaHighlight(text, useRangeColors = false) {
19411941 if (!formulaHighlightLayer) return;
19421942 if (text && text.startsWith('=')) {
19431943 const tokens = tokenizeForHighlighting(text);
19441944- formulaHighlightLayer.innerHTML = renderHighlightedFormula(tokens);
19441944+19451945+ // Build a color map from range highlights so formula-bar refs match grid borders.
19461946+ // The tokenizer splits "A1:B5" into tokens [A1, :, B5] while extractFormulaRanges
19471947+ // returns the full ref "A1:B5". We add both the full ref and the individual parts
19481948+ // so either token shape gets the right color. Cross-sheet refs like "Sheet2!A1"
19491949+ // are emitted as a single token matching the extracted ref directly.
19501950+ let colorMap: Map<string, string> | undefined;
19511951+ if (useRangeColors) {
19521952+ const formula = text.slice(1);
19531953+ const ranges = extractFormulaRanges(formula);
19541954+ if (ranges.length > 0) {
19551955+ const colored = assignRangeColors(ranges);
19561956+ colorMap = new Map<string, string>();
19571957+ for (const cr of colored) {
19581958+ if (!colorMap.has(cr.ref)) {
19591959+ colorMap.set(cr.ref, cr.color);
19601960+ }
19611961+ // For range refs (A1:B5), also map the individual cell parts
19621962+ // so the tokenizer's separate cell_ref tokens get colored
19631963+ const localRef = cr.ref.includes('!') ? cr.ref.split('!').pop()! : cr.ref;
19641964+ if (localRef.includes(':')) {
19651965+ const [startRef, endRef] = localRef.split(':');
19661966+ if (startRef && !colorMap.has(startRef)) {
19671967+ colorMap.set(startRef, cr.color);
19681968+ }
19691969+ if (endRef && !colorMap.has(endRef)) {
19701970+ colorMap.set(endRef, cr.color);
19711971+ }
19721972+ }
19731973+ }
19741974+ }
19751975+ }
19761976+19771977+ formulaHighlightLayer.innerHTML = renderHighlightedFormula(tokens, colorMap);
19451978 formulaHighlightLayer.style.display = '';
19461979 formulaInput.classList.add('formula-highlighting');
19471980 } else {
···1976200919772010function onFormulaInputUpdate() {
19782011 const text = formulaInput.value;
19791979- updateFormulaHighlight(text);
20122012+ updateFormulaHighlight(text, true);
19802013 updateFormulaRangeHighlights(text);
19812014 updateFormulaTooltip(text, formulaInput.selectionStart, formulaInput);
19822015 if (formulaHighlightLayer) {
···45724605});
45734606formulaInput.addEventListener('focus', () => {
45744607 const text = formulaInput.value;
45754575- updateFormulaHighlight(text);
46084608+ updateFormulaHighlight(text, true);
45764609 updateFormulaRangeHighlights(text);
45774610});
45784611formulaInput.addEventListener('blur', () => {
45794612 hideTooltip();
45804613 clearGridHighlights();
46144614+ // Re-render without range colors when editing ends
46154615+ updateFormulaHighlight(formulaInput.value);
45814616});
4582461745834618function attachCellEditorFormulaUX(inputEl, anchorTd) {
45844619 inputEl.addEventListener('input', () => {
45854620 const text = inputEl.value;
45864621 formulaInput.value = text;
45874587- updateFormulaHighlight(text);
46224622+ updateFormulaHighlight(text, true);
45884623 updateFormulaRangeHighlights(text);
45894624 updateFormulaTooltip(text, inputEl.selectionStart, anchorTd);
45904625 });
+73
tests/formula-highlighter.test.ts
···265265 const textContent = html.replace(/<[^>]+>/g, '');
266266 expect(textContent).toBe(formula);
267267 });
268268+269269+ it('applies inline color from colorMap to cell_ref tokens', () => {
270270+ const tokens = tokenizeForHighlighting('=A1+B1');
271271+ const colorMap = new Map<string, string>([
272272+ ['A1', 'oklch(0.55 0.2 250)'],
273273+ ['B1', 'oklch(0.55 0.18 155)'],
274274+ ]);
275275+ const html = renderHighlightedFormula(tokens, colorMap);
276276+ expect(html).toContain('style="color: oklch(0.55 0.2 250)"');
277277+ expect(html).toContain('style="color: oklch(0.55 0.18 155)"');
278278+ });
279279+280280+ it('does not apply inline color when colorMap is omitted', () => {
281281+ const tokens = tokenizeForHighlighting('=A1+B1');
282282+ const html = renderHighlightedFormula(tokens);
283283+ expect(html).not.toContain('style=');
284284+ });
285285+286286+ it('does not apply inline color to non-ref tokens even with colorMap', () => {
287287+ const tokens = tokenizeForHighlighting('=SUM(A1)');
288288+ const colorMap = new Map<string, string>([['A1', 'red']]);
289289+ const html = renderHighlightedFormula(tokens, colorMap);
290290+ // SUM token should not have inline style
291291+ expect(html).toMatch(/<span class="formula-token-function">SUM<\/span>/);
292292+ // A1 token should have inline style
293293+ expect(html).toContain('style="color: red"');
294294+ });
295295+296296+ it('applies color to cross-sheet references in colorMap', () => {
297297+ const tokens = tokenizeForHighlighting('=Sheet2!A1+B1');
298298+ const colorMap = new Map<string, string>([
299299+ ['Sheet2!A1', 'oklch(0.5 0.2 300)'],
300300+ ['B1', 'oklch(0.55 0.2 25)'],
301301+ ]);
302302+ const html = renderHighlightedFormula(tokens, colorMap);
303303+ expect(html).toContain('style="color: oklch(0.5 0.2 300)"');
304304+ expect(html).toContain('style="color: oklch(0.55 0.2 25)"');
305305+ });
306306+307307+ it('applies color to quoted cross-sheet references in colorMap', () => {
308308+ const tokens = tokenizeForHighlighting("='My Sheet'!A1");
309309+ const colorMap = new Map<string, string>([
310310+ ["'My Sheet'!A1", 'oklch(0.6 0.18 60)'],
311311+ ]);
312312+ const html = renderHighlightedFormula(tokens, colorMap);
313313+ expect(html).toContain('style="color: oklch(0.6 0.18 60)"');
314314+ });
315315+316316+ it('same ref used multiple times gets same color from colorMap', () => {
317317+ const tokens = tokenizeForHighlighting('=A1+A1');
318318+ const colorMap = new Map<string, string>([['A1', 'blue']]);
319319+ const html = renderHighlightedFormula(tokens, colorMap);
320320+ // Both A1 tokens should have the same color
321321+ const matches = html.match(/style="color: blue"/g);
322322+ expect(matches).toHaveLength(2);
323323+ });
324324+325325+ it('falls back to class-only styling for refs not in colorMap', () => {
326326+ const tokens = tokenizeForHighlighting('=A1+B1');
327327+ const colorMap = new Map<string, string>([['A1', 'red']]);
328328+ const html = renderHighlightedFormula(tokens, colorMap);
329329+ // A1 gets color
330330+ expect(html).toContain('style="color: red"');
331331+ // B1 keeps class-only (no style attribute on that span)
332332+ expect(html).toMatch(/<span class="formula-token-cell_ref">B1<\/span>/);
333333+ });
334334+335335+ it('applies color to absolute cell references in colorMap', () => {
336336+ const tokens = tokenizeForHighlighting('=$B$2');
337337+ const colorMap = new Map<string, string>([['$B$2', 'green']]);
338338+ const html = renderHighlightedFormula(tokens, colorMap);
339339+ expect(html).toContain('style="color: green"');
340340+ });
268341});