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
2 changes: 1 addition & 1 deletion apps/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"private": true,
"engines": {
"vscode": "^1.75.0",
"positron": "^2025.6.0"
"positron": "^2025.12.0"
},
"main": "./out/main.js",
"browser": "./browser.js",
Expand Down
5 changes: 5 additions & 0 deletions apps/vscode/src/host/executors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,21 @@ import { documentFrontMatter } from "../markdown/document";
import { isExecutableLanguageBlockOf } from "quarto-core";
import { workspace } from "vscode";
import { JupyterKernelspec } from "core";
import { Position } from "vscode";

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

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

// This function is always used by the `defaultExtensionHost`, and is used
// by the `hooksExtensionHost` as a backup. Please see `hooksExtensionHost`
// how executors are retrieved in Positron.
export async function cellExecutorForLanguage(
language: string,
document: TextDocument,
Expand Down
16 changes: 16 additions & 0 deletions apps/vscode/src/host/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { CellExecutor, cellExecutorForLanguage, executableLanguages, isKnitrDocu
import { ExecuteQueue } from './execute-queue';
import { MarkdownEngine } from '../markdown/engine';
import { virtualDoc, adjustedPosition, unadjustedRange, withVirtualDocUri } from "../vdoc/vdoc";
import { Position } from 'vscode';
import { Uri } from 'vscode';

declare global {
function acquirePositronApi(): hooks.PositronApi;
Expand Down Expand Up @@ -83,6 +85,20 @@ export function hooksExtensionHost(): ExtensionHost {
},
executeSelection: async (): Promise<void> => {
await vscode.commands.executeCommand('workbench.action.positronConsole.executeCode', { languageId: language });
},
executeAtPosition: async (uri: Uri, position: Position): Promise<Position> => {
try {
return await vscode.commands.executeCommand(
'positron.executeCodeFromPosition',
language,
uri,
position
) as Position;
} catch (e) {
// an error can happen, we think, if the statementRangeProvider errors
console.error('error when using `positron.executeCodeFromPosition`');
}
return position;
}
};

Expand Down
95 changes: 66 additions & 29 deletions apps/vscode/src/providers/cell/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,18 @@ import {
codeWithoutOptionsFromBlock,
executeInteractive,
executeSelectionInteractive,
executeAtPositionInteractive,
} from "./executors";
import { ExtensionHost } from "../../host";
import { hasHooks } from "../../host/hooks";
import { tryAcquirePositronApi } from "@posit-dev/positron";
import { isKnitrDocument } from "../../host/executors";
import { commands } from "vscode";
import { virtualDocForCode, withVirtualDocUri } from "../../vdoc/vdoc";
import { embeddedLanguage } from "../../vdoc/languages";
import { Uri } from "vscode";
import { StatementRange } from "positron";

const isPositron = tryAcquirePositronApi();

export function cellCommands(host: ExtensionHost, engine: MarkdownEngine): Command[] {
return [
Expand Down Expand Up @@ -146,9 +153,7 @@ abstract class RunCommand {
}

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

}
Expand Down Expand Up @@ -259,7 +264,11 @@ class RunPreviousCellCommand extends RunCommand implements Command {
}
}

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


// Run the code at the cursor
class RunCurrentCommand extends RunCommand implements Command {
constructor(
host: ExtensionHost,
Expand Down Expand Up @@ -292,7 +301,7 @@ class RunCurrentCommand extends RunCommand implements Command {
const resolveToRunCell = editor.selection.isEmpty &&
!this.runSelection_ &&
!isKnitrDocument(editor.document, this.engine_) &&
(!hasHooks() && (language === "python" || language === "r"));
(!isPositron && (language === "python" || language === "r"));

if (resolveToRunCell) {
const code = codeWithoutOptionsFromBlock(block);
Expand Down Expand Up @@ -336,38 +345,66 @@ class RunCurrentCommand extends RunCommand implements Command {
context: CodeViewActiveBlockContext
): Promise<void> {
// get selection and active block
let selection = context.selectedText;
const selection = context.selectedText;
const activeBlock = context.blocks.find(block => block.active);

// if the selection is empty and this isn't a knitr document then it resolves to run cell
if (selection.length <= 0 && !isKnitrDocument(editor.document, this.engine_)) {
if (activeBlock) {
const executor = await this.cellExecutorForLanguage(activeBlock.language, editor.document, this.engine_);
if (executor) {
await executeInteractive(executor, [activeBlock.code], editor.document);
await activateIfRequired(editor);
// if in Positron
if (isPositron) {
if (activeBlock && selection.length <= 0) {
const codeLines = lines(activeBlock.code);
const vdoc = virtualDocForCode(codeLines, embeddedLanguage(activeBlock.language)!);
if (vdoc) {
const parentUri = Uri.file(editor.document.fileName);
const injectedLines = (vdoc.language?.inject?.length ?? 0);

const positionIntoVdoc = (p: LineAndCharPos) =>
new Position(p.line + injectedLines, p.character);
const positionOutOfVdoc = (p: LineAndCharPos) =>
new Position(p.line - injectedLines, p.character);

const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_);
if (executor) {
const nextStatementPos = await withVirtualDocUri(
vdoc,
parentUri,
"executeSelectionAtPositionInteractive",
(uri) => executeAtPositionInteractive(
executor,
uri,
positionIntoVdoc(context.selection.start)
)
);

if (nextStatementPos !== undefined) {
await editor.setBlockSelection(context, positionOutOfVdoc(nextStatementPos));
}
}
}
}

// if not in Positron
} else {
// if the selection is empty take the whole line, otherwise take the selected text exactly
let action: CodeViewSelectionAction | undefined;
if (selection.length <= 0) {
// if the selection is empty and this isn't a knitr document then it resolves to run cell
if (selection.length <= 0 && !isKnitrDocument(editor.document, this.engine_)) {
if (activeBlock) {
selection = lines(activeBlock.code)[context.selection.start.line];
action = "nextline";
const executor = await this.cellExecutorForLanguage(activeBlock.language, editor.document, this.engine_);
if (executor) {
await executeInteractive(executor, [activeBlock.code], editor.document);
await activateIfRequired(editor);
}
}
}

// run code
const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_);
if (executor) {
await executeInteractive(executor, [selection], editor.document);
} else {
const executor = await this.cellExecutorForLanguage(context.activeLanguage, editor.document, this.engine_);
if (executor) {
if (selection.length > 0) {
await executeInteractive(executor, [selection], editor.document);
await editor.setBlockSelection(context, "nextline");
} else if (activeBlock) { // if the selection is empty take the whole line as the selection
await executeInteractive(executor, [lines(activeBlock.code)[context.selection.start.line]], editor.document);
await editor.setBlockSelection(context, "nextline");
}

// advance cursor if necessary
if (action) {
editor.setBlockSelection(context, "nextline");
}

}
}
}
Expand All @@ -382,7 +419,7 @@ class RunSelectionCommand extends RunCurrentCommand implements Command {

}


// Run Cell and Advance
class RunCurrentAdvanceCommand extends RunCommand implements Command {
constructor(host: ExtensionHost, engine: MarkdownEngine) {
super(host, engine);
Expand Down
8 changes: 8 additions & 0 deletions apps/vscode/src/providers/cell/executors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import { cellOptionsForToken, kExecuteEval } from "./options";

import { CellExecutor, ExtensionHost } from "../../host";
import { executableLanguages } from "../../host/executors";
import { Position } from "vscode";
import { Uri } from "vscode";


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


export async function executeAtPositionInteractive(executor: CellExecutor, uri: Uri, position: Position) {
if (executor?.executeAtPosition) {
return await executor.executeAtPosition(uri, position);
}
}
// attempt language aware execution of current selection (returns false
// if the executor doesn't support this, in which case generic
// executeInteractive will be called)
Expand Down
5 changes: 5 additions & 0 deletions apps/vscode/src/providers/editor/codeview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export function vscodeCodeViewServer(_engine: MarkdownEngine, document: TextDocu
async codeViewAssist(context: CodeViewCellContext) {
await commands.executeCommand("quarto.codeViewAssist", context, lspRequest);
},
// This execute command is used when the user clicks an execute button on a cell in the visual editor.
//
// Note: this is NOT used when the user uses a keyboard command to execute a cell,
// that goes through VSCode commands (commands are registered in package.json),
// the keyboard command code is in apps/vscode/src/providers/cell/commands.ts.
async codeViewExecute(execute: CodeViewExecute) {
switch (execute) {
case "cell":
Expand Down
1 change: 1 addition & 0 deletions apps/vscode/src/vdoc/vdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export type VirtualDocAction =
"format" |
"statementRange" |
"helpTopic" |
"executeSelectionAtPositionInteractive" |
"semanticTokens";

export type VirtualDocUri = { uri: Uri, cleanup?: () => Promise<void>; };
Expand Down
22 changes: 11 additions & 11 deletions packages/editor-codemirror/src/behaviors/trackselection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { DispatchEvent, codeViewCellContext, kCodeViewNextLineTransaction } from
import { Behavior, BehaviorContext, State } from ".";

// track the selection in prosemirror
export function trackSelectionBehavior(context: BehaviorContext) : Behavior {
export function trackSelectionBehavior(context: BehaviorContext): Behavior {

let unsubscribe: VoidFunction;

Expand All @@ -50,32 +50,32 @@ export function trackSelectionBehavior(context: BehaviorContext) : Behavior {
unsubscribe = context.pmContext.events.subscribe(DispatchEvent, (tr: Transaction | undefined) => {
if (tr) {
// track selection changes that occur when we don't have focus
if (!cmView.hasFocus && tr.selectionSet && !tr.docChanged && (tr.selection instanceof TextSelection)) {
if (tr.selectionSet && !tr.docChanged && (tr.selection instanceof TextSelection)) {
Comment on lines 52 to +53
Copy link
Collaborator Author

@vezwork vezwork Dec 10, 2025

Choose a reason for hiding this comment

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

To be honest, this change may be a bit risky because I don't understand everything it could impact. BUT, it makes it possible to set the selection in packages/editor/src/api/codeview.ts in a seemingly very reasonable way and I did not find another way to do so.

const cmSelection = asCodeMirrorSelection(context.view, cmView, context.getPos);
context.withState(State.Updating, () => {
if (cmSelection) {
cmView.dispatch({ selection: cmSelection });
} else {
cmView.dispatch({ selection: EditorSelection.single(0)})
}
cmView.dispatch({ selection: EditorSelection.single(0) })
}
})
} else if (tr.getMeta(kCodeViewNextLineTransaction) === true) {
// NOTE: this is a special directive to advance to the next line. as distinct
// from the block above it is not a reporting of a change in the PM selection
// but rather an instruction to move the CM selection to the next line. as
// but rather an instruction to move the CM selection to the next line. as
// such we do not encose the code in State.Updating, because we want an update
// to the PM selection to occur
const cmSelection = asCodeMirrorSelection(context.view, cmView, context.getPos);
if (cmSelection) {
if (cursorLineDown(cmView)) {
cursorLineStart(cmView);
}
}
}
// for other selection changes
// for other selection changes
} else if (cmView.hasFocus && tr.selectionSet && (tr.selection instanceof TextSelection)) {
codeViewAssist();
}
}
}
});
},

Expand All @@ -91,7 +91,7 @@ export const asCodeMirrorSelection = (
cmView: EditorView,
getPos: (() => number) | boolean
) => {
if (typeof(getPos) === "function") {
if (typeof (getPos) === "function") {
const offset = getPos() + 1;
const node = pmView.state.doc.nodeAt(getPos());
if (node) {
Expand All @@ -104,8 +104,8 @@ export const asCodeMirrorSelection = (
} else if (selection.from <= cmRange.from && selection.to >= cmRange.to) {
return EditorSelection.single(0, cmView.state.doc.length);
}

}
}
return undefined;
}
}
5 changes: 3 additions & 2 deletions packages/editor-types/src/codeview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ export const kCodeViewGetDiagnostics = 'code_view_get_diagnostics';

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

export type CodeViewBlock = { pos: number, language: string, code: string; active: boolean; };
export interface CodeViewActiveBlockContext {
activeLanguage: string;
blocks: Array<{ pos: number, language: string, code: string; active: boolean; }>;
blocks: Array<CodeViewBlock>;
selection: Range;
selectedText: string;
}

export type CodeViewSelectionAction = "nextline" | "nextblock" | "prevblock";
export type CodeViewSelectionAction = "nextline" | "nextblock" | "prevblock" | { line: number, character: number; };

export interface CodeViewCellContext {
filepath: string;
Expand Down
Loading