Skip to content

Commit dd39e57

Browse files
authored
Add new command for positron.executeCodeInConsole to be used by extensions that handles uri, position, next position (#10580)
- Addresses #7709 - Related to quarto-dev/quarto#867 I've been collaborating with @vezwork on how to get more language features hooked up in Quarto's visual editor, and we think this is what we need for the Quarto custom editor interface to be able to send code statement-by-statement to the console. Over in the Quarto extension, we can now do something like: ```typescript const nextPosition = await vscode.commands.executeCommand( 'positron.executeCodeInConsole', 'python', // language ID vscode.Uri.file('/path/to/script.py'), // document URI new vscode.Position(5, 0) // position ) as vscode.Position | undefined; ``` What we get back is the next position to move the cursor to, in Quarto's case for the virtual doc underlying the visual editor. It's a little weird to now have two commands, both the old `workbench.action.positronConsole.executeCode` and the new `positron.executeCodeInConsole`, but now that we're dealing with positions (and less so, URIs) we need to use the converters that we have access to in `src/vs/workbench/api/common/extHostApiCommands.ts`. I think I'd argue they do different enough things that this is OK? Although you can now technically call `workbench.action.positronConsole.executeCode` with a uri and position, but they would have to be the _internal_ main process versions of those; this feels somewhat weird. Is it worth doing more refactoring here to make this nicer? @:sessions @:console ### Release Notes #### New Features - Added new command `positron.executeCodeInConsole` for extensions to execute code for a given language, URI, and position (such as for the Quarto visual editor) #### Bug Fixes - N/A ### QA Notes Once we get changes merged on both the Positron and Quarto sides, you should be able to execute multi-line statements in the console from the visual editor. Once we get further along, I can share more detailed examples
1 parent 668a82c commit dd39e57

File tree

2 files changed

+105
-37
lines changed

2 files changed

+105
-37
lines changed

src/vs/workbench/api/common/extHostApiCommands.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,18 @@ const newCommands: ApiCommand[] = [
557557
return result;
558558
})
559559
),
560+
// -- execute code in console
561+
new ApiCommand(
562+
'positron.executeCodeFromPosition', '_executeCodeInConsole', 'Execute code in the Positron console.',
563+
[
564+
ApiCommandArgument.String.with('languageId', 'The language ID of the code to execute'),
565+
ApiCommandArgument.Uri.with('uri', 'The URI of the document to execute code from'),
566+
ApiCommandArgument.Position.with('position', 'The position in the document to execute code from')
567+
],
568+
new ApiCommandResult<IPosition | undefined, types.Position | undefined>('A promise that resolves to the next position after executing the code.', result => {
569+
return result ? typeConverters.Position.to(result) : undefined;
570+
})
571+
),
560572
// --- End Positron
561573

562574
// --- context keys

src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { localize } from '../../../../nls.js';
7-
import { isString } from '../../../../base/common/types.js';
7+
import { URI } from '../../../../base/common/uri.js';
8+
import { isString, assertType } from '../../../../base/common/types.js';
89
import { Codicon } from '../../../../base/common/codicons.js';
910
import { ITextModel } from '../../../../editor/common/model.js';
1011
import { IRange } from '../../../../editor/common/core/range.js';
1112
import { IEditor } from '../../../../editor/common/editorCommon.js';
1213
import { ILogService } from '../../../../platform/log/common/log.js';
13-
import { Position } from '../../../../editor/common/core/position.js';
14+
import { IModelService } from '../../../../editor/common/services/model.js';
15+
import { IPosition, Position } from '../../../../editor/common/core/position.js';
1416
import { CancellationToken } from '../../../../base/common/cancellation.js';
1517
import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
1618
import { ILocalizedString } from '../../../../platform/action/common/action.js';
@@ -32,7 +34,7 @@ import { IPositronModalDialogsService } from '../../../services/positronModalDia
3234
import { IPositronConsoleService, POSITRON_CONSOLE_VIEW_ID } from '../../../services/positronConsole/browser/interfaces/positronConsoleService.js';
3335
import { IExecutionHistoryService } from '../../../services/positronHistory/common/executionHistoryService.js';
3436
import { CodeAttributionSource, IConsoleCodeAttribution } from '../../../services/positronConsole/common/positronConsoleCodeExecution.js';
35-
import { ICommandService } from '../../../../platform/commands/common/commands.js';
37+
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
3638
import { POSITRON_NOTEBOOK_CELL_EDITOR_FOCUSED } from '../../positronNotebook/browser/ContextKeysManager.js';
3739
import { getContextFromActiveEditor } from '../../notebook/browser/controller/coreActions.js';
3840

