From 710da284189b431d28b661dde1e8189cca6f57a6 Mon Sep 17 00:00:00 2001 From: elliot Date: Fri, 8 May 2026 12:46:05 -0400 Subject: [PATCH 1/7] Add setting and command to show/hide cells in outline --- apps/lsp/src/config.ts | 10 ++++- apps/lsp/src/service/config.ts | 4 +- apps/lsp/src/service/index.ts | 2 +- .../src/service/providers/document-symbols.ts | 22 +++++++++- apps/vscode/package.json | 10 +++++ apps/vscode/src/main.ts | 3 ++ apps/vscode/src/providers/symbols.ts | 44 +++++++++++++++++++ 7 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 apps/vscode/src/providers/symbols.ts diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index cd8941fa2..4c870b81f 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -47,6 +47,7 @@ export interface Settings { }; readonly symbols: { readonly exportToWorkspace: 'default' | 'all' | 'none'; + readonly showCodeCellsInOutline: boolean; }; }; readonly markdown: { @@ -96,7 +97,8 @@ function defaultSettings(): Settings { extensions: [] }, symbols: { - exportToWorkspace: 'all' + exportToWorkspace: 'all', + showCodeCellsInOutline: true } }, markdown: { @@ -181,7 +183,8 @@ export class ConfigurationManager extends Disposable { extensions: quarto?.mathjax?.extensions ?? this._settings.quarto.mathjax.extensions }, symbols: { - exportToWorkspace: quarto?.symbols?.exportToWorkspace ?? this._settings.quarto.symbols.exportToWorkspace + exportToWorkspace: quarto?.symbols?.exportToWorkspace ?? this._settings.quarto.symbols.exportToWorkspace, + showCodeCellsInOutline: quarto?.symbols?.showCodeCellsInOutline ?? this._settings.quarto.symbols.showCodeCellsInOutline } } }; @@ -258,6 +261,9 @@ export function lsConfiguration(configManager: ConfigurationManager): LsConfigur }, get exportSymbolsToWorkspace(): 'default' | 'all' | 'none' { return configManager.getSettings().quarto.symbols.exportToWorkspace; + }, + get showCodeCellsInOutline(): boolean { + return configManager.getSettings().quarto.symbols.showCodeCellsInOutline; } }; } diff --git a/apps/lsp/src/service/config.ts b/apps/lsp/src/service/config.ts index 14404470a..0f2ddbcd9 100644 --- a/apps/lsp/src/service/config.ts +++ b/apps/lsp/src/service/config.ts @@ -80,6 +80,7 @@ export interface LsConfiguration { readonly mathjaxScale: number; readonly mathjaxExtensions: readonly MathjaxSupportedExtension[]; readonly exportSymbolsToWorkspace: 'default' | 'all' | 'none'; + readonly showCodeCellsInOutline: boolean; } export const defaultMarkdownFileExtension = 'qmd'; @@ -111,7 +112,8 @@ const defaultConfig: LsConfiguration = { colorTheme: 'light', mathjaxScale: 1, mathjaxExtensions: [], - exportSymbolsToWorkspace: 'all' + exportSymbolsToWorkspace: 'all', + showCodeCellsInOutline: true }; export function defaultLsConfiguration(): LsConfiguration { diff --git a/apps/lsp/src/service/index.ts b/apps/lsp/src/service/index.ts index 1c6b40033..6544891c9 100644 --- a/apps/lsp/src/service/index.ts +++ b/apps/lsp/src/service/index.ts @@ -208,7 +208,7 @@ export function createLanguageService(init: LanguageServiceInitialization): IMdL const definitionsProvider = new MdDefinitionProvider(config, init.workspace, tocProvider, linkCache); const diagnosticOnSaveComputer = new DiagnosticOnSaveComputer(init.quarto); const diagnosticsComputer = new DiagnosticComputer(config, init.workspace, linkProvider, tocProvider, logger); - const docSymbolProvider = new MdDocumentSymbolProvider(tocProvider, linkProvider, logger); + const docSymbolProvider = new MdDocumentSymbolProvider(config, tocProvider, linkProvider, logger); const workspaceSymbolProvider = new MdWorkspaceSymbolProvider(init.workspace, init.config, docSymbolProvider); const documentHighlightProvider = new MdDocumentHighlightProvider(config, tocProvider, linkProvider); diff --git a/apps/lsp/src/service/providers/document-symbols.ts b/apps/lsp/src/service/providers/document-symbols.ts index 678a8a1a5..91b120d49 100644 --- a/apps/lsp/src/service/providers/document-symbols.ts +++ b/apps/lsp/src/service/providers/document-symbols.ts @@ -16,9 +16,10 @@ import { CancellationToken } from 'vscode-languageserver'; import * as lsp from 'vscode-languageserver-types'; import { isBefore, makeRange, Document } from 'quarto-core'; -import { ILogger, LogLevel } from '../logging'; +import { ILogger } from '../logging'; import { MdTableOfContentsProvider, TableOfContents, TocEntry, TocEntryType } from '../toc'; import { MdLinkDefinition, MdLinkKind, MdLinkProvider } from './document-links'; +import { LsConfiguration } from '../config'; interface MarkdownSymbol { readonly level: number; @@ -36,12 +37,15 @@ export class MdDocumentSymbolProvider { readonly #tocProvider: MdTableOfContentsProvider; readonly #linkProvider: MdLinkProvider; readonly #logger: ILogger; + readonly #config: LsConfiguration; constructor( + config: LsConfiguration, tocProvider: MdTableOfContentsProvider, linkProvider: MdLinkProvider, logger: ILogger, ) { + this.#config = config; this.#tocProvider = tocProvider; this.#linkProvider = linkProvider; this.#logger = logger; @@ -75,7 +79,21 @@ export class MdDocumentSymbolProvider { range: makeRange(0, 0, document.lineCount + 1, 0), }; const additionalSymbols = [...linkSymbols]; - this.#buildTocSymbolTree(root, toc.entries.filter(entry => entry.type !== TocEntryType.Title), additionalSymbols); + + // Filter out TOC entries based on configuration + const filteredEntries = toc.entries.filter(entry => { + // Always exclude title entries + if (entry.type === TocEntryType.Title) { + return false; + } + // Exclude all code cells if the setting is disabled + if (entry.type === TocEntryType.CodeCell && !this.#config.showCodeCellsInOutline) { + return false; + } + return true; + }); + + this.#buildTocSymbolTree(root, filteredEntries, additionalSymbols); // Put remaining link definitions into top level document instead of last header root.children.push(...additionalSymbols); return root.children; diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 596dbe845..c5606fd35 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -292,6 +292,11 @@ "title": "Clear Cache...", "category": "Quarto" }, + { + "command": "quarto.toggleCodeCellsInOutline", + "title": "Toggle Code Cells in Outline", + "category": "Quarto" + }, { "command": "quarto.convertToIpynb", "title": "Convert to .ipynb", @@ -1364,6 +1369,11 @@ ], "default": "default", "description": "Whether Markdown elements like section headers are included in workspace symbol search." + }, + "quarto.symbols.showCodeCellsInOutline": { + "type": "boolean", + "default": true, + "description": "Show code cells in the document outline." } } }, diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index e4eca1d70..d24465aae 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -40,6 +40,7 @@ import { activateDenoConfig } from "./providers/deno-config"; import { textFormattingCommands } from "./providers/text-format"; import { newDocumentCommands } from "./providers/newdoc"; import { insertCommands } from "./providers/insert"; +import { symbolsCommands } from "./providers/symbols"; import { activateDiagram } from "./providers/diagram/diagram"; import { activateCodeFormatting } from "./providers/format"; import { activateOptionEnterProvider } from "./providers/option"; @@ -157,6 +158,8 @@ export async function activate(context: vscode.ExtensionContext): Promise("symbols.showCodeCellsInOutline", true); + const newValue = !currentValue; + + await config.update("symbols.showCodeCellsInOutline", newValue, vscode.ConfigurationTarget.Global); + + // Hack: trigger outline refresh by making a no-op edit + const editor = vscode.window.activeTextEditor; + if (editor && editor.document.languageId === "quarto") { + await editor.edit(edit => edit.insert(new vscode.Position(0, 0), " ")); + await editor.edit(edit => edit.delete(new vscode.Range(0, 0, 0, 1))); + } + + vscode.window.showInformationMessage( + `Code cells in outline will now be ${newValue ? "shown" : "hidden"}.` + ); + } +} + +export function symbolsCommands(): Command[] { + return [new ToggleCodeCellsInOutlineCommand()]; +} From f6bd5f84728ce1b71c0fd5b95e4e0d0cdb4543d9 Mon Sep 17 00:00:00 2001 From: Elliot Date: Thu, 14 May 2026 14:43:16 -0400 Subject: [PATCH 2/7] Update apps/vscode/src/providers/symbols.ts Co-authored-by: Julia Silge --- apps/vscode/src/providers/symbols.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vscode/src/providers/symbols.ts b/apps/vscode/src/providers/symbols.ts index 8bb7a4687..dd4eec31b 100644 --- a/apps/vscode/src/providers/symbols.ts +++ b/apps/vscode/src/providers/symbols.ts @@ -1,7 +1,7 @@ /* * symbols.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 From bd1d8b3f7442c3f96c8d0a20ef90dfe01a77a0ae Mon Sep 17 00:00:00 2001 From: elliot Date: Thu, 14 May 2026 14:50:46 -0400 Subject: [PATCH 3/7] Add changelog entry --- apps/vscode/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/vscode/CHANGELOG.md b/apps/vscode/CHANGELOG.md index 984feb9ba..80656b4a0 100644 --- a/apps/vscode/CHANGELOG.md +++ b/apps/vscode/CHANGELOG.md @@ -2,6 +2,8 @@ ## 1.133.0 +- Add setting and command to show/hide cells in outline () + ## 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 d866b0307e967475aba31c49e281638bafe9ef8b Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 19 May 2026 12:09:30 -0400 Subject: [PATCH 4/7] Fix outline not being refreshed when user manually changes config --- apps/overview.md | 21 ++++++++++++++++++ apps/vscode/src/main.ts | 33 ++++++++++++++++++++++++++++ apps/vscode/src/providers/symbols.ts | 8 +------ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/apps/overview.md b/apps/overview.md index c7490d1d3..b4f243e0b 100644 --- a/apps/overview.md +++ b/apps/overview.md @@ -89,6 +89,27 @@ out the extension there are a couple of places where your logs can end up: that says EXTENSION HOST. - `Quarto` output console for [[LSP]] code +### LSP Log Levels + +The `quarto.server.logLevel` setting controls **LSP server** logs (from `apps/lsp/`): +- `"trace"` - Most verbose, includes all requests/notifications +- `"debug"` - Debug information +- `"info"` - Informational messages +- `"warn"` - Warnings only (default) +- `"error"` - Errors only +- `"off"` - No logging + +When debugging the LSP, you may need to set `"quarto.server.logLevel": "info"` or `"trace"` in your user settings to see detailed LSP logs in the Quarto output channel. + +Available logging methods in the LSP (in `apps/lsp/`): +- `logger.logTrace()` - Only appears at trace level +- `logger.logDebug()` - Appears at debug level and below +- `logger.logInfo()` - Appears at info level and below +- `logger.logWarn()` - Appears at warn level and below (use for important debug messages during development) +- `logger.logError()` - Always appears unless logging is off + +Note: Extension host code (in `apps/vscode/src/`) uses `outputChannel.info()`, `outputChannel.warn()`, etc. for logging (e.g., "Activated Quarto extension."). These logs appear in the same Quarto output channel but are not controlled by the `quarto.server.logLevel` setting. + ## Examples of Controlling the Visual Editor from the server-side of the extension ### Example: Setting cursor position diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index d24465aae..6484dcfd0 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -121,6 +121,9 @@ export async function activate(context: vscode.ExtensionContext): Promise { + if (event.affectsConfiguration("quarto.symbols.showCodeCellsInOutline")) { + // This edit triggers VS Code to re-request document symbols from the LSP, + // which will then use the updated configuration value. It is necessary + // because VSCode seems to have its own outline cache that we cannot otherwise + // invalidate from the extension side. + const editor = vscode.window.activeTextEditor; + if (editor && editor.document.languageId === "quarto") { + // The undoStopBefore/After: false options prevent these edits from creating undo stops, + // minimizing their impact on the undo history. + // See: https://code.visualstudio.com/api/references/vscode-api#TextEditorEdit + await editor.edit( + edit => edit.insert(new vscode.Position(0, 0), " "), + { undoStopBefore: false, undoStopAfter: false } + ); + await editor.edit( + edit => edit.delete(new vscode.Range(0, 0, 0, 1)), + { undoStopBefore: false, undoStopAfter: false } + ); + } + } + }) + ); +} + export async function deactivate() { return deactivateLsp(); } diff --git a/apps/vscode/src/providers/symbols.ts b/apps/vscode/src/providers/symbols.ts index dd4eec31b..68f21e80e 100644 --- a/apps/vscode/src/providers/symbols.ts +++ b/apps/vscode/src/providers/symbols.ts @@ -24,15 +24,9 @@ class ToggleCodeCellsInOutlineCommand implements Command { const currentValue = config.get("symbols.showCodeCellsInOutline", true); const newValue = !currentValue; + // Update the configuration - the `registerOutlineConfigListener`config listener handles outline refresh await config.update("symbols.showCodeCellsInOutline", newValue, vscode.ConfigurationTarget.Global); - // Hack: trigger outline refresh by making a no-op edit - const editor = vscode.window.activeTextEditor; - if (editor && editor.document.languageId === "quarto") { - await editor.edit(edit => edit.insert(new vscode.Position(0, 0), " ")); - await editor.edit(edit => edit.delete(new vscode.Range(0, 0, 0, 1))); - } - vscode.window.showInformationMessage( `Code cells in outline will now be ${newValue ? "shown" : "hidden"}.` ); From 6aae8212d1b90d62198900a22f6c57c949fc3173 Mon Sep 17 00:00:00 2001 From: elliot Date: Tue, 19 May 2026 12:16:54 -0400 Subject: [PATCH 5/7] Fix non-active editor not refreshing outline on config change --- apps/vscode/src/main.ts | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 6484dcfd0..0e429047f 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -292,22 +292,21 @@ function registerOutlineConfigListener(context: vscode.ExtensionContext) { vscode.workspace.onDidChangeConfiguration(async (event) => { if (event.affectsConfiguration("quarto.symbols.showCodeCellsInOutline")) { // This edit triggers VS Code to re-request document symbols from the LSP, - // which will then use the updated configuration value. It is necessary - // because VSCode seems to have its own outline cache that we cannot otherwise - // invalidate from the extension side. - const editor = vscode.window.activeTextEditor; - if (editor && editor.document.languageId === "quarto") { - // The undoStopBefore/After: false options prevent these edits from creating undo stops, - // minimizing their impact on the undo history. - // See: https://code.visualstudio.com/api/references/vscode-api#TextEditorEdit - await editor.edit( - edit => edit.insert(new vscode.Position(0, 0), " "), - { undoStopBefore: false, undoStopAfter: false } - ); - await editor.edit( - edit => edit.delete(new vscode.Range(0, 0, 0, 1)), - { undoStopBefore: false, undoStopAfter: false } - ); + // which will then use the updated configuration value. + // The undoStopBefore/After: false options prevent these edits from creating undo stops, + // minimizing their impact on the undo history. + // See: https://code.visualstudio.com/api/references/vscode-api#TextEditorEdit + for (const editor of vscode.window.visibleTextEditors) { + if (editor.document.languageId === "quarto") { + await editor.edit( + edit => edit.insert(new vscode.Position(0, 0), " "), + { undoStopBefore: false, undoStopAfter: false } + ); + await editor.edit( + edit => edit.delete(new vscode.Range(0, 0, 0, 1)), + { undoStopBefore: false, undoStopAfter: false } + ); + } } } }) From 338c1459dcd9730bb375302b357288b6891b60c2 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Tue, 19 May 2026 19:25:08 -0600 Subject: [PATCH 6/7] Refresh outline via dynamic registrationand fix cold-start race --- apps/lsp/src/index.ts | 43 ++++++++++++++++++++++- apps/vscode/src/main.ts | 51 ++++++++++++++++++---------- apps/vscode/src/providers/symbols.ts | 3 +- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/apps/lsp/src/index.ts b/apps/lsp/src/index.ts index 11a4b6a71..2095b63cd 100644 --- a/apps/lsp/src/index.ts +++ b/apps/lsp/src/index.ts @@ -18,8 +18,10 @@ import path from "path"; import { ClientCapabilities, Definition, + Disposable, DocumentLink, DocumentSymbol, + DocumentSymbolRequest, FoldingRange, InitializeParams, ProposedFeatures, @@ -74,6 +76,13 @@ let initializationOptions: LspInitializationOptions | undefined; // Markdown language service let mdLs: IMdLanguageService | undefined; +// Resolved once `mdLs` has been created in `onInitialized`. Request handlers +// that depend on `mdLs` should `await` this so that requests arriving during +// the async portion of startup do not race and return empty results that the +// client then caches (e.g. an empty document outline after a window reload). +let resolveMdLsReady!: () => void; +const mdLsReady = new Promise(resolve => { resolveMdLsReady = resolve; }); + connection.onInitialize((params: InitializeParams) => { // Set log level from initialization options if provided so that we use the // expected level as soon as possible @@ -131,6 +140,11 @@ connection.onInitialize((params: InitializeParams) => { connection.onDocumentSymbol(async (params, token): Promise => { logger.logRequest('documentSymbol'); + await mdLsReady; + if (token.isCancellationRequested) { + return []; + } + const document = documents.get(params.textDocument.uri); if (!document) { return []; @@ -141,6 +155,11 @@ connection.onInitialize((params: InitializeParams) => { connection.onFoldingRanges(async (params, token): Promise => { logger.logRequest('foldingRanges'); + await mdLsReady; + if (token.isCancellationRequested) { + return []; + } + const document = documents.get(params.textDocument.uri); if (!document) { return []; @@ -198,7 +217,6 @@ connection.onInitialize((params: InitializeParams) => { hoverProvider: true, definitionProvider: true, documentLinkProvider: { resolveProvider: true }, - documentSymbolProvider: true, foldingRangeProvider: true, referencesProvider: true, selectionRangeProvider: true, @@ -291,6 +309,29 @@ connection.onInitialized(async () => { // register custom methods registerCustomMethods(quarto, lspConnection, documents); + + // dynamically register the document symbol provider now that `mdLs` exists + // so the client only learns about the capability once we can actually serve + // it (avoids the cold-start race where the client requests symbols, caches + // an empty response, and never re-queries on its own). Re-register on every + // config change so the client re-queries symbols with the new shape; the + // VS Code extension restores outline expansion state after the re-query. + let documentSymbolRegistration: Disposable | undefined; + const registerDocumentSymbolProvider = async () => { + documentSymbolRegistration?.dispose(); + documentSymbolRegistration = await connection.client.register( + DocumentSymbolRequest.type, + { documentSelector: null } + ); + }; + await registerDocumentSymbolProvider(); + configManager.onDidChangeConfiguration(() => { + registerDocumentSymbolProvider(); + }); + + // signal that `mdLs` is now ready to serve requests: + // handlers like document symbols, folding ranges, etc will now proceed + resolveMdLsReady(); }); diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 0e429047f..e18948824 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -121,7 +121,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { + // brief delay so the re-query and tree rebuild settle before expanding + await new Promise(resolve => setTimeout(resolve, 200)); + await vscode.commands.executeCommand("outline.expand"); + }; + context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(async (event) => { if (event.affectsConfiguration("quarto.symbols.showCodeCellsInOutline")) { - // This edit triggers VS Code to re-request document symbols from the LSP, - // which will then use the updated configuration value. - // The undoStopBefore/After: false options prevent these edits from creating undo stops, - // minimizing their impact on the undo history. - // See: https://code.visualstudio.com/api/references/vscode-api#TextEditorEdit - for (const editor of vscode.window.visibleTextEditors) { - if (editor.document.languageId === "quarto") { - await editor.edit( - edit => edit.insert(new vscode.Position(0, 0), " "), - { undoStopBefore: false, undoStopAfter: false } - ); - await editor.edit( - edit => edit.delete(new vscode.Range(0, 0, 0, 1)), - { undoStopBefore: false, undoStopAfter: false } - ); - } + if (vscode.window.activeTextEditor?.document.languageId === "quarto") { + await expandOutline(); + } else { + expandPending = true; } } }) ); + + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(async (editor) => { + if (expandPending && editor?.document.languageId === "quarto") { + expandPending = false; + await expandOutline(); + } + }) + ); } export async function deactivate() { diff --git a/apps/vscode/src/providers/symbols.ts b/apps/vscode/src/providers/symbols.ts index 68f21e80e..1270bf11d 100644 --- a/apps/vscode/src/providers/symbols.ts +++ b/apps/vscode/src/providers/symbols.ts @@ -24,7 +24,8 @@ class ToggleCodeCellsInOutlineCommand implements Command { const currentValue = config.get("symbols.showCodeCellsInOutline", true); const newValue = !currentValue; - // Update the configuration - the `registerOutlineConfigListener`config listener handles outline refresh + // The LSP re-registers its document symbol provider on config change, which triggers VS Code to re-query the outline. + // The VS Code extension restores outline expansion state after the re-query (see `registerOutlineConfigListener`). await config.update("symbols.showCodeCellsInOutline", newValue, vscode.ConfigurationTarget.Global); vscode.window.showInformationMessage( From dc5b7cb95683f2bdf2dae21700f6e96f3a5a89f0 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Tue, 19 May 2026 19:42:05 -0600 Subject: [PATCH 7/7] Add new tests for showing/hiding code cells in outline --- apps/vscode/src/test/symbols.test.ts | 97 ++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/apps/vscode/src/test/symbols.test.ts b/apps/vscode/src/test/symbols.test.ts index cc079b049..f5ee1834b 100644 --- a/apps/vscode/src/test/symbols.test.ts +++ b/apps/vscode/src/test/symbols.test.ts @@ -1,5 +1,7 @@ +import * as path from "path"; import * as vscode from "vscode"; import * as assert from "assert"; +import { WORKSPACE_PATH } from "./test-utils"; suite("Workspace Symbols", function () { teardown(async function () { @@ -53,3 +55,98 @@ suite("Workspace Symbols", function () { assert.ok(!symbols.find((s) => s.name === "Regular-Project Header 2")); }); }); + +suite("Document Symbols", function () { + const basicsUri = vscode.Uri.file(path.join(WORKSPACE_PATH, "format", "basics.qmd")); + + teardown(async function () { + await vscode.workspace + .getConfiguration("quarto") + .update("symbols.showCodeCellsInOutline", undefined); + }); + + test("includes code cells when showCodeCellsInOutline is true", async function () { + await vscode.workspace + .getConfiguration("quarto") + .update("symbols.showCodeCellsInOutline", true); + + // give the LSP time to re-register its symbol provider after the config change + await new Promise(r => setTimeout(r, 500)); + + await vscode.workspace.openTextDocument(basicsUri); + const symbols = await vscode.commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + basicsUri + ); + + const names = flattenSymbolNames(symbols); + assert.ok( + names.includes("(code cell)"), + `expected a code cell in symbols, got: ${names.join(", ")}` + ); + }); + + test("excludes code cells when showCodeCellsInOutline is false", async function () { + await vscode.workspace + .getConfiguration("quarto") + .update("symbols.showCodeCellsInOutline", false); + + await new Promise(r => setTimeout(r, 500)); + + await vscode.workspace.openTextDocument(basicsUri); + const symbols = await vscode.commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + basicsUri + ); + + const names = flattenSymbolNames(symbols); + assert.ok( + !names.includes("(code cell)"), + `expected no code cells in symbols, got: ${names.join(", ")}` + ); + }); + + test("toggling showCodeCellsInOutline live updates symbols", async function () { + await vscode.workspace.openTextDocument(basicsUri); + + await vscode.workspace + .getConfiguration("quarto") + .update("symbols.showCodeCellsInOutline", true); + await new Promise(r => setTimeout(r, 500)); + + let symbols = await vscode.commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + basicsUri + ); + assert.ok( + flattenSymbolNames(symbols).includes("(code cell)"), + "expected code cells to appear after setting true" + ); + + await vscode.workspace + .getConfiguration("quarto") + .update("symbols.showCodeCellsInOutline", false); + await new Promise(r => setTimeout(r, 500)); + + symbols = await vscode.commands.executeCommand( + "vscode.executeDocumentSymbolProvider", + basicsUri + ); + assert.ok( + !flattenSymbolNames(symbols).includes("(code cell)"), + "expected code cells to disappear after setting false" + ); + }); +}); + +function flattenSymbolNames(symbols: vscode.DocumentSymbol[]): string[] { + const result: string[] = []; + const walk = (syms: vscode.DocumentSymbol[]) => { + for (const sym of syms) { + result.push(sym.name); + if (sym.children?.length) walk(sym.children); + } + }; + walk(symbols); + return result; +}