Skip to content

Commit 36ddb85

Browse files
authored
in VE use Positron's statementRangeProvider to execute statements (#867)
* use statementRangeProvider in Positron to execute statements at cursor in VE * WIP fix advancing to next statement I don't think my changes to `codeViewSetBlockSelection` work. It seems that `navigateToPos` does not work inside codeMirror? That must be why only "nextline" works in the command - it doesn't use `navigateToPos` it uses a special codeView thing. * fix advancing to next statement * WIP move to next statement * Working Positron VE statement range execution in r and python * Add comment clarifying `codeViewExecute` * Add `isPositron`, semicolons * Fake code to call `positronConsole.executeCode` with uri, position returning position * Rename `executeSelectionAtPosition` to `executeAtPosition` * modify command call to match @juliasilge's posit-dev/positron#10580 * Working VE statement execution with navigation to next statement * bump required Positron version * Add changelog entry
1 parent 591b352 commit 36ddb85

File tree

12 files changed

+156
-61
lines changed

12 files changed

+156
-61
lines changed

apps/vscode/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
- By default, headers from markdown files _in R projects_ (projects with a `DESCRIPTION` file such as R package) are no longer exported as workspace symbols. They remain exported as usual in other projects. This behaviour can be controlled manually with the new `quarto.symbols.exportToWorkspace` setting.
66
- Added a new setting `quarto.useBundledQuartoInPositron` to prefer the Quarto CLI bundled with Positron when available. This setting has precedence _between_ `quarto.path` and `quarto.usePipQuarto`, and has no effect outside of Positron (<https://github.com/quarto-dev/quarto/pull/841>).
7-
- Visual Editor: uses a text box for alternative text and captions in callouts, images, and tables interface. (<https://github.com/quarto-dev/quarto/pull/644>)
7+
- Visual Editor: uses a text box for alternative text and captions in callouts, images, and tables interface. (<https://github.com/quarto-dev/quarto/pull/644>).
88
- Fixed a bug where previewing showed "Not Found" on Quarto files with spaces in the name in subfolders of projects (<https://github.com/quarto-dev/quarto/pull/853>).
99
- Added support for semantic highlighting in Quarto documents, when using an LSP that supports it (for example, Pylance) (<https://github.com/quarto-dev/quarto/pull/868>).
10+
- Visual Editor: in Positron, add support for statement execution (<https://github.com/quarto-dev/quarto/pull/867/>).
1011

1112
## 1.126.0 (Release on 2025-10-08)
1213

apps/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"private": true,
3030
"engines": {
3131
"vscode": "^1.75.0",
32-
"positron": "^2025.6.0"
32+
"positron": "^2025.12.0"
3333
},
3434
"main": "./out/main.js",
3535
"browser": "./browser.js",

apps/vscode/src/host/executors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,21 @@ import { documentFrontMatter } from "../markdown/document";
2222
import { isExecutableLanguageBlockOf } from "quarto-core";
2323
import { workspace } from "vscode";
2424
import { JupyterKernelspec } from "core";
25+
import { Position } from "vscode";
2526

2627
export interface CellExecutor {
2728
execute: (blocks: string[], editorUri?: Uri) => Promise<void>;
2829
executeSelection?: () => Promise<void>;
30+
executeAtPosition?: (uri: Uri, pos: Position) => Promise<Position>;
2931
}
3032

3133
export function executableLanguages() {
3234
return kCellExecutors.map((executor) => executor.language);
3335
}
3436

37+
// This function is always used by the `defaultExtensionHost`, and is used
38+
// by the `hooksExtensionHost` as a backup. Please see `hooksExtensionHost`
39+
// how executors are retrieved in Positron.
3540
export async function cellExecutorForLanguage(
3641
language: string,
3742
document: TextDocument,

apps/vscode/src/host/hooks.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { CellExecutor, cellExecutorForLanguage, executableLanguages, isKnitrDocu
2323
import { ExecuteQueue } from './execute-queue';
2424
import { MarkdownEngine } from '../markdown/engine';
2525
import { virtualDoc, adjustedPosition, unadjustedRange, withVirtualDocUri } from "../vdoc/vdoc";
26+
import { Position } from 'vscode';
27+
import { Uri } from 'vscode';
2628

2729
declare global {
2830
function acquirePositronApi(): hooks.PositronApi;
@@ -83,6 +85,20 @@ export function hooksExtensionHost(): ExtensionHost {
8385
},
8486
executeSelection: async (): Promise<void> => {
8587
await vscode.commands.executeCommand('workbench.action.positronConsole.executeCode', { languageId: language });
88+
},
89+
executeAtPosition: async (uri: Uri, position: Position): Promise<Position> => {
90+
try {
91+
return await vscode.commands.executeCommand(
92+
'positron.executeCodeFromPosition',
93+
language,
94+
uri,
95+
position
96+
) as Position;
97+
} catch (e) {
98+
// an error can happen, we think, if the statementRangeProvider errors
99+
console.error('error when using `positron.executeCodeFromPosition`');
100+
}
101+
return position;
86102
}
87103
};
88104

apps/vscode/src/providers/cell/commands.ts

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,18 @@ import {
4646
codeWithoutOptionsFromBlock,
4747
executeInteractive,
4848
executeSelectionInteractive,
49+
executeAtPositionInteractive,
4950
} from "./executors";
5051
import { ExtensionHost } from "../../host";
51-
import { hasHooks } from "../../host/hooks";
52+
import { tryAcquirePositronApi } from "@posit-dev/positron";
5253
import { isKnitrDocument } from "../../host/executors";
5354
import { commands } from "vscode";
55+
import { virtualDocForCode, withVirtualDocUri } from "../../vdoc/vdoc";
56+
import { embeddedLanguage } from "../../vdoc/languages";
57+
import { Uri } from "vscode";
58+
import { StatementRange } from "positron";
59+
60+
const isPositron = tryAcquirePositronApi();
5461

5562
export function cellCommands(host: ExtensionHost, engine: MarkdownEngine): Command[] {
5663
return [
@@ -146,9 +153,7 @@ abstract class RunCommand {
146153
}
147154

148155
private async hasExecutorForLanguage(language: string, document: TextDocument, engine: MarkdownEngine) {
149-
// TODO: this is incorrect right? `cellExecutorForLanguage` returns a promise, and a promise will always be truthy?
150-
// We should have to await it before doing `!!`
151-
return !!this.cellExecutorForLanguage(language, document, engine);
156+
return undefined !== (await this.cellExecutorForLanguage(language, document, engine));
152157
}
153158

154159
}
@@ -259,7 +264,11 @@ class RunPreviousCellCommand extends RunCommand implements Command {
259264
}
260265
}
261266

267+
// More permissive type than `Position` so its easier to construct via a literal
268+
type LineAndCharPos = { line: number, character: number; };
269+
262270

271+
// Run the code at the cursor
263272
class RunCurrentCommand extends RunCommand implements Command {
264273
constructor(
265274
host: ExtensionHost,
@@ -292,7 +301,7 @@ class RunCurrentCommand extends RunCommand implements Command {
292301
const resolveToRunCell = editor.selection.isEmpty &&
293302
!this.runSelection_ &&
294303
!isKnitrDocument(editor.document, this.engine_) &&
295-
(!hasHooks() && (language === "python" || language === "r"));
304+
(!isPositron && (language === "python" || language === "r"));
296305

297306
if (resolveToRunCell) {
298307
const code = codeWithoutOptionsFromBlock(block);
@@ -336,38 +345,66 @@ class RunCurrentCommand extends RunCommand implements Command {
336345
context: CodeViewActiveBlockContext
337346
): Promise<void> {
338347
// get selection and active block
339-
let selection = context.selectedText;
348+
const selection = context.selectedText;
340349
const activeBlock = context.blocks.find(block => block.active);
341350

342-
// if the selection is empty and this isn't a knitr document then it resolves to run cell
343-
if (selection.length <= 0 && !isKnitrDocument(editor.document, this.engine_)) {
344-
if (activeBlock) {
345-
const executor = await this.cellExecutorForLanguage(activeBlock.language, editor.document, this.engine_);
346-
if (executor) {
347-
await executeInteractive(executor, [activeBlock.code], editor.document);
348-
await activateIfRequired(editor);
351+
// if in Positron
352+
if (isPositron) {
353+
if (activeBlock && selection.length <= 0) {
354+
const codeLines = lines(activeBlock.code);
355+
const vdoc = virtualDocForCode(codeLines, embeddedLanguage(activeBlock.language)!);
356+
if (vdoc) {
357+
const parentUri = Uri.file(editor.document.fileName);
358+
const injectedLines = (vdoc.language?.inject?.length ?? 0);
359+
360+
const positionIntoVdoc = (p: LineAndCharPos) =>
361+
new Position(p.line + injectedLines, p.character);
362+
const positionOutOfVdoc = (p: LineAndCharPos) =>
363+
new Position(p.line - injectedLines, p.character);
364+
365+
const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_);
366+
if (executor) {
367+
const nextStatementPos = await withVirtualDocUri(
368+
vdoc,
369+
parentUri,
370+
"executeSelectionAtPositionInteractive",
371+
(uri) => executeAtPositionInteractive(
372+
executor,
373+
uri,
374+
positionIntoVdoc(context.selection.start)
375+
)
376+
);
377+
378+
if (nextStatementPos !== undefined) {
379+
await editor.setBlockSelection(context, positionOutOfVdoc(nextStatementPos));
380+
}
381+
}
349382
}
350383
}
351-
384+
// if not in Positron
352385
} else {
353-
// if the selection is empty take the whole line, otherwise take the selected text exactly
354-
let action: CodeViewSelectionAction | undefined;
355-
if (selection.length <= 0) {
386+
// if the selection is empty and this isn't a knitr document then it resolves to run cell
387+
if (selection.length <= 0 && !isKnitrDocument(editor.document, this.engine_)) {
356388
if (activeBlock) {
357-
selection = lines(activeBlock.code)[context.selection.start.line];
358-
action = "nextline";
389+
const executor = await this.cellExecutorForLanguage(activeBlock.language, editor.document, this.engine_);
390+
if (executor) {
391+
await executeInteractive(executor, [activeBlock.code], editor.document);
392+
await activateIfRequired(editor);
393+
}
359394
}
360-
}
361-
362-
// run code
363-
const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_);
364-
if (executor) {
365-
await executeInteractive(executor, [selection], editor.document);
395+
} else {
396+
const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_);
397+
if (executor) {
398+
if (selection.length > 0) {
399+
await executeInteractive(executor, [selection], editor.document);
400+
await editor.setBlockSelection(context, "nextline");
401+
} else if (activeBlock) { // if the selection is empty take the whole line as the selection
402+
await executeInteractive(executor, [lines(activeBlock.code)[context.selection.start.line]], editor.document);
403+
await editor.setBlockSelection(context, "nextline");
404+
}
366405

367-
// advance cursor if necessary
368-
if (action) {
369-
editor.setBlockSelection(context, "nextline");
370406
}
407+
371408
}
372409
}
373410
}
@@ -382,7 +419,7 @@ class RunSelectionCommand extends RunCurrentCommand implements Command {
382419

383420
}
384421

385-
422+
// Run Cell and Advance
386423
class RunCurrentAdvanceCommand extends RunCommand implements Command {
387424
constructor(host: ExtensionHost, engine: MarkdownEngine) {
388425
super(host, engine);

apps/vscode/src/providers/cell/executors.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import { cellOptionsForToken, kExecuteEval } from "./options";
3434

3535
import { CellExecutor, ExtensionHost } from "../../host";
3636
import { executableLanguages } from "../../host/executors";
37+
import { Position } from "vscode";
38+
import { Uri } from "vscode";
3739

3840

3941
export function hasExecutor(_host: ExtensionHost, language: string) {
@@ -90,6 +92,12 @@ export async function executeInteractive(
9092
return await executor.execute(blocks, !document.isUntitled ? document.uri : undefined);
9193
}
9294

95+
96+
export async function executeAtPositionInteractive(executor: CellExecutor, uri: Uri, position: Position) {
97+
if (executor?.executeAtPosition) {
98+
return await executor.executeAtPosition(uri, position);
99+
}
100+
}
93101
// attempt language aware execution of current selection (returns false
94102
// if the executor doesn't support this, in which case generic
95103
// executeInteractive will be called)

apps/vscode/src/providers/editor/codeview.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ export function vscodeCodeViewServer(_engine: MarkdownEngine, document: TextDocu
6060
async codeViewAssist(context: CodeViewCellContext) {
6161
await commands.executeCommand("quarto.codeViewAssist", context, lspRequest);
6262
},
63+
// This execute command is used when the user clicks an execute button on a cell in the visual editor.
64+
//
65+
// Note: this is NOT used when the user uses a keyboard command to execute a cell,
66+
// that goes through VSCode commands (commands are registered in package.json),
67+
// the keyboard command code is in apps/vscode/src/providers/cell/commands.ts.
6368
async codeViewExecute(execute: CodeViewExecute) {
6469
switch (execute) {
6570
case "cell":

apps/vscode/src/vdoc/vdoc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export type VirtualDocAction =
120120
"format" |
121121
"statementRange" |
122122
"helpTopic" |
123+
"executeSelectionAtPositionInteractive" |
123124
"semanticTokens";
124125

125126
export type VirtualDocUri = { uri: Uri, cleanup?: () => Promise<void>; };

packages/editor-codemirror/src/behaviors/trackselection.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { DispatchEvent, codeViewCellContext, kCodeViewNextLineTransaction } from
2727
import { Behavior, BehaviorContext, State } from ".";
2828

2929
// track the selection in prosemirror
30-
export function trackSelectionBehavior(context: BehaviorContext) : Behavior {
30+
export function trackSelectionBehavior(context: BehaviorContext): Behavior {
3131

3232
let unsubscribe: VoidFunction;
3333

@@ -50,32 +50,32 @@ export function trackSelectionBehavior(context: BehaviorContext) : Behavior {
5050
unsubscribe = context.pmContext.events.subscribe(DispatchEvent, (tr: Transaction | undefined) => {
5151
if (tr) {
5252
// track selection changes that occur when we don't have focus
53-
if (!cmView.hasFocus && tr.selectionSet && !tr.docChanged && (tr.selection instanceof TextSelection)) {
53+
if (tr.selectionSet && !tr.docChanged && (tr.selection instanceof TextSelection)) {
5454
const cmSelection = asCodeMirrorSelection(context.view, cmView, context.getPos);
5555
context.withState(State.Updating, () => {
5656
if (cmSelection) {
5757
cmView.dispatch({ selection: cmSelection });
5858
} else {
59-
cmView.dispatch({ selection: EditorSelection.single(0)})
60-
}
59+
cmView.dispatch({ selection: EditorSelection.single(0) })
60+
}
6161
})
6262
} else if (tr.getMeta(kCodeViewNextLineTransaction) === true) {
6363
// NOTE: this is a special directive to advance to the next line. as distinct
6464
// from the block above it is not a reporting of a change in the PM selection
65-
// but rather an instruction to move the CM selection to the next line. as
65+
// but rather an instruction to move the CM selection to the next line. as
6666
// such we do not encose the code in State.Updating, because we want an update
6767
// to the PM selection to occur
6868
const cmSelection = asCodeMirrorSelection(context.view, cmView, context.getPos);
6969
if (cmSelection) {
7070
if (cursorLineDown(cmView)) {
7171
cursorLineStart(cmView);
72-
}
72+
}
7373
}
74-
// for other selection changes
74+
// for other selection changes
7575
} else if (cmView.hasFocus && tr.selectionSet && (tr.selection instanceof TextSelection)) {
7676
codeViewAssist();
7777
}
78-
}
78+
}
7979
});
8080
},
8181

@@ -91,7 +91,7 @@ export const asCodeMirrorSelection = (
9191
cmView: EditorView,
9292
getPos: (() => number) | boolean
9393
) => {
94-
if (typeof(getPos) === "function") {
94+
if (typeof (getPos) === "function") {
9595
const offset = getPos() + 1;
9696
const node = pmView.state.doc.nodeAt(getPos());
9797
if (node) {
@@ -104,8 +104,8 @@ export const asCodeMirrorSelection = (
104104
} else if (selection.from <= cmRange.from && selection.to >= cmRange.to) {
105105
return EditorSelection.single(0, cmView.state.doc.length);
106106
}
107-
107+
108108
}
109109
}
110110
return undefined;
111-
}
111+
}

packages/editor-types/src/codeview.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ export const kCodeViewGetDiagnostics = 'code_view_get_diagnostics';
2727

2828
export type CodeViewExecute = "selection" | "cell" | "cell+advance" | "above" | "below";
2929

30+
export type CodeViewBlock = { pos: number, language: string, code: string; active: boolean; };
3031
export interface CodeViewActiveBlockContext {
3132
activeLanguage: string;
32-
blocks: Array<{ pos: number, language: string, code: string; active: boolean; }>;
33+
blocks: Array<CodeViewBlock>;
3334
selection: Range;
3435
selectedText: string;
3536
}
3637

37-
export type CodeViewSelectionAction = "nextline" | "nextblock" | "prevblock";
38+
export type CodeViewSelectionAction = "nextline" | "nextblock" | "prevblock" | { line: number, character: number; };
3839

3940
export interface CodeViewCellContext {
4041
filepath: string;

0 commit comments

Comments
 (0)