diff --git a/extensions/positron-r/src/session.ts b/extensions/positron-r/src/session.ts index 5d09fba0059c..c82b69ae9692 100644 --- a/extensions/positron-r/src/session.ts +++ b/extensions/positron-r/src/session.ts @@ -203,9 +203,15 @@ export class RSession implements positron.LanguageRuntimeSession, vscode.Disposa throw new Error(`Debugging is not supported in R sessions`); } - execute(code: string, id: string, mode: positron.RuntimeCodeExecutionMode, errorBehavior: positron.RuntimeErrorBehavior): void { + execute( + code: string, + id: string, + mode: positron.RuntimeCodeExecutionMode, + errorBehavior: positron.RuntimeErrorBehavior, + codeLocation?: vscode.Location, + ): void { if (this._kernel) { - this._kernel.execute(code, id, mode, errorBehavior); + this._kernel.execute(code, id, mode, errorBehavior, codeLocation); } else { throw new Error(`Cannot execute '${code}'; kernel not started`); } diff --git a/extensions/positron-supervisor/src/KallichoreSession.ts b/extensions/positron-supervisor/src/KallichoreSession.ts index c8ddc486c14b..05bfe7401d60 100644 --- a/extensions/positron-supervisor/src/KallichoreSession.ts +++ b/extensions/positron-supervisor/src/KallichoreSession.ts @@ -8,6 +8,7 @@ import * as positron from 'positron'; import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; +import * as typesConverters from './jupyter/TypesConverters'; import { CommBackendMessage, JupyterKernelExtra, JupyterKernelSpec, JupyterLanguageRuntimeSession, JupyterSession, Comm } from './positron-supervisor'; import { ActiveSession, ConnectionInfo, DefaultApi, InterruptMode, NewSession, RestartSession, StartupEnvironment, Status, VarAction, VarActionType } from './kcclient/api'; import { JupyterMessage } from './jupyter/JupyterMessage'; @@ -693,10 +694,13 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { * @param mode The execution mode * @param errorBehavior What to do if an error occurs */ - execute(code: string, + execute( + code: string, id: string, mode: positron.RuntimeCodeExecutionMode, - errorBehavior: positron.RuntimeErrorBehavior): void { + errorBehavior: positron.RuntimeErrorBehavior, + codeLocation?: vscode.Location, + ): void { // Translate the parameters into a Jupyter execute request const request: JupyterExecuteRequest = { @@ -708,6 +712,10 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { stop_on_error: errorBehavior === positron.RuntimeErrorBehavior.Stop, }; + if (codeLocation) { + request.positron = { code_location: typesConverters.JupyterPositronLocation.from(codeLocation, code) }; + } + // Create and send the execute request const execute = new ExecuteRequest(id, request); this.sendRequest(execute).then((reply) => { diff --git a/extensions/positron-supervisor/src/jupyter/ExecuteRequest.ts b/extensions/positron-supervisor/src/jupyter/ExecuteRequest.ts index e9c06e405075..7733c1e59369 100644 --- a/extensions/positron-supervisor/src/jupyter/ExecuteRequest.ts +++ b/extensions/positron-supervisor/src/jupyter/ExecuteRequest.ts @@ -6,6 +6,7 @@ import { JupyterChannel } from './JupyterChannel'; import { JupyterDisplayData } from './JupyterDisplayData'; import { JupyterMessageType } from './JupyterMessageType.js'; +import { JupyterPositronLocation, JupyterPositronRange } from './JupyterPositronTypes'; import { JupyterRequest } from './JupyterRequest'; @@ -41,6 +42,14 @@ export interface JupyterExecuteRequest { /** Whether the kernel should stop the execution queue when an error occurs */ stop_on_error: boolean; + + /** Positron extension */ + positron?: JupyterPositronExecuteRequest; +} + +export interface JupyterPositronExecuteRequest { + /** Location of `code`. Encoded in UTF-16 offsets. */ + code_location?: JupyterPositronLocation; } /** diff --git a/extensions/positron-supervisor/src/jupyter/JupyterPositronTypes.ts b/extensions/positron-supervisor/src/jupyter/JupyterPositronTypes.ts new file mode 100644 index 000000000000..f861f7205aca --- /dev/null +++ b/extensions/positron-supervisor/src/jupyter/JupyterPositronTypes.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface JupyterPositronLocation { + uri: string; + range: JupyterPositronRange; +} + +export interface JupyterPositronRange { + start: JupyterPositronPosition; + end: JupyterPositronPosition; +} + +// See https://jupyter-client.readthedocs.io/en/stable/messaging.html#cursor-pos-unicode-note +// regarding choice of offset in unicode points +export interface JupyterPositronPosition { + line: number; + /** Column offset in unicode points */ + character: number; +} diff --git a/extensions/positron-supervisor/src/jupyter/TypesConverters.ts b/extensions/positron-supervisor/src/jupyter/TypesConverters.ts new file mode 100644 index 000000000000..df235b09570c --- /dev/null +++ b/extensions/positron-supervisor/src/jupyter/TypesConverters.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as PositronTypes from './JupyterPositronTypes'; + +export namespace JupyterPositronLocation { + export function from(location: vscode.Location, text: string): PositronTypes.JupyterPositronLocation { + return { + uri: location.uri.toString(), + range: JupyterPositronRange.from(location.range, text), + }; + } +} + +export namespace JupyterPositronRange { + export function from(range: vscode.Range, text: string): PositronTypes.JupyterPositronRange { + return { + start: JupyterPositronPosition.from(range.start, text), + end: JupyterPositronPosition.from(range.end, text), + }; + } +} + +export namespace JupyterPositronPosition { + export function from(position: vscode.Position, text: string): PositronTypes.JupyterPositronPosition { + return { + line: position.line, + character: codePointOffsetFromUtf16Index(text, position.character), + }; + } +} + + +export function codePointOffsetFromUtf16Index(text: string, utf16Index: number): number { + if (utf16Index <= 0) { + return 0; + } + + let offset = 0; + let i = 0; + + while (i < text.length && i < utf16Index) { + const codePoint = text.codePointAt(i); + if (codePoint === undefined) { + break; + } + + // Advance by 2 for surrogate pairs (code points > 0xFFFF), 1 otherwise + i += codePoint > 0xFFFF ? 2 : 1; + + // Only count this code point if we haven't passed the target index + if (i <= utf16Index) { + ++offset; + } + } + + return offset; +} diff --git a/extensions/positron-supervisor/src/test/typesConverters.test.ts b/extensions/positron-supervisor/src/test/typesConverters.test.ts new file mode 100644 index 000000000000..9a31daddadd7 --- /dev/null +++ b/extensions/positron-supervisor/src/test/typesConverters.test.ts @@ -0,0 +1,269 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { codePointOffsetFromUtf16Index, JupyterPositronPosition, JupyterPositronRange, JupyterPositronLocation } from '../jupyter/TypesConverters'; + +suite('TypesConverters', () => { + suite('codePointOffsetFromUtf16Index', () => { + test('Empty string', () => { + assert.strictEqual(codePointOffsetFromUtf16Index('', 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index('', 5), 0); + }); + + test('Negative index', () => { + assert.strictEqual(codePointOffsetFromUtf16Index('hello', -1), 0); + assert.strictEqual(codePointOffsetFromUtf16Index('hello', -10), 0); + }); + + test('Zero index', () => { + assert.strictEqual(codePointOffsetFromUtf16Index('hello', 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index('😀', 0), 0); + }); + + test('ASCII text', () => { + const text = 'hello'; + assert.strictEqual(codePointOffsetFromUtf16Index(text, 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 1), 1); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 2), 2); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 3), 3); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 4), 4); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 5), 5); + }); + + test('Index beyond string length', () => { + const text = 'hi'; + assert.strictEqual(codePointOffsetFromUtf16Index(text, 10), 2); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 100), 2); + }); + + test('Single emoji', () => { + const text = '😀'; + // '😀' is 2 UTF-16 units, 1 code point + assert.strictEqual(text.length, 2, 'UTF-16 length should be 2'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 1), 0, 'Index at high surrogate should not count emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 2), 1, 'Index after emoji should count it'); + }); + + test('Emoji at start', () => { + const text = '😀abc'; + // '😀' = 2 units, then 3 ASCII chars + assert.strictEqual(text.length, 5); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 1), 0, 'Middle of emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 2), 1, 'After emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 3), 2, 'After emoji + 1 char'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 4), 3); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 5), 4); + }); + + test('Emoji at end', () => { + const text = 'abc😀'; + assert.strictEqual(text.length, 5); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 1), 1); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 2), 2); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 3), 3, 'Before emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 4), 3, 'Middle of emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 5), 4, 'After emoji'); + }); + + test('Emoji in middle', () => { + const text = 'a😀b'; + assert.strictEqual(text.length, 4); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 1), 1, 'After a'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 2), 1, 'Middle of emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 3), 2, 'After emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 4), 3, 'After b'); + }); + + test('Multiple emojis', () => { + const text = '😀😁😂'; + // Each emoji is 2 UTF-16 units + assert.strictEqual(text.length, 6); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 1), 0, 'Middle of first emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 2), 1, 'After first emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 3), 1, 'Middle of second emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 4), 2, 'After second emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 5), 2, 'Middle of third emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 6), 3, 'After third emoji'); + }); + + test('Mixed ASCII and emojis', () => { + const text = 'Hi😀!'; + // H=1, i=1, 😀=2, !=1 => 5 UTF-16 units, 4 code points + assert.strictEqual(text.length, 5); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 1), 1); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 2), 2, 'After "Hi"'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 3), 2, 'Middle of emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 4), 3, 'After emoji'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 5), 4, 'After !'); + }); + + test('Non-BMP characters (Chinese)', () => { + // U+20000 is a CJK Ideograph Extension B character (surrogate pair) + const text = '\u{20000}ab'; + assert.strictEqual(text.length, 4, '2 for surrogate pair + 2 ASCII'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 1), 0, 'Middle of surrogate pair'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 2), 1, 'After surrogate pair'); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 3), 2); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 4), 3); + }); + + test('BMP special characters', () => { + // These are within BMP (1 UTF-16 unit each) + const text = '€£¥'; + assert.strictEqual(text.length, 3); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 0), 0); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 1), 1); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 2), 2); + assert.strictEqual(codePointOffsetFromUtf16Index(text, 3), 3); + }); + }); + + suite('JupyterPositronPosition', () => { + test('ASCII text at line start', () => { + const position = new vscode.Position(0, 0); + const text = 'hello world'; + const result = JupyterPositronPosition.from(position, text); + + assert.strictEqual(result.line, 0); + assert.strictEqual(result.character, 0); + }); + + test('ASCII text mid-line', () => { + const position = new vscode.Position(0, 5); + const text = 'hello world'; + const result = JupyterPositronPosition.from(position, text); + + assert.strictEqual(result.line, 0); + assert.strictEqual(result.character, 5); + }); + + test('Line number is preserved', () => { + const position = new vscode.Position(10, 5); + const text = 'hello'; + const result = JupyterPositronPosition.from(position, text); + + assert.strictEqual(result.line, 10); + assert.strictEqual(result.character, 5); + }); + + test('Text with emoji - position after emoji', () => { + const position = new vscode.Position(0, 2); + const text = '😀a'; + const result = JupyterPositronPosition.from(position, text); + + assert.strictEqual(result.line, 0); + assert.strictEqual(result.character, 1, 'Should count emoji as 1 code point'); + }); + + test('Text with emoji - position in middle of emoji', () => { + const position = new vscode.Position(0, 1); + const text = '😀a'; + const result = JupyterPositronPosition.from(position, text); + + assert.strictEqual(result.line, 0); + assert.strictEqual(result.character, 0, 'Should not count partial emoji'); + }); + + test('Text with multiple emojis', () => { + const position = new vscode.Position(0, 4); + const text = '😀😁ab'; + const result = JupyterPositronPosition.from(position, text); + + assert.strictEqual(result.line, 0); + assert.strictEqual(result.character, 2, 'Should count 2 emojis as 2 code points'); + }); + }); + + suite('JupyterPositronRange', () => { + test('ASCII text range', () => { + const range = new vscode.Range( + new vscode.Position(0, 0), + new vscode.Position(0, 5) + ); + const text = 'hello world'; + const result = JupyterPositronRange.from(range, text); + + assert.strictEqual(result.start.line, 0); + assert.strictEqual(result.start.character, 0); + assert.strictEqual(result.end.line, 0); + assert.strictEqual(result.end.character, 5); + }); + + test('Range with emoji', () => { + const range = new vscode.Range( + new vscode.Position(0, 0), + new vscode.Position(0, 4) + ); + const text = '😀😁'; + const result = JupyterPositronRange.from(range, text); + + assert.strictEqual(result.start.line, 0); + assert.strictEqual(result.start.character, 0); + assert.strictEqual(result.end.line, 0); + assert.strictEqual(result.end.character, 2, 'Should count 2 emojis as 2 code points'); + }); + + test('Multi-line range', () => { + const range = new vscode.Range( + new vscode.Position(1, 2), + new vscode.Position(3, 4) + ); + const text = 'test'; + const result = JupyterPositronRange.from(range, text); + + assert.strictEqual(result.start.line, 1); + assert.strictEqual(result.start.character, 2); + assert.strictEqual(result.end.line, 3); + assert.strictEqual(result.end.character, 4); + }); + }); + + suite('JupyterPositronLocation', () => { + + test('Location with file URI', () => { + const uri = vscode.Uri.file('/path/to/file.txt'); + const range = new vscode.Range( + new vscode.Position(0, 0), + new vscode.Position(0, 5) + ); + const location = new vscode.Location(uri, range); + const text = 'hello'; + const result = JupyterPositronLocation.from(location, text); + + assert.ok(result.uri.includes('file')); + assert.ok(result.uri.includes('file.txt')); + assert.strictEqual(result.range.start.line, 0); + assert.strictEqual(result.range.start.character, 0); + assert.strictEqual(result.range.end.line, 0); + assert.strictEqual(result.range.end.character, 5); + }); + + test('Location with emoji in text', () => { + const uri = vscode.Uri.file('/test.txt'); + const range = new vscode.Range( + new vscode.Position(0, 0), + new vscode.Position(0, 3) + ); + const location = new vscode.Location(uri, range); + const text = '😀a'; + const result = JupyterPositronLocation.from(location, text); + + assert.ok(result.uri.includes('test.txt')); + assert.strictEqual(result.range.start.line, 0); + assert.strictEqual(result.range.start.character, 0); + assert.strictEqual(result.range.end.line, 0); + assert.strictEqual(result.range.end.character, 2, 'Should count emoji + a as 2 code points'); + }); + }); +}); diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 3ce162222c48..3c868eaafe7b 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -1079,12 +1079,16 @@ declare module 'positron' { * @param id The ID of the code * @param mode The code execution mode * @param errorBehavior The code execution error behavior + * @param location Optionally, the location of `code` in the source editor. * Note: The errorBehavior parameter is currently ignored by kernels */ - execute(code: string, + execute( + code: string, id: string, mode: RuntimeCodeExecutionMode, - errorBehavior: RuntimeErrorBehavior): void; + errorBehavior: RuntimeErrorBehavior, + codeLocation?: vscode.Location, + ): void; /** * Shut down the runtime; returns a Thenable that resolves when the diff --git a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts index b5fe993cb335..0222da98c8fb 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadLanguageRuntime.ts @@ -52,6 +52,7 @@ import { IPositronVariablesInstance } from '../../../services/positronVariables/ import { isWebviewPreloadMessage, isWebviewReplayMessage } from '../../../services/positronIPyWidgets/common/webviewPreloadUtils.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { ActiveRuntimeSessionMetadata, LanguageRuntimeDynState } from 'positron'; +import { Location } from '../../../../editor/common/languages.js'; /** * Represents a language runtime event (for example a message or state change) @@ -461,9 +462,24 @@ class ExtHostLanguageRuntimeSessionAdapter extends Disposable implements ILangua return this._proxy.$openResource(this.handle, resource); } - execute(code: string, id: string, mode: RuntimeCodeExecutionMode, errorBehavior: RuntimeErrorBehavior): void { + execute( + code: string, + id: string, + mode: RuntimeCodeExecutionMode, + errorBehavior: RuntimeErrorBehavior, + attribution?: IConsoleCodeAttribution, + ): void { this._lastUsed = Date.now(); - this._proxy.$executeCode(this.handle, code, id, mode, errorBehavior); + + let codeLocation: Location | undefined = undefined; + + // For now we only provide source locations for scripts, but we might be + // able to provide it for notebooks as well + if (attribution?.source === CodeAttributionSource.Script) { + codeLocation = attribution.metadata?.codeLocation; + } + + this._proxy.$executeCode(this.handle, code, id, mode, errorBehavior, codeLocation); } isCodeFragmentComplete(code: string): Thenable { diff --git a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts index 266cf1a95163..d47d1d7798d6 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts @@ -19,6 +19,7 @@ import { PlotRenderSettings } from '../../../services/positronPlots/common/posit import { QueryTableSummaryResult, Variable } from '../../../services/languageRuntime/common/positronVariablesComm.js'; import { ILanguageRuntimeCodeExecutedEvent } from '../../../services/positronConsole/common/positronConsoleCodeExecution.js'; import { IPositronChatProvider } from '../../../contrib/chat/common/languageModels.js'; +import { Location } from '../../../../editor/common/languages.js'; // NOTE: This check is really to ensure that extHost.protocol is included by the TypeScript compiler // as a dependency of this module, and therefore that it's initialized first. This is to avoid a @@ -76,7 +77,7 @@ export interface ExtHostLanguageRuntimeShape { $disposeLanguageRuntime(handle: number): Promise; $startLanguageRuntime(handle: number): Promise; $openResource(handle: number, resource: URI | string): Promise; - $executeCode(handle: number, code: string, id: string, mode: RuntimeCodeExecutionMode, errorBehavior: RuntimeErrorBehavior, executionId?: string): void; + $executeCode(handle: number, code: string, id: string, mode: RuntimeCodeExecutionMode, errorBehavior: RuntimeErrorBehavior, codeLocation?: Location, executionId?: string): void; $isCodeFragmentComplete(handle: number, code: string): Promise; $createClient(handle: number, id: string, type: RuntimeClientType, params: any, metadata?: any): Promise; $listClients(handle: number, type?: RuntimeClientType): Promise>; diff --git a/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts b/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts index b9349677c579..dcbeabca2bf2 100644 --- a/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts +++ b/src/vs/workbench/api/common/positron/extHostLanguageRuntime.ts @@ -7,6 +7,7 @@ import type * as positron from 'positron'; import { debounce } from '../../../../base/common/decorators.js'; import { ILanguageRuntimeMessage, ILanguageRuntimeMessageCommClosed, ILanguageRuntimeMessageCommData, ILanguageRuntimeMessageCommOpen, ILanguageRuntimeMessageStream, ILanguageRuntimeMessageOutput, ILanguageRuntimeMessageState, ILanguageRuntimeMetadata, LanguageRuntimeSessionMode, RuntimeCodeExecutionMode, RuntimeCodeFragmentStatus, RuntimeErrorBehavior, RuntimeState, ILanguageRuntimeMessageResult, ILanguageRuntimeMessageError, RuntimeOnlineState } from '../../../services/languageRuntime/common/languageRuntimeService.js'; import * as extHostProtocol from './extHost.positron.protocol.js'; +import * as typeConverters from '../extHostTypeConverters.js'; import { Emitter } from '../../../../base/common/event.js'; import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { Disposable, LanguageRuntimeMessageType } from '../extHostTypes.js'; @@ -23,6 +24,7 @@ import { generateUuid } from '../../../../base/common/uuid.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { QueryTableSummaryResult, Variable } from '../../../services/languageRuntime/common/positronVariablesComm.js'; import { ILanguageRuntimeCodeExecutedEvent } from '../../../services/positronConsole/common/positronConsoleCodeExecution.js'; +import { Location } from '../../../../editor/common/languages.js'; /** * Interface for code execution observers @@ -774,11 +776,17 @@ export class ExtHostLanguageRuntime implements extHostProtocol.ExtHostLanguageRu return Promise.resolve(this._runtimeSessions[handle].openResource!(resource)); } - $executeCode(handle: number, code: string, id: string, mode: RuntimeCodeExecutionMode, errorBehavior: RuntimeErrorBehavior): void { + $executeCode(handle: number, code: string, id: string, mode: RuntimeCodeExecutionMode, errorBehavior: RuntimeErrorBehavior, codeLocation?: Location): void { if (handle >= this._runtimeSessions.length) { throw new Error(`Cannot execute code: session handle '${handle}' not found or no longer valid.`); } - this._runtimeSessions[handle].execute(code, id, mode, errorBehavior); + + let codeLocationConverted = undefined; + if (codeLocation) { + codeLocationConverted = typeConverters.Location.to(codeLocation); + } + + this._runtimeSessions[handle].execute(code, id, mode, errorBehavior, codeLocationConverted); } $isCodeFragmentComplete(handle: number, code: string): Promise { diff --git a/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts b/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts index f912f19929c9..e3a34a7d0b0e 100644 --- a/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts +++ b/src/vs/workbench/contrib/positronConsole/browser/positronConsoleActions.ts @@ -8,7 +8,7 @@ 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 { IRange, Range } 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'; @@ -24,7 +24,7 @@ import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/c import { IViewsService } from '../../../services/views/common/viewsService.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IStatementRange, StatementRangeProvider } from '../../../../editor/common/languages.js'; +import { IStatementRange, StatementRangeProvider, Location } from '../../../../editor/common/languages.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; @@ -79,8 +79,8 @@ const trimNewlines = (str: string) => str.replace(/^\n+|\n+$/g, ''); */ async function executeCodeInConsole( code: string, - position: Position, - model: ITextModel, + cursorLocation: Location, + codeLocation: Location | undefined, services: { editorService: IEditorService; languageService: ILanguageService; @@ -114,11 +114,8 @@ async function executeCodeInConsole( const attribution: IConsoleCodeAttribution = { source: CodeAttributionSource.Script, metadata: { - file: model.uri.path, - position: { - line: position.lineNumber, - column: position.column - }, + cursorLocation, + codeLocation, } }; @@ -387,10 +384,14 @@ export function registerPositronConsoleActions() { // Get the code to execute. const selection = editor?.getSelection(); + // Track the source and range of the executed code + let codeLocation: Location | undefined = undefined; + // 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. if (selection && !selection.isEmpty()) { code = model.getValueInRange(selection); + codeLocation = { uri: model.uri, range: selection }; // 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 multiline, indented Python code fragments. This allows @@ -429,6 +430,7 @@ export function registerPositronConsoleActions() { // range provider returns, even if it is an empty string, as it should have // returned `undefined` if it didn't think it was important. code = isString(statementRange.code) ? statementRange.code : model.getValueInRange(statementRange.range); + codeLocation = { uri: model.uri, range: statementRange.range }; if (advance) { nextPosition = await this.advanceStatement(model, editor, statementRange, statementRangeProviders[0], logService); @@ -486,11 +488,16 @@ export function registerPositronConsoleActions() { } } + const cursorLocation: Location = { + uri: model.uri, + range: Range.fromPositions(position, position), + }; + // Use the helper function to execute the code await executeCodeInConsole( code, - position, - model, + cursorLocation, + codeLocation, { editorService, languageService, @@ -770,11 +777,16 @@ export function registerPositronConsoleActions() { return; } + const cursorLocation: Location = { + uri: model.uri, + range: Range.fromPositions(position, position), + }; + // Use the helper function to execute the code await executeCodeInConsole( code, - position, - model, + cursorLocation, + undefined, { editorService, languageService, diff --git a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts index 77c238b61ad8..0f484e2a0ef5 100644 --- a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts @@ -3048,7 +3048,9 @@ class PositronConsoleInstance extends Disposable implements IPositronConsoleInst code, id, mode, - errorBehavior); + errorBehavior, + attribution, + ); // Create and fire the onDidExecuteCode event. const event: ILanguageRuntimeCodeExecutedEvent = { diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts index 79ce38894c53..c1e6399cfa59 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSessionService.ts @@ -11,6 +11,7 @@ import { ILanguageRuntimeMetadata, LanguageRuntimeSessionMode, ILanguageRuntimeS import { RuntimeClientType, IRuntimeClientInstance } from '../../languageRuntime/common/languageRuntimeClientInstance.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { ActiveRuntimeSession } from './activeRuntimeSession.js'; +import { IConsoleCodeAttribution } from '../../positronConsole/common/positronConsoleCodeExecution.js'; export const IRuntimeSessionService = createDecorator('runtimeSessionService'); @@ -179,10 +180,13 @@ export interface ILanguageRuntimeSession extends IDisposable { openResource(resource: URI | string): Thenable; /** Execute code in the runtime */ - execute(code: string, + execute( + code: string, id: string, mode: RuntimeCodeExecutionMode, - errorBehavior: RuntimeErrorBehavior): void; + errorBehavior: RuntimeErrorBehavior, + attribution?: IConsoleCodeAttribution, + ): void; /** Test a code fragment for completeness */ isCodeFragmentComplete(code: string): Thenable;