diff --git a/apps/vscode/src/@types/hooks.d.ts b/apps/vscode/src/@types/hooks.d.ts index 32bf8a12..c8b0c2b7 100644 --- a/apps/vscode/src/@types/hooks.d.ts +++ b/apps/vscode/src/@types/hooks.d.ts @@ -10,6 +10,7 @@ declare module 'positron' { runtime: PositronRuntime; languages: PositronLanguages; window: PositronWindow; + StatementRangeSyntaxError: typeof StatementRangeSyntaxError; } export interface PositronRuntime { @@ -58,6 +59,11 @@ declare module 'positron' { readonly code?: string; } + export class StatementRangeSyntaxError extends Error { + readonly line?: number; + constructor(line?: number); + } + export interface PositronWindow { createPreviewPanel( viewType: string, diff --git a/apps/vscode/src/host/hooks.ts b/apps/vscode/src/host/hooks.ts index afcd8b34..bd16a8bd 100644 --- a/apps/vscode/src/host/hooks.ts +++ b/apps/vscode/src/host/hooks.ts @@ -18,11 +18,12 @@ import * as vscode from 'vscode'; import * as hooks from 'positron'; +import semver from "semver"; import { ExtensionHost, HostWebviewPanel, HostStatementRangeProvider, HostHelpTopicProvider } from '.'; import { CellExecutor, cellExecutorForLanguage, executableLanguages, isKnitrDocument, pythonWithReticulate } from './executors'; import { ExecuteQueue } from './execute-queue'; import { MarkdownEngine } from '../markdown/engine'; -import { virtualDoc, adjustedPosition, unadjustedRange, withVirtualDocUri, VirtualDocStyle } from "../vdoc/vdoc"; +import { virtualDoc, adjustedPosition, unadjustedRange, withVirtualDocUri, VirtualDocStyle, unadjustedLine } from "../vdoc/vdoc"; import { Position, Range } from 'vscode'; import { Uri } from 'vscode'; @@ -200,18 +201,45 @@ class EmbeddedStatementRangeProvider implements HostStatementRangeProvider { position: vscode.Position, token: vscode.CancellationToken): Promise { const vdoc = await virtualDoc(document, position, this._engine, VirtualDocStyle.Block); - if (vdoc) { - return await withVirtualDocUri(vdoc, document.uri, "statementRange", async (uri: vscode.Uri) => { + + if (!vdoc) { + return undefined; + } + + return await withVirtualDocUri(vdoc, document.uri, "statementRange", async (uri: vscode.Uri) => { + try { const result = await vscode.commands.executeCommand( "vscode.executeStatementRangeProvider", uri, adjustedPosition(vdoc.language, position) ); return { range: unadjustedRange(vdoc.language, result.range), code: result.code }; - }); - } else { - return undefined; - } + } catch (err) { + let hooks = hooksApi(); + + if (!hooks) { + throw err; + } + + // TODO: Remove this once `apps/vscode/package.json` bumps to `"positron": "^2026.03.0"` or higher. + // For now we avoid aggressive bumping due to https://github.com/posit-dev/positron/issues/11321. + // We can't use `semver.lt()` because calendar versioning isn't compatible with semver due to the + // leading `0` in `03`. Instead, we use lexicographic string comparison and rely on the year and + // month to be zero padded so sorting always works correctly. + if (hooks.version < "2026.03.0") { + throw err; + } + + if (err instanceof hooks.StatementRangeSyntaxError) { + // Rethrow syntax error with unadjusted line number, so Positron's notification will + // jump to the correct line + throw new hooks.StatementRangeSyntaxError(err.line ? unadjustedLine(vdoc.language, err.line) : undefined); + } else { + // Rethrow unrecognized error + throw err; + } + } + }); }; } diff --git a/apps/vscode/src/vdoc/vdoc.ts b/apps/vscode/src/vdoc/vdoc.ts index 29a8c178..c17720e4 100644 --- a/apps/vscode/src/vdoc/vdoc.ts +++ b/apps/vscode/src/vdoc/vdoc.ts @@ -240,13 +240,21 @@ export function isBlockOfLanguage(language: EmbeddedLanguage) { }; } -// adjust position for inject +// adjust line for inject +export function adjustedLine(language: EmbeddedLanguage, line: number): number { + return line + (language.inject?.length || 0); +} + +export function unadjustedLine(language: EmbeddedLanguage, line: number): number { + return line - (language.inject?.length || 0); +} + export function adjustedPosition(language: EmbeddedLanguage, pos: Position) { - return new Position(pos.line + (language.inject?.length || 0), pos.character); + return new Position(adjustedLine(language, pos.line), pos.character); } export function unadjustedPosition(language: EmbeddedLanguage, pos: Position) { - return new Position(pos.line - (language.inject?.length || 0), pos.character); + return new Position(unadjustedLine(language, pos.line), pos.character); } export function unadjustedRange(language: EmbeddedLanguage, range: Range) {