@@ -315,6 +317,8 @@ export function registerPositronConsoleActions() {
315317
* - advance: Optionally, if the cursor should be advanced to the next statement. If `undefined`, fallbacks to `true`.
316318
* - mode: Optionally, the code execution mode for a language runtime. If `undefined` fallbacks to `Interactive`.
317319
* - errorBehavior: Optionally, the error behavior for a language runtime. If `undefined` fallbacks to `Continue`.
320+
* - uri: The URI of the document to execute code from. Must be provided together with `position`.
321+
* - position: The position in the document to execute code from. Must be provided together with `uri`.
318322
*/
319323
async run(
320324
accessor: ServicesAccessor,
@@ -324,13 +328,17 @@ export function registerPositronConsoleActions() {
324328
advance?: boolean;
325329
mode?: RuntimeCodeExecutionMode;
326330
errorBehavior?: RuntimeErrorBehavior;
327-
} = {}
328-
) {
331+
} & (
332+
| { uri: URI; position: Position }
333+
| { uri?: never; position?: never }
334+
) = {}
335+
): Promise<Position | undefined> {
329336
// Access services.
330337
const editorService = accessor.get(IEditorService);
331338
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
332339
const languageService = accessor.get(ILanguageService);
333340
const logService = accessor.get(ILogService);
341+
const modelService = accessor.get(IModelService);
334342
const notificationService = accessor.get(INotificationService);
335343
const positronConsoleService = accessor.get(IPositronConsoleService);
336344

@@ -340,15 +348,44 @@ export function registerPositronConsoleActions() {
340348
// The code to execute.
341349
let code: string | undefined = undefined;
342350

343-
// If there is no active editor, there is nothing to execute.
344-
const editor = editorService.activeTextEditorControl as IEditor;
345-
if (!editor) {
346-
return;
351+
// Determine if we're using a provided URI or the active editor
352+
let editor: IEditor | undefined;
353+
let model: ITextModel | undefined;
354+
let position: Position;
355+
let nextPosition: Position | undefined;
356+
357+
if (opts.uri) {
358+
// Use the provided URI to get the model
359+
const foundModel = modelService.getModel(opts.uri);
360+
if (!foundModel) {
361+
notificationService.notify({
362+
severity: Severity.Info,
363+
message: localize('positron.executeCode.noModel', "Cannot execute code. Unable to find document at {0}.", opts.uri.toString()),
364+
sticky: false
365+
});
366+
return;
367+
}
368+
model = foundModel;
369+
// Use the provided position (guaranteed to exist when uri is provided)
370+
position = opts.position;
371+
// No editor context when URI is provided
372+
editor = undefined;
373+
} else {
374+
// Use the active editor
375+
editor = editorService.activeTextEditorControl as IEditor;
376+
if (!editor) {
377+
return;
378+
}
379+
model = editor.getModel() as ITextModel;
380+
const editorPosition = editor.getPosition();
381+
if (!editorPosition) {
382+
return;
383+
}
384+
position = editorPosition;
347385
}
348386

349387
// Get the code to execute.
350-
const selection = editor.getSelection();
351-
const model = editor.getModel() as ITextModel;
388+
const selection = editor?.getSelection();
352389

353390
// If we have a selection and it isn't empty, then we use its contents (even if it
354391
// only contains whitespace or comments) and also retain the user's selection location.
@@ -367,13 +404,6 @@ export function registerPositronConsoleActions() {
367404
// HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK
368405
}
369406

370-
// Get the position of the cursor. If we don't have a selection, we'll use this to
371-
// determine the code to execute.
372-
const position = editor.getPosition();
373-
if (!position) {
374-
return;
375-
}
376-
377407
// Get all the statement range providers for the active document.
378408
const statementRangeProviders =
379409
languageFeaturesService.statementRangeProvider.all(model);
@@ -401,7 +431,7 @@ export function registerPositronConsoleActions() {
401431
code = isString(statementRange.code) ? statementRange.code : model.getValueInRange(statementRange.range);
402432

403433
if (advance) {
404-
await this.advanceStatement(model, editor, statementRange, statementRangeProviders[0], logService);
434+
nextPosition = await this.advanceStatement(model, editor, statementRange, statementRangeProviders[0], logService);
405435
}
406436
} else {
407437
// The statement range provider didn't return a range. This
@@ -412,8 +442,7 @@ export function registerPositronConsoleActions() {
412442
// If no selection was found, use the contents of the line containing the cursor
413443
// position.
414444
if (!isString(code)) {
415-
const position = editor.getPosition();
416-
let lineNumber = position?.lineNumber ?? 0;
445+
let lineNumber = position.lineNumber;
417446

418447
if (lineNumber > 0) {
419448
// Find the first non-empty line after the cursor position and read the
@@ -431,21 +460,23 @@ export function registerPositronConsoleActions() {
431460

432461
// If we have code and a position move the cursor to the next line with code on it,
433462
// or just to the next line if all additional lines are blank.
434-
if (advance && isString(code) && position) {
435-
this.advanceLine(model, editor, position, lineNumber, code, editorService);
463+
if (advance && isString(code)) {
464+
nextPosition = this.advanceLine(model, editor, position, lineNumber, code, editorService);
436465
}
437466

438-
if (!isString(code) && position && lineNumber === model.getLineCount()) {
467+
if (!isString(code) && lineNumber === model.getLineCount()) {
439468
// If we still don't have code and we are at the end of the document, add a
440469
// newline to the end of the document.
441470
this.amendNewlineToEnd(model);
442471

443472
// We don't move to that new line to avoid adding a bunch of empty
444473
// lines to the end. The edit operation typically moves us to the new line,
445474
// so we have to undo that.
446-
const newPosition = new Position(lineNumber, 1);
447-
editor.setPosition(newPosition);
448-
editor.revealPositionInCenterIfOutsideViewport(newPosition);
475+
if (editor) {
476+
const newPosition = new Position(lineNumber, 1);
477+
editor.setPosition(newPosition);
478+
editor.revealPositionInCenterIfOutsideViewport(newPosition);
479+
}
449480
}
450481

451482
// If we still don't have code after looking at the cursor position,
@@ -472,18 +503,18 @@ export function registerPositronConsoleActions() {
472503
mode: opts.mode,
473504
errorBehavior: opts.errorBehavior
474505
});
506+
return nextPosition;
475507
}
476508

477509
async advanceStatement(
478510
model: ITextModel,
479-
editor: IEditor,
511+
editor: IEditor | undefined,
480512
statementRange: IStatementRange,
481513
provider: StatementRangeProvider,
482514
logService: ILogService,
483-
) {
515+
): Promise<Position> {
484516

485-
// Move the cursor to the next
486-
// statement by creating a position on the line
517+
// Calculate the next position by creating a position on the line
487518
// following the statement and then invoking the
488519
// statement range provider again to find the appropriate
489520
// boundary of the next statement.
@@ -505,8 +536,6 @@ export function registerPositronConsoleActions() {
505536
model.getLineCount(),
506537
1
507538
);
508-
editor.setPosition(newPosition);
509-
editor.revealPositionInCenterIfOutsideViewport(newPosition);
510539
} else {
511540
// Invoke the statement range provider again to
512541
// find the appropriate boundary of the next statement.
@@ -548,20 +577,24 @@ export function registerPositronConsoleActions() {
548577
);
549578
}
550579
}
580+
}
551581

582+
// Only move the cursor if we have an editor
583+
if (editor) {
552584
editor.setPosition(newPosition);
553585
editor.revealPositionInCenterIfOutsideViewport(newPosition);
554586
}
587+
return newPosition;
555588
}
556589

557590
advanceLine(
558591
model: ITextModel,
559-
editor: IEditor,
592+
editor: IEditor | undefined,
560593
position: Position,
561594
lineNumber: number,
562595
code: string,
563596
editorService: IEditorService,
564-
) {
597+
): Position {
565598
// HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK
566599
// This attempts to address https://github.com/posit-dev/positron/issues/1177
567600
// by tacking a newline onto indented Python code fragments that end at an empty
@@ -598,8 +631,12 @@ export function registerPositronConsoleActions() {
598631
}
599632

600633
const newPosition = position.with(lineNumber, 0);
601-
editor.setPosition(newPosition);
602-
editor.revealPositionInCenterIfOutsideViewport(newPosition);
634+
// Only move the cursor if we have an editor
635+
if (editor) {
636+
editor.setPosition(newPosition);
637+
editor.revealPositionInCenterIfOutsideViewport(newPosition);
638+
}
639+
return newPosition;
603640
}
604641

605642
amendNewlineToEnd(model: ITextModel) {
@@ -1062,3 +1099,22 @@ export function registerPositronConsoleActions() {
10621099
}
10631100
});
10641101
}
1102+
1103+
1104+
/**
1105+
* Register the internal command for executing code in console from the extension API.
1106+
* This command is called by the positron.executeCodeInConsole API command.
1107+
*/
1108+
CommandsRegistry.registerCommand('_executeCodeInConsole', async (accessor, ...args: [string, URI, IPosition]) => {
1109+
const [languageId, uri, position] = args;
1110+
assertType(typeof languageId === 'string');
1111+
assertType(URI.isUri(uri));
1112+
assertType(Position.isIPosition(position));
1113+
1114+
const commandService = accessor.get(ICommandService);
1115+
return await commandService.executeCommand('workbench.action.positronConsole.executeCode', {
1116+
languageId,
1117+
uri,
1118+
position: Position.lift(position)
1119+
});
1120+
});

0 commit comments

Comments
 (0)