Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(sheets): color-code cell references in formula bar to match grid highlights

Cell references in the formula bar now display with inline colors that
match the colored range highlight borders shown on the grid. Colors are
applied when editing (focus/input) and cleared on blur for clean display.

Handles cross-sheet refs, absolute refs ($B$2), range refs (A1:B5),
and quoted sheet names. Adds 7 new unit tests for colorMap rendering.

Closes #112

+129 -8
+14 -1
src/sheets/formula-highlighter.ts
··· 253 253 /** 254 254 * Render highlighted formula tokens as an HTML string. 255 255 * Each token is wrapped in a <span> with a class based on its type. 256 + * 257 + * When a colorMap is provided, cell_ref tokens whose text appears as a key 258 + * in the map receive an inline `style="color: <value>"` matching the grid 259 + * range-highlight color. Tokens not in the map keep class-only styling. 256 260 */ 257 - export function renderHighlightedFormula(tokens: HighlightToken[]): string { 261 + export function renderHighlightedFormula( 262 + tokens: HighlightToken[], 263 + colorMap?: Map<string, string>, 264 + ): string { 258 265 return tokens.map(t => { 259 266 const escaped = escapeHtml(t.text); 267 + if (colorMap && t.type === 'cell_ref') { 268 + const color = colorMap.get(t.text); 269 + if (color) { 270 + return `<span class="formula-token-${t.type}" style="color: ${color}">${escaped}</span>`; 271 + } 272 + } 260 273 return `<span class="formula-token-${t.type}">${escaped}</span>`; 261 274 }).join(''); 262 275 }
+42 -7
src/sheets/main.ts
··· 1345 1345 attachCellEditorAutocomplete(input); 1346 1346 // Attach formula UX enhancements: range highlights + tooltip 1347 1347 attachCellEditorFormulaUX(input, td); 1348 - // Initial highlight/range update 1349 - updateFormulaHighlight(value); 1348 + // Initial highlight/range update (with range colors — editing active) 1349 + updateFormulaHighlight(value, true); 1350 1350 updateFormulaRangeHighlights(value); 1351 1351 } 1352 1352 ··· 1937 1937 // --- Formula syntax highlighting helpers --- 1938 1938 const formulaHighlightLayer = document.getElementById('formula-highlight-layer'); 1939 1939 1940 - function updateFormulaHighlight(text) { 1940 + function updateFormulaHighlight(text, useRangeColors = false) { 1941 1941 if (!formulaHighlightLayer) return; 1942 1942 if (text && text.startsWith('=')) { 1943 1943 const tokens = tokenizeForHighlighting(text); 1944 - formulaHighlightLayer.innerHTML = renderHighlightedFormula(tokens); 1944 + 1945 + // Build a color map from range highlights so formula-bar refs match grid borders. 1946 + // The tokenizer splits "A1:B5" into tokens [A1, :, B5] while extractFormulaRanges 1947 + // returns the full ref "A1:B5". We add both the full ref and the individual parts 1948 + // so either token shape gets the right color. Cross-sheet refs like "Sheet2!A1" 1949 + // are emitted as a single token matching the extracted ref directly. 1950 + let colorMap: Map<string, string> | undefined; 1951 + if (useRangeColors) { 1952 + const formula = text.slice(1); 1953 + const ranges = extractFormulaRanges(formula); 1954 + if (ranges.length > 0) { 1955 + const colored = assignRangeColors(ranges); 1956 + colorMap = new Map<string, string>(); 1957 + for (const cr of colored) { 1958 + if (!colorMap.has(cr.ref)) { 1959 + colorMap.set(cr.ref, cr.color); 1960 + } 1961 + // For range refs (A1:B5), also map the individual cell parts 1962 + // so the tokenizer's separate cell_ref tokens get colored 1963 + const localRef = cr.ref.includes('!') ? cr.ref.split('!').pop()! : cr.ref; 1964 + if (localRef.includes(':')) { 1965 + const [startRef, endRef] = localRef.split(':'); 1966 + if (startRef && !colorMap.has(startRef)) { 1967 + colorMap.set(startRef, cr.color); 1968 + } 1969 + if (endRef && !colorMap.has(endRef)) { 1970 + colorMap.set(endRef, cr.color); 1971 + } 1972 + } 1973 + } 1974 + } 1975 + } 1976 + 1977 + formulaHighlightLayer.innerHTML = renderHighlightedFormula(tokens, colorMap); 1945 1978 formulaHighlightLayer.style.display = ''; 1946 1979 formulaInput.classList.add('formula-highlighting'); 1947 1980 } else { ··· 1976 2009 1977 2010 function onFormulaInputUpdate() { 1978 2011 const text = formulaInput.value; 1979 - updateFormulaHighlight(text); 2012 + updateFormulaHighlight(text, true); 1980 2013 updateFormulaRangeHighlights(text); 1981 2014 updateFormulaTooltip(text, formulaInput.selectionStart, formulaInput); 1982 2015 if (formulaHighlightLayer) { ··· 4572 4605 }); 4573 4606 formulaInput.addEventListener('focus', () => { 4574 4607 const text = formulaInput.value; 4575 - updateFormulaHighlight(text); 4608 + updateFormulaHighlight(text, true); 4576 4609 updateFormulaRangeHighlights(text); 4577 4610 }); 4578 4611 formulaInput.addEventListener('blur', () => { 4579 4612 hideTooltip(); 4580 4613 clearGridHighlights(); 4614 + // Re-render without range colors when editing ends 4615 + updateFormulaHighlight(formulaInput.value); 4581 4616 }); 4582 4617 4583 4618 function attachCellEditorFormulaUX(inputEl, anchorTd) { 4584 4619 inputEl.addEventListener('input', () => { 4585 4620 const text = inputEl.value; 4586 4621 formulaInput.value = text; 4587 - updateFormulaHighlight(text); 4622 + updateFormulaHighlight(text, true); 4588 4623 updateFormulaRangeHighlights(text); 4589 4624 updateFormulaTooltip(text, inputEl.selectionStart, anchorTd); 4590 4625 });
+73
tests/formula-highlighter.test.ts
··· 265 265 const textContent = html.replace(/<[^>]+>/g, ''); 266 266 expect(textContent).toBe(formula); 267 267 }); 268 + 269 + it('applies inline color from colorMap to cell_ref tokens', () => { 270 + const tokens = tokenizeForHighlighting('=A1+B1'); 271 + const colorMap = new Map<string, string>([ 272 + ['A1', 'oklch(0.55 0.2 250)'], 273 + ['B1', 'oklch(0.55 0.18 155)'], 274 + ]); 275 + const html = renderHighlightedFormula(tokens, colorMap); 276 + expect(html).toContain('style="color: oklch(0.55 0.2 250)"'); 277 + expect(html).toContain('style="color: oklch(0.55 0.18 155)"'); 278 + }); 279 + 280 + it('does not apply inline color when colorMap is omitted', () => { 281 + const tokens = tokenizeForHighlighting('=A1+B1'); 282 + const html = renderHighlightedFormula(tokens); 283 + expect(html).not.toContain('style='); 284 + }); 285 + 286 + it('does not apply inline color to non-ref tokens even with colorMap', () => { 287 + const tokens = tokenizeForHighlighting('=SUM(A1)'); 288 + const colorMap = new Map<string, string>([['A1', 'red']]); 289 + const html = renderHighlightedFormula(tokens, colorMap); 290 + // SUM token should not have inline style 291 + expect(html).toMatch(/<span class="formula-token-function">SUM<\/span>/); 292 + // A1 token should have inline style 293 + expect(html).toContain('style="color: red"'); 294 + }); 295 + 296 + it('applies color to cross-sheet references in colorMap', () => { 297 + const tokens = tokenizeForHighlighting('=Sheet2!A1+B1'); 298 + const colorMap = new Map<string, string>([ 299 + ['Sheet2!A1', 'oklch(0.5 0.2 300)'], 300 + ['B1', 'oklch(0.55 0.2 25)'], 301 + ]); 302 + const html = renderHighlightedFormula(tokens, colorMap); 303 + expect(html).toContain('style="color: oklch(0.5 0.2 300)"'); 304 + expect(html).toContain('style="color: oklch(0.55 0.2 25)"'); 305 + }); 306 + 307 + it('applies color to quoted cross-sheet references in colorMap', () => { 308 + const tokens = tokenizeForHighlighting("='My Sheet'!A1"); 309 + const colorMap = new Map<string, string>([ 310 + ["'My Sheet'!A1", 'oklch(0.6 0.18 60)'], 311 + ]); 312 + const html = renderHighlightedFormula(tokens, colorMap); 313 + expect(html).toContain('style="color: oklch(0.6 0.18 60)"'); 314 + }); 315 + 316 + it('same ref used multiple times gets same color from colorMap', () => { 317 + const tokens = tokenizeForHighlighting('=A1+A1'); 318 + const colorMap = new Map<string, string>([['A1', 'blue']]); 319 + const html = renderHighlightedFormula(tokens, colorMap); 320 + // Both A1 tokens should have the same color 321 + const matches = html.match(/style="color: blue"/g); 322 + expect(matches).toHaveLength(2); 323 + }); 324 + 325 + it('falls back to class-only styling for refs not in colorMap', () => { 326 + const tokens = tokenizeForHighlighting('=A1+B1'); 327 + const colorMap = new Map<string, string>([['A1', 'red']]); 328 + const html = renderHighlightedFormula(tokens, colorMap); 329 + // A1 gets color 330 + expect(html).toContain('style="color: red"'); 331 + // B1 keeps class-only (no style attribute on that span) 332 + expect(html).toMatch(/<span class="formula-token-cell_ref">B1<\/span>/); 333 + }); 334 + 335 + it('applies color to absolute cell references in colorMap', () => { 336 + const tokens = tokenizeForHighlighting('=$B$2'); 337 + const colorMap = new Map<string, string>([['$B$2', 'green']]); 338 + const html = renderHighlightedFormula(tokens, colorMap); 339 + expect(html).toContain('style="color: green"'); 340 + }); 268 341 });