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
12 changes: 12 additions & 0 deletions src/vs/workbench/api/common/extHostApiCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,18 @@ const newCommands: ApiCommand[] = [
return result;
})
),
// -- execute code in console
new ApiCommand(
'positron.executeCodeInConsole', '_executeCodeInConsole', 'Execute code in the Positron console.',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason we need a new command over here is to use the type converters for position, to get the position type that extensions can use.

[
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')
],
new ApiCommandResult<IPosition | undefined, types.Position | undefined>('A promise that resolves to the next position after executing the code.', result => {
return result ? typeConverters.Position.to(result) : undefined;
})
),
// --- End Positron

// --- context keys
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
*--------------------------------------------------------------------------------------------*/

import { localize } from '../../../../nls.js';
import { isString } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.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 { Position } from '../../../../editor/common/core/position.js';
import { IModelService } from '../../../../editor/common/services/model.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';
Expand All @@ -32,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';

Expand Down Expand Up @@ -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,
Expand All @@ -324,13 +328,17 @@ export function registerPositronConsoleActions() {
advance?: boolean;
mode?: RuntimeCodeExecutionMode;
errorBehavior?: RuntimeErrorBehavior;
} = {}
) {
} & (
| { uri: URI; position: Position }
| { uri?: never; position?: never }
) = {}
): Promise<Position | undefined> {
// 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);

Expand All @@ -340,15 +348,44 @@ 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;
let nextPosition: Position | undefined;

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;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're dealing with URIs for the visual editor, and don't want to mess with the editor itself.

} 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.
Expand All @@ -367,13 +404,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);
Expand Down Expand Up @@ -401,7 +431,7 @@ export function registerPositronConsoleActions() {
code = isString(statementRange.code) ? statementRange.code : model.getValueInRange(statementRange.range);

if (advance) {
await this.advanceStatement(model, editor, statementRange, statementRangeProviders[0], logService);
nextPosition = await this.advanceStatement(model, editor, statementRange, statementRangeProviders[0], logService);
}
} else {
// The statement range provider didn't return a range. This
Expand All @@ -412,8 +442,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
Expand All @@ -431,21 +460,23 @@ 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) {
this.advanceLine(model, editor, position, lineNumber, code, editorService);
if (advance && isString(code)) {
nextPosition = 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);

// 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,
Expand All @@ -472,18 +503,18 @@ export function registerPositronConsoleActions() {
mode: opts.mode,
errorBehavior: opts.errorBehavior
});
return nextPosition;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The visual editor needs to know the next position directly, because it manages its own cursor and selection and such.

}

async advanceStatement(
model: ITextModel,
editor: IEditor,
editor: IEditor | undefined,
statementRange: IStatementRange,
provider: StatementRangeProvider,
logService: ILogService,
) {
): Promise<Position> {

// 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.
Expand All @@ -505,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.
Expand Down Expand Up @@ -548,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
Expand Down Expand Up @@ -598,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) {
Expand Down Expand Up @@ -1062,3 +1099,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]) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This command is registered here so we can make one in extHostApiCommands.ts. I want to note that right now these are not optional arguments for using with this command. Good? Bad? We could do these arguments like URI | undefined if we want.

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)
});
});