Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/lsp/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface Settings {
};
readonly symbols: {
readonly exportToWorkspace: 'default' | 'all' | 'none';
readonly showCodeCellsInOutline: boolean;
};
};
readonly markdown: {
Expand Down Expand Up @@ -96,7 +97,8 @@ function defaultSettings(): Settings {
extensions: []
},
symbols: {
exportToWorkspace: 'all'
exportToWorkspace: 'all',
showCodeCellsInOutline: true
}
},
markdown: {
Expand Down Expand Up @@ -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
}
}
};
Expand Down Expand Up @@ -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;
}
};
}
Expand Down
43 changes: 42 additions & 1 deletion apps/lsp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import path from "path";
import {
ClientCapabilities,
Definition,
Disposable,
DocumentLink,
DocumentSymbol,
DocumentSymbolRequest,
FoldingRange,
InitializeParams,
ProposedFeatures,
Expand Down Expand Up @@ -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<void>(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
Expand Down Expand Up @@ -131,6 +140,11 @@ connection.onInitialize((params: InitializeParams) => {
connection.onDocumentSymbol(async (params, token): Promise<DocumentSymbol[]> => {
logger.logRequest('documentSymbol');

await mdLsReady;
if (token.isCancellationRequested) {
return [];
}

const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
Expand All @@ -141,6 +155,11 @@ connection.onInitialize((params: InitializeParams) => {
connection.onFoldingRanges(async (params, token): Promise<FoldingRange[]> => {
logger.logRequest('foldingRanges');

await mdLsReady;
if (token.isCancellationRequested) {
return [];
}

const document = documents.get(params.textDocument.uri);
if (!document) {
return [];
Expand Down Expand Up @@ -198,7 +217,6 @@ connection.onInitialize((params: InitializeParams) => {
hoverProvider: true,
definitionProvider: true,
documentLinkProvider: { resolveProvider: true },
documentSymbolProvider: true,
foldingRangeProvider: true,
referencesProvider: true,
selectionRangeProvider: true,
Expand Down Expand Up @@ -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();
});


Expand Down
4 changes: 3 additions & 1 deletion apps/lsp/src/service/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -111,7 +112,8 @@ const defaultConfig: LsConfiguration = {
colorTheme: 'light',
mathjaxScale: 1,
mathjaxExtensions: [],
exportSymbolsToWorkspace: 'all'
exportSymbolsToWorkspace: 'all',
showCodeCellsInOutline: true
};

export function defaultLsConfiguration(): LsConfiguration {
Expand Down
2 changes: 1 addition & 1 deletion apps/lsp/src/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
22 changes: 20 additions & 2 deletions apps/lsp/src/service/providers/document-symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
21 changes: 21 additions & 0 deletions apps/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions apps/vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 1.133.0

- Add setting and command to show/hide cells in outline (<https://github.com/quarto-dev/quarto/pull/974>)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Add setting and command to show/hide cells in outline (<https://github.com/quarto-dev/quarto/pull/974>)
- Added setting and command to show/hide cells in outline (<https://github.com/quarto-dev/quarto/pull/974>)

For consistency with the rest of this file


## 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 (<https://github.com/quarto-dev/quarto/pull/906>).
Expand Down
10 changes: 10 additions & 0 deletions apps/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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."
}
}
},
Expand Down
50 changes: 50 additions & 0 deletions apps/vscode/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -120,6 +121,9 @@ export async function activate(context: vscode.ExtensionContext): Promise<Quarto
// lsp
const lspClient = await activateLsp(context, quartoContext, engine, outputChannel);

// restore outline expansion after the LSP re-registers symbols on config change
registerOutlineConfigListener(context);

// provide visual editor
const editorCommands = activateEditor(context, host, quartoContext, lspClient, engine);
commands.push(...editorCommands);
Expand Down Expand Up @@ -157,6 +161,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<Quarto

commands.push(...insertCommands(engine));

commands.push(...symbolsCommands());

commands.push(...activateDiagram(context, host, engine));

commands.push(...activateCodeFormatting(engine));
Expand Down Expand Up @@ -278,6 +284,50 @@ function registerQuartoPathConfigListener(context: vscode.ExtensionContext, outp
);
}

/**
* Restore outline expansion state after settings that affect symbol output change.
*
* The LSP re-registers its document symbol provider whenever the relevant
* settings change, which forces VS Code to re-query and refresh the outline.
* That re-query rebuilds the tree from scratch, so VS Code's heuristic for
* symbols with newly-appearing children defaults them to collapsed (e.g.
* toggling on code cells leaves their parent headers collapsed).
*
* We expand the outline once a Quarto editor is active: immediately if the
* user already has one focused (e.g. they ran the toggle command), or on the
* next switch back if the setting was changed from the Settings UI.
*/
function registerOutlineConfigListener(context: vscode.ExtensionContext) {
let expandPending = false;

const expandOutline = async () => {
// 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")) {
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() {
return deactivateLsp();
}
39 changes: 39 additions & 0 deletions apps/vscode/src/providers/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* symbols.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.
*
*/

import * as vscode from "vscode";
import { Command } from "../core/command";

class ToggleCodeCellsInOutlineCommand implements Command {
public readonly id = "quarto.toggleCodeCellsInOutline";

public async execute() {
const config = vscode.workspace.getConfiguration("quarto");
const currentValue = config.get<boolean>("symbols.showCodeCellsInOutline", true);
const newValue = !currentValue;

// 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(
`Code cells in outline will now be ${newValue ? "shown" : "hidden"}.`
);
}
}

export function symbolsCommands(): Command[] {
return [new ToggleCodeCellsInOutlineCommand()];
}
Loading
Loading