-
Notifications
You must be signed in to change notification settings - Fork 62
In Positron, use virtual notebook (in memory) for LSP features instead of vdoc (on disk) #1115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
be7d747
First draft of virtual notebook for LSP features
juliasilge bbec2bb
Add some logging for vdocs, to make it easier to find them
juliasilge 4bf8f9c
Merged origin/main into feat/positron-native-embedded-features
juliasilge b37b319
Update CHANGELOG
juliasilge 62598c7
Extract native feature detection logic into separate function
juliasilge 6d8b511
Gate semantic tokens per document instead of per cursor language
juliasilge 98c3988
Skip the cell symbol request when the outline has no chunk symbols
juliasilge ef8e717
Update apps/vscode/src/lsp/client.ts
juliasilge 36d93ba
Merge branch 'main' into feat/positron-native-embedded-features
juliasilge dd52fdc
Update vocabulary to "host ownership"
juliasilge bbb46cf
Add some documentation for cell features module
juliasilge File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| /* | ||
| * cell-features.ts | ||
| * | ||
| * Copyright (C) 2026 by Posit Software, PBC | ||
| */ | ||
|
|
||
| /** | ||
| * Who answers language feature requests inside Quarto code cells: this | ||
| * extension, from a `.vdoc.*` temp file, or the host, from an in memory | ||
| * virtual notebook. Both answers reach the editor for one request, so only one | ||
| * can answer it. | ||
| * | ||
| * This module is the entry point for that decision, and only the decision. The | ||
| * features stay in their own providers. Each one asks here, then stands down or | ||
| * carries on. | ||
| * | ||
| * Gated per language, so a cell the host does not cover keeps its virtual | ||
| * document: | ||
| * | ||
| * - completion, hover, signature help, go to definition (`lsp/client.ts`) | ||
| * - diagnostics (`providers/diagnostics.ts`) | ||
| * | ||
| * Gated per document, because one request covers the whole file and only one | ||
| * answer survives. Any cell the host owns hands it every cell, including ones | ||
| * in languages the host does not cover: | ||
| * | ||
| * - document symbols (`lsp/client.ts`) | ||
| * - document and range formatting (`providers/format.ts`) | ||
| * - semantic tokens (`providers/semantic-tokens.ts`) | ||
| * - statement range and help topic (`lsp/client.ts`), which are registrations, | ||
| * so they are disposed and registered again rather than returning early | ||
| * | ||
| * Not gated: `quarto.formatCell` still uses a virtual document. | ||
| * | ||
| * The host owns a feature when it has the cell commands (probed at activation | ||
| * by {@link detectCellFeatureOwnership}), the setting is on, and, for a | ||
| * per-language gate, the language is one of `kHostOwnedLanguages`. | ||
| */ | ||
|
|
||
| import { commands, LogOutputChannel, workspace } from "vscode"; | ||
| import { tryAcquirePositronApi } from "@posit-dev/positron"; | ||
|
|
||
| import { EmbeddedLanguage } from "../vdoc/languages"; | ||
|
|
||
| /** | ||
| * The Positron setting that turns the virtual notebook on. Contributed by | ||
| * Positron core, not by this extension, so it is read through the full | ||
| * configuration rather than the `quarto` section we contribute. | ||
| */ | ||
| export const kHostCellFeaturesSetting = "quarto.embeddedLanguageFeatures.native"; | ||
|
|
||
| /** | ||
| * Commands whose presence says this host carries the virtual notebook (see | ||
| * {@link detectCellFeatureOwnership}). | ||
| * | ||
| * These are the INTERNAL ids, and the code calls the public | ||
| * `positron.executeQuartoCell*` ones. The internal ids are what a probe can | ||
| * see. The public ones are API commands, registered inside the extension host | ||
| * and deliberately never mirrored into the registry that `getCommands` reads, | ||
| * so they do not appear there at all. Neither does any | ||
| * `vscode.executeDocumentSymbolProvider`-style command, for the same reason. | ||
| * The internal commands are registered in the workbench, so they are visible, | ||
| * as long as the probe does not filter underscore-prefixed ids. | ||
| */ | ||
| const kCellOwnershipCommands = [ | ||
| "_executeQuartoCellSymbolProvider", | ||
| "_executeQuartoCellFormattingProvider", | ||
| "_executeQuartoCellRangeFormattingProvider", | ||
| ]; | ||
|
|
||
| /** | ||
| * Languages whose cells the host is verified to own. Matched against | ||
| * {@link EmbeddedLanguage.ids}, so an alias of a listed language counts too. | ||
| * | ||
| * Add one language at a time, once its cell providers have been verified end to | ||
| * end: a document that is not covered here keeps its virtual document, which is | ||
| * the safe direction. | ||
| */ | ||
| const kHostOwnedLanguages = new Set(["r", "python"]); | ||
|
|
||
| let cellCommandsAvailable = false; | ||
|
|
||
| /** | ||
| * Whether this host carries the virtual notebook. | ||
| * | ||
| * Capability detection is command presence rather than a Positron API flag or a | ||
| * version comparison. Positron registers these commands unconditionally: with | ||
| * the setting off there are no cells and they answer empty, so their presence | ||
| * tracks "this build can own the cells" exactly. Vanilla VS Code and older | ||
| * Positron builds have no such commands, so a user who pastes the setting key | ||
| * into their own `settings.json` there stays on virtual documents. | ||
| */ | ||
| async function hostHasCellCommands(): Promise<boolean> { | ||
| if (!tryAcquirePositronApi()) { | ||
| return false; | ||
| } | ||
|
|
||
| // `false` keeps the underscore-prefixed ids we are looking for | ||
| const all = await commands.getCommands(false); | ||
| return kCellOwnershipCommands.every((command) => all.includes(command)); | ||
| } | ||
|
|
||
| /** | ||
| * Determine whether the host owns language features for code cells, and | ||
| * record the answer where the gates can read it. | ||
| * | ||
| * Must be awaited during activation, before any gate can be consulted. | ||
| */ | ||
| export async function detectCellFeatureOwnership( | ||
| outputChannel?: LogOutputChannel | ||
| ): Promise<void> { | ||
| cellCommandsAvailable = await hostHasCellCommands(); | ||
|
|
||
| if (cellCommandsAvailable) { | ||
| outputChannel?.info( | ||
| "[CellFeatures] Host owns Quarto cell language features. " + | ||
| `The extension stands down for ${[...kHostOwnedLanguages].join(", ")} ` + | ||
| `while ${kHostCellFeaturesSetting} is on.` | ||
| ); | ||
| } else if ( | ||
| workspace.getConfiguration().get<boolean>(kHostCellFeaturesSetting) === true | ||
| ) { | ||
| outputChannel?.warn( | ||
| `[CellFeatures] ${kHostCellFeaturesSetting} is on, but this host has no ` + | ||
| "Quarto cell commands. Serving embedded language features from virtual " + | ||
| "documents, which can duplicate what the host provides." | ||
| ); | ||
|
Comment on lines
+115
to
+127
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice logs. These were very helpful in testing. |
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Whether the host owns the cells of a language. Pure, so the language set | ||
| * can be tested without an extension host. | ||
| */ | ||
| export function hostOwnsLanguage(language: EmbeddedLanguage): boolean { | ||
| return language.ids.some((id) => kHostOwnedLanguages.has(id)); | ||
| } | ||
|
|
||
| /** | ||
| * Whether the host owns embedded language features for `language`, meaning | ||
| * this extension should stand down and not serve them from a virtual document. | ||
| * | ||
| * Pass no language to ask about the document as a whole, which is what the | ||
| * whole-document commands (symbols, formatting) cover. | ||
| * | ||
| * The setting is read live on every call so that toggling it takes effect | ||
| * without a window reload. The statement range and help topic registrations in | ||
| * `lsp/client.ts` follow the setting live too, via a configuration listener. | ||
| * | ||
| * A gated pull feature answers `undefined` rather than delegating to the Quarto | ||
| * language server with `next()`. The server has nothing real to say about a code | ||
| * cell: it declares the signature help, definition, and semantic tokens | ||
| * capabilities only so that the client can intercept them with middleware, and | ||
| * its handlers answer null (see `apps/lsp/src/middleware.ts`). For semantic | ||
| * tokens delegating is worse than pointless, because the server's empty token | ||
| * stream counts as an answer and would suppress the host's own provider. | ||
| */ | ||
| export function hostOwnsCellFeatures(language?: EmbeddedLanguage): boolean { | ||
| if (!cellCommandsAvailable) { | ||
| return false; | ||
| } | ||
| if (workspace.getConfiguration().get<boolean>(kHostCellFeaturesSetting) !== true) { | ||
| return false; | ||
| } | ||
| return language === undefined || hostOwnsLanguage(language); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| /* | ||
| * cell-symbols.ts | ||
| * | ||
| * Copyright (C) 2026 by Posit Software, PBC | ||
| */ | ||
|
|
||
| import { | ||
| commands, | ||
| DocumentSymbol, | ||
| Range, | ||
| SymbolKind, | ||
| Uri, | ||
| } from "vscode"; | ||
|
|
||
| /** | ||
| * One code cell's symbols, as answered by | ||
| * `positron.executeQuartoCellSymbolProvider`. | ||
| */ | ||
| export interface QuartoCellSymbols { | ||
| /** The cell's code span in source coordinates, fences excluded. */ | ||
| readonly range: Range; | ||
|
|
||
| /** Already in source coordinates. Never empty. */ | ||
| readonly symbols: DocumentSymbol[]; | ||
| } | ||
|
|
||
| /** | ||
| * The symbols of every code cell in a Quarto document, grouped by cell. | ||
| * | ||
| * One request for the whole document, so callers walking a symbol tree should | ||
| * ask once and then look cells up by range with {@link nestCellSymbols}. | ||
| * | ||
| * Answers `[]` for every unservable state: a host without the command, a | ||
| * document with no cells, and a document whose cells have no language server | ||
| * attached yet. That last case is why the caller must gate on | ||
| * `hostOwnsCellFeatures()` rather than treat an empty answer as a reason to | ||
| * fall back, and it needs no retry: when a server does register, the editor | ||
| * re-requests document symbols on its own. | ||
| */ | ||
| export async function quartoCellSymbols( | ||
| uri: Uri | ||
| ): Promise<QuartoCellSymbols[]> { | ||
| try { | ||
| const cells = await commands.executeCommand<QuartoCellSymbols[] | undefined>( | ||
| "positron.executeQuartoCellSymbolProvider", | ||
| uri | ||
| ); | ||
| return cells ?? []; | ||
| } catch (error) { | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Whether a symbol tree holds any chunk symbol for a cell to nest under. | ||
| * | ||
| * The language server marks chunks with `SymbolKind.Function` (its `toc.ts`), | ||
| * and drops every one of them when `quarto.symbols.showCodeCellsInOutline` is | ||
| * off. A `_quarto.yml`, which this client's document selector also covers, never | ||
| * has one either. In both cases {@link nestCellSymbols} would have nothing to | ||
| * attach to, so the caller can answer without asking the host for cell symbols. | ||
| */ | ||
| export function hasChunkSymbols(symbols: readonly DocumentSymbol[]): boolean { | ||
| return symbols.some( | ||
| (symbol) => | ||
| symbol.kind === SymbolKind.Function || hasChunkSymbols(symbol.children) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Nests each cell's symbols under the chunk symbol it came from. | ||
| * | ||
| * Chunks are matched to cells by range containment: a chunk symbol's range | ||
| * covers its fences, so the cell's code span sits inside it. Chunks are the | ||
| * `SymbolKind.Function` symbols the Quarto language server's `toc.ts` emits, | ||
| * which is the same marker the virtual document path uses. | ||
| * | ||
| * Symbols the language server already nested under a chunk are kept, and a | ||
| * chunk with no matching cell is left as it is. | ||
| */ | ||
| export function nestCellSymbols( | ||
| symbols: DocumentSymbol[], | ||
| cells: readonly QuartoCellSymbols[] | ||
| ): DocumentSymbol[] { | ||
| for (const symbol of symbols) { | ||
| if (symbol.kind === SymbolKind.Function) { | ||
| const cell = cells.find((candidate) => | ||
| symbol.range.contains(candidate.range) | ||
| ); | ||
| if (cell) { | ||
| symbol.children = [...symbol.children, ...cell.symbols]; | ||
| } | ||
| } else { | ||
| symbol.children = nestCellSymbols(symbol.children, cells); | ||
| } | ||
| } | ||
|
|
||
| return symbols; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.