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

Configure Feed

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

Merge pull request 'feat(sheets): color-code cell references in formula bar' (#98) from feat/formula-bar-colors into main

scott e9c4972c be4d0494

+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 });