Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions apps/vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 1.133.0 (Unreleased)

- Add code symbols into outline (<https://github.com/quarto-dev/quarto/pull/972>).
- Added setting and command to show/hide cells in outline (<https://github.com/quarto-dev/quarto/pull/974>).
- Added custom pair colorization and highlighting for divs in qmds (<https://github.com/quarto-dev/quarto/pull/973>).

Expand Down
124 changes: 123 additions & 1 deletion apps/vscode/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ import {
Uri,
Diagnostic,
window,
ColorThemeKind
ColorThemeKind,
DocumentSymbol,
Range,
SymbolKind,
} from "vscode";
import {
LanguageClient,
Expand All @@ -48,6 +51,7 @@ import {
ProvideDefinitionSignature,
ProvideHoverSignature,
ProvideSignatureHelpSignature,
ProvideDocumentSymbolsSignature,
State,
HandleDiagnosticsSignature
} from "vscode-languageclient";
Expand All @@ -57,6 +61,7 @@ import {
unadjustedRange,
virtualDoc,
withVirtualDocUri,
VirtualDocStyle,
} from "../vdoc/vdoc";
import { isVirtualDoc } from "../vdoc/vdoc-tempfile";
import { activateVirtualDocEmbeddedContent } from "../vdoc/vdoc-content";
Expand All @@ -72,6 +77,8 @@ import { imageHover } from "../providers/hover-image";
import { LspInitializationOptions, QuartoContext } from "quarto-core";
import { extensionHost } from "../host";
import semver from "semver";
import { EmbeddedLanguage } from "../vdoc/languages";
import { SymbolInformation } from "vscode";

let client: LanguageClient;

Expand Down Expand Up @@ -113,6 +120,7 @@ export async function activateLsp(
engine
),
provideDocumentSemanticTokens: embeddedSemanticTokensProvider(engine),
provideDocumentSymbols: embeddedDocumentSymbolProvider(engine),
};
if (config.get("cells.hoverHelp.enabled", true)) {
middleware.provideHover = embeddedHoverProvider(engine);
Expand Down Expand Up @@ -364,6 +372,120 @@ function isWithinYamlComment(doc: TextDocument, pos: Position) {
return !!line.match(/^\s*#\s*\| /);
}

const isDocumentSymbol = (a: Object): a is DocumentSymbol => {
return ('range' in a && 'selectionRange' in a);
};

/**
* Enhances document symbols by adding code symbols from embedded languages to code cells
*/
function embeddedDocumentSymbolProvider(engine: MarkdownEngine) {
return async (
document: TextDocument,
token: CancellationToken,
next: ProvideDocumentSymbolsSignature
): Promise<DocumentSymbol[] | SymbolInformation[] | undefined> => {
// Get base symbols from LSP (headers, code cells, etc.)
const baseSymbols = await next(document, token);

if (!baseSymbols || token.isCancellationRequested) {
return baseSymbols ?? undefined;
}

// Check if we got DocumentSymbol[] (can be enhanced) or SymbolInformation[] (cannot)
// I don't think we actually ever get SymbolInformation[] here, but I'm not certain
// so this is defensively coded.
if (baseSymbols.length > 0 && isDocumentSymbol(baseSymbols[0])) {
return await enhanceSymbolsWithCodeCellContent(document, baseSymbols as DocumentSymbol[], engine, token);
}

return baseSymbols;
};
}

/**
* Finds code cell symbols, makes vdocs for them, gets symbols from the vdoc, and nests those symbols
* under the code cell's symbol.
*/
async function enhanceSymbolsWithCodeCellContent(
document: TextDocument,
symbols: DocumentSymbol[],
engine: MarkdownEngine,
token: CancellationToken
): Promise<DocumentSymbol[]> {
const enhanced: DocumentSymbol[] = [];

for (const symbol of symbols) {
if (token.isCancellationRequested) return symbols;

// Check if this is a code cell symbol (SymbolKind.Function indicates code cells from toc.ts)
if (symbol.kind === SymbolKind.Function) {
symbol.children = [
...symbol.children,
...(await getCodeCellSymbols(document, symbol.range, engine) || [])
];
} else {
symbol.children =
await enhanceSymbolsWithCodeCellContent(document, symbol.children, engine, token);
}

enhanced.push(symbol);
}

return enhanced;
}

/**
* Gets symbols from an embedded language for a code cell
*/
async function getCodeCellSymbols(
document: TextDocument,
cellRange: Range,
engine: MarkdownEngine
): Promise<DocumentSymbol[] | undefined> {
try {
// Get position at the start of the code cell (skip the fence line)
const position = new Position(cellRange.start.line + 1, 0);

// Create virtual document for ONLY this code block (not all blocks of the language)
const vdoc = await virtualDoc(document, position, engine, VirtualDocStyle.Block);
if (!vdoc) return undefined;

// Get symbols from the embedded language server
return await withVirtualDocUri(vdoc, document.uri, "completion", async (uri: Uri) => {
try {
const result = await commands.executeCommand<DocumentSymbol[] | SymbolInformation[]>(
"vscode.executeDocumentSymbolProvider",
uri
);
if (result.length === 0) return undefined;
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.

The result here could also be nothing at all (undefined probably?) so it might be nicer to handle that in this if so it doesn't show up as an error in that catch().

Copy link
Copy Markdown
Member Author

@vezwork vezwork May 22, 2026

Choose a reason for hiding this comment

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

How do you know it could be nothing at all? The https://code.visualstudio.com/api/references/commands seems to say that it will always resolve to a list (although it is slightly ambiguous if the type of the list is (SymbolInformation | DocumentSymbol)[] or if its SymbolInformation[] | DocumentSymbol[]). Google search summary says it resolves to SymbolInformation[] | DocumentSymbol[] but with no good citation. Do you know a way to figure out the return type of a command? I attempted to find the definition of the command in the vscode repo, but couldn't.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

In my testing I have now come across it being undefined!


if (isDocumentSymbol(result[0])) {
return unadjustSymbolRanges(result as DocumentSymbol[], vdoc.language, cellRange.start.line);
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.

Is it possible to also handle results that come back as SymbolInformation[], as indicated in line 457?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes, committed a change for this.

}
} catch (error) { }
});
} catch (error) { }
}

/**
* Adjusts symbol ranges from virtual document to real document coordinates
*/
function unadjustSymbolRanges(
symbols: DocumentSymbol[],
language: EmbeddedLanguage,
baseLineOffset: number
): DocumentSymbol[] {
return symbols.map(symbol => {
return {
...symbol,
range: unadjustedRange(language, symbol.range),
selectionRange: unadjustedRange(language, symbol.selectionRange),
children: symbol.children ? unadjustSymbolRanges(symbol.children, language, baseLineOffset) : []
};
});
}

/**
* Creates a diagnostic handler middleware that filters out diagnostics from virtual documents
*
Expand Down
Loading