From 60c08277061c59804dac28878f4456f0bc9b7e00 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Fri, 14 Nov 2025 12:09:11 -0700 Subject: [PATCH 1/3] Add new URI and position args for `workbench.action.positronConsole.executeCode` --- .../browser/positronConsoleActions.ts | 74 +++++++++++++------ 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts b/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts index a7c7df123ef2..c86fd9256214 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts +++ b/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; +import { URI } from '../../../../base/common/uri.js'; import { isString } from '../../../../base/common/types.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IEditor } from '../../../../editor/common/editorCommon.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; import { Position } from '../../../../editor/common/core/position.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; @@ -315,6 +317,8 @@ export function registerPositronConsoleActions() { * - advance: Optionally, if the cursor should be advanced to the next statement. If `undefined`, fallbacks to `true`. * - mode: Optionally, the code execution mode for a language runtime. If `undefined` fallbacks to `Interactive`. * - errorBehavior: Optionally, the error behavior for a language runtime. If `undefined` fallbacks to `Continue`. + * - uri: The URI of the document to execute code from. Must be provided together with `position`. + * - position: The position in the document to execute code from. Must be provided together with `uri`. */ async run( accessor: ServicesAccessor, @@ -324,13 +328,17 @@ export function registerPositronConsoleActions() { advance?: boolean; mode?: RuntimeCodeExecutionMode; errorBehavior?: RuntimeErrorBehavior; - } = {} + } & ( + | { uri: URI; position: Position } + | { uri?: never; position?: never } + ) = {} ) { // Access services. const editorService = accessor.get(IEditorService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); const languageService = accessor.get(ILanguageService); const logService = accessor.get(ILogService); + const modelService = accessor.get(IModelService); const notificationService = accessor.get(INotificationService); const positronConsoleService = accessor.get(IPositronConsoleService); @@ -340,15 +348,43 @@ export function registerPositronConsoleActions() { // The code to execute. let code: string | undefined = undefined; - // If there is no active editor, there is nothing to execute. - const editor = editorService.activeTextEditorControl as IEditor; - if (!editor) { - return; + // Determine if we're using a provided URI or the active editor + let editor: IEditor | undefined; + let model: ITextModel | undefined; + let position: Position; + + if (opts.uri) { + // Use the provided URI to get the model + const foundModel = modelService.getModel(opts.uri); + if (!foundModel) { + notificationService.notify({ + severity: Severity.Info, + message: localize('positron.executeCode.noModel', "Cannot execute code. Unable to find document at {0}.", opts.uri.toString()), + sticky: false + }); + return; + } + model = foundModel; + // Use the provided position (guaranteed to exist when uri is provided) + position = opts.position; + // No editor context when URI is provided + editor = undefined; + } else { + // Use the active editor + editor = editorService.activeTextEditorControl as IEditor; + if (!editor) { + return; + } + model = editor.getModel() as ITextModel; + const editorPosition = editor.getPosition(); + if (!editorPosition) { + return; + } + position = editorPosition; } // Get the code to execute. - const selection = editor.getSelection(); - const model = editor.getModel() as ITextModel; + const selection = editor?.getSelection(); // If we have a selection and it isn't empty, then we use its contents (even if it // only contains whitespace or comments) and also retain the user's selection location. @@ -367,13 +403,6 @@ export function registerPositronConsoleActions() { // HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK } - // Get the position of the cursor. If we don't have a selection, we'll use this to - // determine the code to execute. - const position = editor.getPosition(); - if (!position) { - return; - } - // Get all the statement range providers for the active document. const statementRangeProviders = languageFeaturesService.statementRangeProvider.all(model); @@ -400,7 +429,7 @@ export function registerPositronConsoleActions() { // returned `undefined` if it didn't think it was important. code = isString(statementRange.code) ? statementRange.code : model.getValueInRange(statementRange.range); - if (advance) { + if (advance && editor) { await this.advanceStatement(model, editor, statementRange, statementRangeProviders[0], logService); } } else { @@ -412,8 +441,7 @@ export function registerPositronConsoleActions() { // If no selection was found, use the contents of the line containing the cursor // position. if (!isString(code)) { - const position = editor.getPosition(); - let lineNumber = position?.lineNumber ?? 0; + let lineNumber = position.lineNumber; if (lineNumber > 0) { // Find the first non-empty line after the cursor position and read the @@ -431,11 +459,11 @@ export function registerPositronConsoleActions() { // If we have code and a position move the cursor to the next line with code on it, // or just to the next line if all additional lines are blank. - if (advance && isString(code) && position) { + if (advance && isString(code) && editor) { this.advanceLine(model, editor, position, lineNumber, code, editorService); } - if (!isString(code) && position && lineNumber === model.getLineCount()) { + if (!isString(code) && lineNumber === model.getLineCount()) { // If we still don't have code and we are at the end of the document, add a // newline to the end of the document. this.amendNewlineToEnd(model); @@ -443,9 +471,11 @@ export function registerPositronConsoleActions() { // We don't move to that new line to avoid adding a bunch of empty // lines to the end. The edit operation typically moves us to the new line, // so we have to undo that. - const newPosition = new Position(lineNumber, 1); - editor.setPosition(newPosition); - editor.revealPositionInCenterIfOutsideViewport(newPosition); + if (editor) { + const newPosition = new Position(lineNumber, 1); + editor.setPosition(newPosition); + editor.revealPositionInCenterIfOutsideViewport(newPosition); + } } // If we still don't have code after looking at the cursor position, From 215fb5663565a302d2c37075927731d29ed4494a Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Fri, 14 Nov 2025 12:40:09 -0700 Subject: [PATCH 2/3] Make a new extension host API command `positron.executeCodeInConsole` that uses vscode.URI and vscode.Position types --- .../api/common/extHostApiCommands.ts | 10 ++++++++ .../browser/positronConsoleActions.ts | 25 ++++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 26fd2503caa9..adc59bb94c59 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -557,6 +557,16 @@ const newCommands: ApiCommand[] = [ return result; }) ), + // -- execute code in console + new ApiCommand( + 'positron.executeCodeInConsole', '_executeCodeInConsole', 'Execute code in the Positron console.', + [ + ApiCommandArgument.String.with('languageId', 'The language ID of the code to execute'), + ApiCommandArgument.Uri.with('uri', 'The URI of the document to execute code from'), + ApiCommandArgument.Position.with('position', 'The position in the document to execute code from') + ], + ApiCommandResult.Void + ), // --- End Positron // --- context keys diff --git a/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts b/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts index c86fd9256214..c1c1872ae850 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts +++ b/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts @@ -5,14 +5,14 @@ import { localize } from '../../../../nls.js'; import { URI } from '../../../../base/common/uri.js'; -import { isString } from '../../../../base/common/types.js'; +import { isString, assertType } from '../../../../base/common/types.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IEditor } from '../../../../editor/common/editorCommon.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { Position } from '../../../../editor/common/core/position.js'; +import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { ILocalizedString } from '../../../../platform/action/common/action.js'; @@ -34,7 +34,7 @@ import { IPositronModalDialogsService } from '../../../services/positronModalDia import { IPositronConsoleService, POSITRON_CONSOLE_VIEW_ID } from '../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; import { IExecutionHistoryService } from '../../../services/positronHistory/common/executionHistoryService.js'; import { CodeAttributionSource, IConsoleCodeAttribution } from '../../../services/positronConsole/common/positronConsoleCodeExecution.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; import { POSITRON_NOTEBOOK_CELL_EDITOR_FOCUSED } from '../../positronNotebook/browser/ContextKeysManager.js'; import { getContextFromActiveEditor } from '../../notebook/browser/controller/coreActions.js'; @@ -1092,3 +1092,22 @@ export function registerPositronConsoleActions() { } }); } + + +/** + * Register the internal command for executing code in console from the extension API. + * This command is called by the positron.executeCodeInConsole API command. + */ +CommandsRegistry.registerCommand('_executeCodeInConsole', async (accessor, ...args: [string, URI, IPosition]) => { + const [languageId, uri, position] = args; + assertType(typeof languageId === 'string'); + assertType(URI.isUri(uri)); + assertType(Position.isIPosition(position)); + + const commandService = accessor.get(ICommandService); + return await commandService.executeCommand('workbench.action.positronConsole.executeCode', { + languageId, + uri, + position: Position.lift(position) + }); +}); From bb485d862d0e9e48667eb0d5931632f4cbd61e50 Mon Sep 17 00:00:00 2001 From: Julia Silge Date: Fri, 14 Nov 2025 13:04:13 -0700 Subject: [PATCH 3/3] New command returns the next position --- .../api/common/extHostApiCommands.ts | 4 +- .../browser/positronConsoleActions.ts | 37 +++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index adc59bb94c59..a29558ac3118 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -565,7 +565,9 @@ const newCommands: ApiCommand[] = [ ApiCommandArgument.Uri.with('uri', 'The URI of the document to execute code from'), ApiCommandArgument.Position.with('position', 'The position in the document to execute code from') ], - ApiCommandResult.Void + new ApiCommandResult('A promise that resolves to the next position after executing the code.', result => { + return result ? typeConverters.Position.to(result) : undefined; + }) ), // --- End Positron diff --git a/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts b/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts index c1c1872ae850..fc3f17b5170c 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts +++ b/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts @@ -332,7 +332,7 @@ export function registerPositronConsoleActions() { | { uri: URI; position: Position } | { uri?: never; position?: never } ) = {} - ) { + ): Promise { // Access services. const editorService = accessor.get(IEditorService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); @@ -352,6 +352,7 @@ export function registerPositronConsoleActions() { let editor: IEditor | undefined; let model: ITextModel | undefined; let position: Position; + let nextPosition: Position | undefined; if (opts.uri) { // Use the provided URI to get the model @@ -429,8 +430,8 @@ export function registerPositronConsoleActions() { // returned `undefined` if it didn't think it was important. code = isString(statementRange.code) ? statementRange.code : model.getValueInRange(statementRange.range); - if (advance && editor) { - await this.advanceStatement(model, editor, statementRange, statementRangeProviders[0], logService); + if (advance) { + nextPosition = await this.advanceStatement(model, editor, statementRange, statementRangeProviders[0], logService); } } else { // The statement range provider didn't return a range. This @@ -459,8 +460,8 @@ export function registerPositronConsoleActions() { // If we have code and a position move the cursor to the next line with code on it, // or just to the next line if all additional lines are blank. - if (advance && isString(code) && editor) { - this.advanceLine(model, editor, position, lineNumber, code, editorService); + if (advance && isString(code)) { + nextPosition = this.advanceLine(model, editor, position, lineNumber, code, editorService); } if (!isString(code) && lineNumber === model.getLineCount()) { @@ -502,18 +503,18 @@ export function registerPositronConsoleActions() { mode: opts.mode, errorBehavior: opts.errorBehavior }); + return nextPosition; } async advanceStatement( model: ITextModel, - editor: IEditor, + editor: IEditor | undefined, statementRange: IStatementRange, provider: StatementRangeProvider, logService: ILogService, - ) { + ): Promise { - // Move the cursor to the next - // statement by creating a position on the line + // Calculate the next position by creating a position on the line // following the statement and then invoking the // statement range provider again to find the appropriate // boundary of the next statement. @@ -535,8 +536,6 @@ export function registerPositronConsoleActions() { model.getLineCount(), 1 ); - editor.setPosition(newPosition); - editor.revealPositionInCenterIfOutsideViewport(newPosition); } else { // Invoke the statement range provider again to // find the appropriate boundary of the next statement. @@ -578,20 +577,24 @@ export function registerPositronConsoleActions() { ); } } + } + // Only move the cursor if we have an editor + if (editor) { editor.setPosition(newPosition); editor.revealPositionInCenterIfOutsideViewport(newPosition); } + return newPosition; } advanceLine( model: ITextModel, - editor: IEditor, + editor: IEditor | undefined, position: Position, lineNumber: number, code: string, editorService: IEditorService, - ) { + ): Position { // HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK // This attempts to address https://github.com/posit-dev/positron/issues/1177 // by tacking a newline onto indented Python code fragments that end at an empty @@ -628,8 +631,12 @@ export function registerPositronConsoleActions() { } const newPosition = position.with(lineNumber, 0); - editor.setPosition(newPosition); - editor.revealPositionInCenterIfOutsideViewport(newPosition); + // Only move the cursor if we have an editor + if (editor) { + editor.setPosition(newPosition); + editor.revealPositionInCenterIfOutsideViewport(newPosition); + } + return newPosition; } amendNewlineToEnd(model: ITextModel) {