From 87fb2f818584a9701e07272a2d72f911149852b4 Mon Sep 17 00:00:00 2001 From: elliot Date: Thu, 7 May 2026 15:45:55 -0400 Subject: [PATCH 1/7] 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]+?))?$/; From 0f925ad03483f9b47b3200ff1b0518025f841f14 Mon Sep 17 00:00:00 2001 From: elliot Date: Thu, 14 May 2026 14:52:20 -0400 Subject: [PATCH 2/7] Add changelog entry --- apps/vscode/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index 984feb9b..af73b17e 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -2,6 +2,9 @@ ## 1.133.0 +- Add custom pair colorization and highlighting for divs in qmds (). + + ## 1.132.0 (Release on 2026-05-05) - Added clickable document links for file paths in `_quarto.yml` files. File paths are now clickable and navigate directly to the referenced file (). From f4b7ac71d23b570b1407e7863726e2be2b036073 Mon Sep 17 00:00:00 2001 From: elliot Date: Thu, 14 May 2026 16:31:11 -0400 Subject: [PATCH 3/7] Change debounce to throttle, now correct --- apps/vscode/src/core/throttle.ts | 41 +++++++++++++++ apps/vscode/src/providers/background.ts | 62 +++++++++++------------ apps/vscode/src/providers/div-brackets.ts | 46 +++++++++++++---- 3 files changed, 108 insertions(+), 41 deletions(-) create mode 100644 apps/vscode/src/core/throttle.ts diff --git a/apps/vscode/src/core/throttle.ts b/apps/vscode/src/core/throttle.ts new file mode 100644 index 00000000..effc84f2 --- /dev/null +++ b/apps/vscode/src/core/throttle.ts @@ -0,0 +1,41 @@ +/* + * throttle.ts + * + * Copyright (C) 2026 by Posit Software, PBC + * + * Unless you have received this program directly from Posit Software pursuant + * to the terms of a commercial license agreement with Posit Software, then + * this program is licensed to you under the terms of version 3 of the + * GNU Affero General Public License. This program is distributed WITHOUT + * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the + * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. + * + */ + +/** + * Creates a throttled version of a function. + * First call executes immediately, subsequent calls within the delay are coalesced. + */ +export function createThrottle( + fn: () => any, + getDelay: () => number +): () => any { + let timer: NodeJS.Timeout | undefined; + let pending = false; + + return () => { + if (timer === undefined) { + fn(); + timer = setTimeout(() => { + if (pending) { + fn(); + pending = false; + } + timer = undefined; + }, getDelay()); + } else { + pending = true; + } + }; +} diff --git a/apps/vscode/src/providers/background.ts b/apps/vscode/src/providers/background.ts index 4f2a0358..7b25a45b 100644 --- a/apps/vscode/src/providers/background.ts +++ b/apps/vscode/src/providers/background.ts @@ -1,7 +1,7 @@ /* * background.ts * - * Copyright (C) 2022 by Posit Software, PBC + * Copyright (C) 2026 by Posit Software, PBC * Copyright (c) [2021] [Chris Bain] (https://github.com/baincd/vscode-markdown-color-plus/) * * Unless you have received this program directly from Posit Software pursuant @@ -16,12 +16,12 @@ import * as vscode from "vscode"; -import debounce from "lodash.debounce"; import { isQuartoDoc, kQuartoDocSelector } from "../core/doc"; import { MarkdownEngine } from "../markdown/engine"; import { isExecutableLanguageBlock } from "quarto-core"; import { vscRange } from "../core/range"; +import { createThrottle } from "../core/throttle"; export function activateBackgroundHighlighter( context: vscode.ExtensionContext, @@ -32,7 +32,7 @@ export function activateBackgroundHighlighter( vscode.workspace.onDidChangeConfiguration( () => { highlightingConfig.sync(); - triggerUpdateAllEditorsDecorations(engine); + updateAllEditorsDecorationsThrottled(engine); }, null, context.subscriptions @@ -45,10 +45,9 @@ export function activateBackgroundHighlighter( if (!isQuartoDoc(doc)) { clearEditorHighlightDecorations(vscode.window.activeTextEditor); } else { - triggerUpdateActiveEditorDecorations( + updateActiveEditorDecorationsThrottled( vscode.window.activeTextEditor, - engine, - highlightingConfig.delayMs() + engine ); } } @@ -59,8 +58,13 @@ export function activateBackgroundHighlighter( // update highlighting when visible text editors change vscode.window.onDidChangeVisibleTextEditors( - (_editors) => { - triggerUpdateAllEditorsDecorations(engine); + (visibleEditors) => { + for (const editor of editorThrottledFunctions.keys()) { + if (!visibleEditors.includes(editor)) { + editorThrottledFunctions.delete(editor); + } + } + updateAllEditorsDecorationsThrottled(engine); }, null, context.subscriptions @@ -73,11 +77,9 @@ export function activateBackgroundHighlighter( return editor.document.uri.toString() === event.document.uri.toString(); }); if (visibleEditor) { - triggerUpdateActiveEditorDecorations( + updateActiveEditorDecorationsThrottled( visibleEditor, engine, - highlightingConfig.delayMs(), - true, event.contentChanges.length === 1 ? event.contentChanges[0].range.start : undefined @@ -97,11 +99,9 @@ export function activateBackgroundHighlighter( token: vscode.CancellationToken ) { if (document === vscode.window.activeTextEditor?.document) { - triggerUpdateActiveEditorDecorations( + updateActiveEditorDecorationsThrottled( vscode.window.activeTextEditor, engine, - highlightingConfig.delayMs(), - true, position, token ); @@ -112,32 +112,32 @@ export function activateBackgroundHighlighter( ); // highlight all editors at activation time - triggerUpdateAllEditorsDecorations(engine); + updateAllEditorsDecorationsThrottled(engine); } -function triggerUpdateActiveEditorDecorations( +// Map of editors to their throttled update functions +const editorThrottledFunctions = new Map void>(); +function updateActiveEditorDecorationsThrottled( editor: vscode.TextEditor, engine: MarkdownEngine, - delay: number, - immediate?: boolean, pos?: vscode.Position, token?: vscode.CancellationToken ) { - debounce( - () => setEditorHighlightDecorations(editor, engine, pos, token), - delay, - { - leading: !!immediate, - } - )(); + let throttled = editorThrottledFunctions.get(editor); + if (!throttled) { + throttled = createThrottle( + () => setEditorHighlightDecorations(editor, engine, pos, token), + () => highlightingConfig.delayMs() + ); + editorThrottledFunctions.set(editor, throttled); + } + throttled(); } -function triggerUpdateAllEditorsDecorations(engine: MarkdownEngine) { - debounce(async () => { - for (const editor of vscode.window.visibleTextEditors) { - await setEditorHighlightDecorations(editor, engine); - } - }, highlightingConfig.delayMs())(); +function updateAllEditorsDecorationsThrottled(engine: MarkdownEngine) { + for (const editor of vscode.window.visibleTextEditors) { + updateActiveEditorDecorationsThrottled(editor, engine); + } } async function setEditorHighlightDecorations( diff --git a/apps/vscode/src/providers/div-brackets.ts b/apps/vscode/src/providers/div-brackets.ts index ac1799cf..3c9a4ebf 100644 --- a/apps/vscode/src/providers/div-brackets.ts +++ b/apps/vscode/src/providers/div-brackets.ts @@ -1,7 +1,7 @@ /* * div-brackets.ts * - * Copyright (C) 2025 by Posit Software, PBC + * Copyright (C) 2026 by Posit Software, PBC * * Unless you have received this program directly from Posit Software pursuant * to the terms of a commercial license agreement with Posit Software, then @@ -15,6 +15,7 @@ import * as vscode from 'vscode'; import { markdownitParser, Token } from 'quarto-core'; +import { createThrottle } from '../core/throttle'; /** * Provides colored decorations for div bracket pairs (:::) @@ -25,12 +26,19 @@ import { markdownitParser, Token } from 'quarto-core'; export function activateDivBracketDecorations(context: vscode.ExtensionContext) { const parser = markdownitParser(); + // Read debounce delay from config + const getDelayMs = () => + vscode.workspace.getConfiguration('quarto').get('cells.background.delay', 250); + // Cache for parsed tokens const parseCache = new Map(); + // Map of editors to their throttled update functions + const editorThrottledFunctions = new Map void>(); + // Define decoration types for different nesting levels (rotating colors) const decorationTypes = [ vscode.window.createTextEditorDecorationType({ @@ -124,18 +132,21 @@ export function activateDivBracketDecorations(context: vscode.ExtensionContext) editor.setDecorations(matchHighlightDecorationType, matchHighlights); } - function triggerUpdateDecorations(editor: vscode.TextEditor | undefined) { - if (editor) { - updateDecorations(editor); + function updateDecorationsThrottled(editor: vscode.TextEditor) { + let throttled = editorThrottledFunctions.get(editor); + if (!throttled) { + throttled = createThrottle(() => updateDecorations(editor), getDelayMs); + editorThrottledFunctions.set(editor, throttled); } + throttled(); } // Update decorations when active editor changes context.subscriptions.push( vscode.window.onDidChangeActiveTextEditor(editor => { if (editor) { - triggerUpdateDecorations(editor); + updateDecorationsThrottled(editor); } }) ); @@ -145,7 +156,7 @@ export function activateDivBracketDecorations(context: vscode.ExtensionContext) vscode.workspace.onDidChangeTextDocument(event => { const editor = vscode.window.activeTextEditor; if (editor && event.document === editor.document) { - triggerUpdateDecorations(editor); + updateDecorationsThrottled(editor); } }) ); @@ -153,15 +164,30 @@ export function activateDivBracketDecorations(context: vscode.ExtensionContext) // Update decorations when cursor moves context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection(event => { - if (event.textEditor === vscode.window.activeTextEditor) { - triggerUpdateDecorations(event.textEditor); + updateDecorationsThrottled(event.textEditor); + }) + ); + + // Clean up cache and throttle state when document is closed + context.subscriptions.push( + vscode.workspace.onDidCloseTextDocument(document => { + parseCache.delete(document.uri.toString()); + }) + ); + + context.subscriptions.push( + vscode.window.onDidChangeVisibleTextEditors(visibleEditors => { + for (const editor of editorThrottledFunctions.keys()) { + if (!visibleEditors.includes(editor)) { + editorThrottledFunctions.delete(editor); + } } }) ); // Update decorations for the active editor now - if (vscode.window.activeTextEditor) { - triggerUpdateDecorations(vscode.window.activeTextEditor); + for (const editor of vscode.window.visibleTextEditors) { + updateDecorationsThrottled(editor); } // Clean up decoration types on deactivation From 2a8f2136baaa92b747803c99b5762729c7448f27 Mon Sep 17 00:00:00 2001 From: elliot Date: Thu, 14 May 2026 16:50:06 -0400 Subject: [PATCH 4/7] Add todo about another incorrect debounce --- apps/vscode/src/providers/context-keys.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/vscode/src/providers/context-keys.ts b/apps/vscode/src/providers/context-keys.ts index f7d588f4..665c5544 100644 --- a/apps/vscode/src/providers/context-keys.ts +++ b/apps/vscode/src/providers/context-keys.ts @@ -65,6 +65,7 @@ export function activateContextKeySetter( vscode.workspace.onDidChangeTextDocument(event => { const activeEditor = vscode.window.activeTextEditor; if (activeEditor) { + // TODO: this debounce is being created and called immediately, which is not correct. debounce( () => { setEditorContextKeys(activeEditor, engine); From 0812de1e2b308fa2252ab262ffc5e6e65007f7db Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 19 May 2026 15:47:14 -0400 Subject: [PATCH 5/7] Change match text color, refactor --- apps/vscode/src/providers/div-brackets.ts | 102 +++++++++++++--------- 1 file changed, 62 insertions(+), 40 deletions(-) diff --git a/apps/vscode/src/providers/div-brackets.ts b/apps/vscode/src/providers/div-brackets.ts index 3c9a4ebf..6dc1f39e 100644 --- a/apps/vscode/src/providers/div-brackets.ts +++ b/apps/vscode/src/providers/div-brackets.ts @@ -17,6 +17,28 @@ import * as vscode from 'vscode'; import { markdownitParser, Token } from 'quarto-core'; import { createThrottle } from '../core/throttle'; +// 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'), + color: new vscode.ThemeColor('editor.foreground'), +}); + /** * Provides colored decorations for div bracket pairs (:::) * @@ -39,34 +61,6 @@ export function activateDivBracketDecorations(context: vscode.ExtensionContext) // Map of editors to their throttled update functions const editorThrottledFunctions = new Map void>(); - // 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; @@ -95,17 +89,7 @@ export function activateDivBracketDecorations(context: vscode.ExtensionContext) 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); - } + const divDepth = getDivDepths(divTokens); // Apply decorations for (const divToken of divTokens) { @@ -132,7 +116,6 @@ export function activateDivBracketDecorations(context: vscode.ExtensionContext) editor.setDecorations(matchHighlightDecorationType, matchHighlights); } - function updateDecorationsThrottled(editor: vscode.TextEditor) { let throttled = editorThrottledFunctions.get(editor); if (!throttled) { @@ -197,3 +180,42 @@ export function activateDivBracketDecorations(context: vscode.ExtensionContext) } }); } + +/** + * Helper to extract ::: range from a line + */ +export 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; +}; + +/** + * Helper to calculate the nesting depths of divs (how many divs a div is nested inside) + * e.g. + * ``` + * :::: + * depth 0 ^ + * ::: + * depth 1 ^ + * ::: + * :::: + * ::: + * depth 0 again + * ::: + * ``` + */ +export function getDivDepths(divTokens: Token[]): Map { + // 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); + } + return divDepth; +} From a65a58b3e4211942a7a29a2dd00f26db1e8ee824 Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 19 May 2026 15:47:20 -0400 Subject: [PATCH 6/7] Add tests --- apps/vscode/src/test/divs.test.ts | 66 +++++++++++++++++++ apps/vscode/src/test/examples/simple-divs.qmd | 15 +++++ 2 files changed, 81 insertions(+) create mode 100644 apps/vscode/src/test/divs.test.ts create mode 100644 apps/vscode/src/test/examples/simple-divs.qmd diff --git a/apps/vscode/src/test/divs.test.ts b/apps/vscode/src/test/divs.test.ts new file mode 100644 index 00000000..a8c1a0ba --- /dev/null +++ b/apps/vscode/src/test/divs.test.ts @@ -0,0 +1,66 @@ +import * as vscode from "vscode"; +import * as assert from "assert"; +import { WORKSPACE_PATH, examplesOutUri, openAndShowExamplesOutTextDocument } from "./test-utils"; +import { MarkdownEngine } from "../markdown/engine"; +import { getDivDepths, getDivMarkerRange } from "../providers/div-brackets"; + +suite("Div detection", function () { + const engine = new MarkdownEngine(); + + suiteSetup(async function () { + await vscode.workspace.fs.delete(examplesOutUri(), { recursive: true }); + await vscode.workspace.fs.copy(vscode.Uri.file(WORKSPACE_PATH), examplesOutUri()); + }); + + test("Detects div tokens in simple document", async function () { + const { doc } = await openAndShowExamplesOutTextDocument("simple-divs.qmd"); + + const tokens = engine.parse(doc); + const divTokens = tokens.filter(t => t.type === "Div"); + + assert.strictEqual( + divTokens.length, + 3, + `Expected 3 div tokens (callout + columns + nested column), found ${divTokens.length}` + ); + }); + + test("Detects many div tokens", async function () { + const { doc } = await openAndShowExamplesOutTextDocument("div-code-blocks.qmd"); + + const tokens = engine.parse(doc); + const divTokens = tokens.filter(t => t.type === "Div"); + + // The file has many .notes divs + assert.ok( + divTokens.length > 10, + `Expected more than 10 div tokens, found ${divTokens.length}` + ); + }); + + test("getDivDepths calculates nesting correctly", async function () { + const { doc } = await openAndShowExamplesOutTextDocument("simple-divs.qmd"); + + const tokens = engine.parse(doc); + const divTokens = tokens.filter(t => t.type === "Div"); + const depths = getDivDepths(divTokens); + + // First div (callout) should be at depth 0 + assert.strictEqual(depths.get(divTokens[0]), 0); + // Second div (columns) should be at depth 0 + assert.strictEqual(depths.get(divTokens[1]), 0); + // Third div (nested column) should be at depth 1 + assert.strictEqual(depths.get(divTokens[2]), 1); + }); + + test("getDivMarkerRange extracts ::: range", async function () { + const { editor } = await openAndShowExamplesOutTextDocument("simple-divs.qmd"); + + // Line 4 has ":::" + const range = getDivMarkerRange(editor, 4); + assert.ok(range, "Expected a range to be found"); + assert.strictEqual(range!.start.line, 4); + assert.strictEqual(range!.start.character, 0); + assert.strictEqual(range!.end.character, 3); + }); +}); diff --git a/apps/vscode/src/test/examples/simple-divs.qmd b/apps/vscode/src/test/examples/simple-divs.qmd new file mode 100644 index 00000000..6acdca2b --- /dev/null +++ b/apps/vscode/src/test/examples/simple-divs.qmd @@ -0,0 +1,15 @@ +--- +title: "Simple Divs Test" +--- + +::: {.callout-note} +A simple div +::: + +Some text. + +:::: {.columns} +::: {.column} +Nested div +::: +:::: From 676c06dc4149095dc5f686572d4ee35e8f35918c Mon Sep 17 00:00:00 2001 From: Elliot Date: Wed, 20 May 2026 11:28:54 -0400 Subject: [PATCH 7/7] Update apps/vscode/CHANGELOG.md Co-authored-by: Julia Silge --- apps/vscode/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index af73b17e..689142f3 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.133.0 -- Add custom pair colorization and highlighting for divs in qmds (). +- Added custom pair colorization and highlighting for divs in qmds (). ## 1.132.0 (Release on 2026-05-05)