-
Notifications
You must be signed in to change notification settings - Fork 126
Add new command for positron.executeCodeInConsole to be used by extensions that handles uri, position, next position
#10580
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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'; | ||
|
|
||
|
|
@@ -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 } | ||
| ) = {} | ||
| ): 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); | ||
|
|
||
|
|
@@ -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; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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); | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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, | ||
|
|
@@ -472,18 +503,18 @@ export function registerPositronConsoleActions() { | |
| mode: opts.mode, | ||
| errorBehavior: opts.errorBehavior | ||
| }); | ||
| return nextPosition; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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. | ||
|
|
@@ -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 | ||
|
|
@@ -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) { | ||
|
|
@@ -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]) => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This command is registered here so we can make one in |
||
| 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) | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
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.