From 87fb2f818584a9701e07272a2d72f911149852b4 Mon Sep 17 00:00:00 2001 From: elliot Date: Thu, 7 May 2026 15:45:55 -0400 Subject: [PATCH] Add custom pair colorization and highlighting for divs --- apps/vscode/src/main.ts | 4 + apps/vscode/src/providers/div-brackets.ts | 173 ++++++++++++++++++++++ packages/core/src/markdownit/divs.ts | 2 +- 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 apps/vscode/src/providers/div-brackets.ts diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index e4eca1d7..c333c3d4 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -47,6 +47,7 @@ import { activateBackgroundHighlighter } from "./providers/background"; import { activateYamlLinks } from "./providers/yaml-links"; import { activateYamlFilepathCompletions } from "./providers/yaml-filepath-completions"; import { activateContextKeySetter } from "./providers/context-keys"; +import { activateDivBracketDecorations } from "./providers/div-brackets"; import { CommandManager } from "./core/command"; import { createQuartoExtensionApi, QuartoExtensionApi } from "./api"; @@ -221,6 +222,9 @@ export async function activate(context: vscode.ExtensionContext): Promise(); + + // Define decoration types for different nesting levels (rotating colors) + const decorationTypes = [ + vscode.window.createTextEditorDecorationType({ + color: new vscode.ThemeColor('editorBracketHighlight.foreground1'), + }), + vscode.window.createTextEditorDecorationType({ + color: new vscode.ThemeColor('editorBracketHighlight.foreground2'), + }), + vscode.window.createTextEditorDecorationType({ + color: new vscode.ThemeColor('editorBracketHighlight.foreground3'), + }), + ]; + + // Decoration type for matching pairs when cursor is on a bracket + const matchHighlightDecorationType = vscode.window.createTextEditorDecorationType({ + backgroundColor: new vscode.ThemeColor('editor.wordHighlightBackground'), + border: '1px solid', + borderRadius: '6px', + borderColor: new vscode.ThemeColor('editor.wordHighlightBorder'), + }); + + // Helper to extract ::: range from a line + const getDivMarkerRange = (editor: vscode.TextEditor, line: number): vscode.Range | null => { + const lineText = editor.document.lineAt(line).text; + const match = lineText.match(/^(:::+)/); + return match ? new vscode.Range(line, 0, line, match[1].length) : null; + }; + + function updateDecorations(editor: vscode.TextEditor) { + if (editor.document.languageId !== 'quarto') return; + + const docUri = editor.document.uri.toString(); + const docVersion = editor.document.version; + + // Check cache + let divTokens: Token[]; + const cached = parseCache.get(docUri); + if (cached && cached.version === docVersion) { + divTokens = cached.divTokens; + } else { + // Parse the document + const doc = { + getText: () => editor.document.getText(), + uri: docUri, + version: docVersion, + lineCount: editor.document.lineCount, + }; + + divTokens = parser(doc as any).filter(t => t.type === 'Div'); + parseCache.set(docUri, { version: docVersion, divTokens }); + } + + // Group decorations by nesting level + const decorationsByLevel = decorationTypes.map(() => [] as vscode.Range[]); + const matchHighlights: vscode.Range[] = []; + + // Calculate nesting depth for all divs in a single pass using a stack + const divDepth = new Map(); + const stack: Token[] = []; + for (const divToken of divTokens) { + // Pop divs from stack that have ended before this div starts + while (stack.length > 0 && stack.at(-1)!.range.end.line < divToken.range.start.line) { + stack.pop(); + } + divDepth.set(divToken, stack.length); + stack.push(divToken); + } + + // Apply decorations + for (const divToken of divTokens) { + const openLine = divToken.range.start.line; + const closeLine = divToken.range.end.line; + const depth = divDepth.get(divToken)!; + const colorIndex = depth % decorationTypes.length; + const cursorLine = editor.selection.active.line; + const isCursorOver = cursorLine === openLine || cursorLine === closeLine; + + const openRange = getDivMarkerRange(editor, openLine); + const closeRange = getDivMarkerRange(editor, closeLine); + + const targetList = isCursorOver ? + matchHighlights : + decorationsByLevel[colorIndex]; + if (openRange) targetList.push(openRange); + if (closeRange) targetList.push(closeRange); + } + + decorationTypes.forEach((decorationType, i) => + editor.setDecorations(decorationType, decorationsByLevel[i]) + ); + editor.setDecorations(matchHighlightDecorationType, matchHighlights); + } + + function triggerUpdateDecorations(editor: vscode.TextEditor | undefined) { + + if (editor) { + updateDecorations(editor); + } + } + + // Update decorations when active editor changes + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(editor => { + if (editor) { + triggerUpdateDecorations(editor); + } + }) + ); + + // Update decorations when document changes + context.subscriptions.push( + vscode.workspace.onDidChangeTextDocument(event => { + const editor = vscode.window.activeTextEditor; + if (editor && event.document === editor.document) { + triggerUpdateDecorations(editor); + } + }) + ); + + // Update decorations when cursor moves + context.subscriptions.push( + vscode.window.onDidChangeTextEditorSelection(event => { + if (event.textEditor === vscode.window.activeTextEditor) { + triggerUpdateDecorations(event.textEditor); + } + }) + ); + + // Update decorations for the active editor now + if (vscode.window.activeTextEditor) { + triggerUpdateDecorations(vscode.window.activeTextEditor); + } + + // Clean up decoration types on deactivation + context.subscriptions.push({ + dispose: () => { + decorationTypes.forEach(type => type.dispose()); + } + }); +} diff --git a/packages/core/src/markdownit/divs.ts b/packages/core/src/markdownit/divs.ts index 3eab79c8..7a3947f5 100644 --- a/packages/core/src/markdownit/divs.ts +++ b/packages/core/src/markdownit/divs.ts @@ -69,7 +69,7 @@ export const divPlugin = (md: MarkdownIt) => { } // Three or more colons followed by a an optional brace with attributes - const divBraceRegex = /^(:::+)\s*(?:(\{[\s\S]+?\}))?$/; + const divBraceRegex = /^(:::+)\s*(?:(\{[\s\S]*?\}))?$/; // Three or more colons followed by a string with no braces const divNoBraceRegex = /^(:::+)\s*(?:([^{}\s]+?))?$/;