diff --git a/src/dotnet-interactive-npm/package-lock.json b/src/dotnet-interactive-npm/package-lock.json index 2254d7f792..fbd5282944 100644 --- a/src/dotnet-interactive-npm/package-lock.json +++ b/src/dotnet-interactive-npm/package-lock.json @@ -1,5 +1,5 @@ { - "name": "dotnet-interactive", + "name": "@microsoft/dotnet-interactive", "version": "42.42.42", "lockfileVersion": 1, "requires": true, diff --git a/src/dotnet-interactive-vscode/common/clientMapper.ts b/src/dotnet-interactive-vscode/common/clientMapper.ts index 9683c082a9..deb59b0727 100644 --- a/src/dotnet-interactive-vscode/common/clientMapper.ts +++ b/src/dotnet-interactive-vscode/common/clientMapper.ts @@ -2,24 +2,30 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { KernelTransport } from './interfaces/contracts'; -import { InteractiveClient } from "./interactiveClient"; +import { ErrorOutputCreator, InteractiveClient } from "./interactiveClient"; import { ReportChannel, Uri } from "./interfaces/vscode-like"; +export interface ClientMapperConfiguration { + kernelTransportCreator: (notebookUri: Uri) => Promise, + createErrorOutput: ErrorOutputCreator, + diagnosticChannel?: ReportChannel, +} + export class ClientMapper { private clientMap: Map> = new Map(); private clientCreationCallbackMap: Map Promise> = new Map(); - constructor(readonly kernelTransportCreator: (notebookPath: string) => Promise, readonly diagnosticChannel?: ReportChannel) { + constructor(readonly config: ClientMapperConfiguration) { } private writeDiagnosticMessage(message: string) { - if (this.diagnosticChannel) { - this.diagnosticChannel.appendLine(message); + if (this.config.diagnosticChannel) { + this.config.diagnosticChannel.appendLine(message); } } static keyFromUri(uri: Uri): string { - return uri.fsPath; + return uri.toString(); } tryGetClient(uri: Uri): Promise { @@ -41,8 +47,12 @@ export class ClientMapper { this.writeDiagnosticMessage(`creating client for '${key}'`); clientPromise = new Promise(async (resolve, reject) => { try { - const transport = await this.kernelTransportCreator(uri.fsPath); - const client = new InteractiveClient(transport); + const transport = await this.config.kernelTransportCreator(uri); + const config = { + transport, + createErrorOutput: this.config.createErrorOutput, + }; + const client = new InteractiveClient(config); let onCreate = this.clientCreationCallbackMap.get(key); if (onCreate) { @@ -98,6 +108,7 @@ export class ClientMapper { } isDotNetClient(uri: Uri): boolean { - return this.clientMap.has(uri.fsPath); + const key = ClientMapper.keyFromUri(uri); + return this.clientMap.has(key); } } diff --git a/src/dotnet-interactive-vscode/common/interactiveClient.ts b/src/dotnet-interactive-vscode/common/interactiveClient.ts index 6b7b73497b..07da86e15b 100644 --- a/src/dotnet-interactive-vscode/common/interactiveClient.ts +++ b/src/dotnet-interactive-vscode/common/interactiveClient.ts @@ -52,10 +52,19 @@ import { Cancel } from './interfaces/contracts'; import { Eol } from './interfaces'; -import { clearDebounce, createErrorOutput, createOutput } from './utilities'; +import { clearDebounce, createOutput } from './utilities'; import * as vscodeLike from './interfaces/vscode-like'; +export interface ErrorOutputCreator { + (message: string, outputId?: string): vscodeLike.NotebookCellOutput; +} + +export interface InteractiveClientConfiguration { + readonly transport: KernelTransport, + readonly createErrorOutput: ErrorOutputCreator, +} + export class InteractiveClient { private nextOutputId: number = 1; private nextToken: number = 1; @@ -63,13 +72,13 @@ export class InteractiveClient { private deferredOutput: Array = []; private valueIdMap: Map, observer: { (outputs: Array): void } }> = new Map, observer: { (outputs: Array): void } }>(); - constructor(readonly kernelTransport: KernelTransport) { - kernelTransport.subscribeToKernelEvents(eventEnvelope => this.eventListener(eventEnvelope)); + constructor(readonly config: InteractiveClientConfiguration) { + config.transport.subscribeToKernelEvents(eventEnvelope => this.eventListener(eventEnvelope)); } public tryGetProperty(propertyName: string): T | null { try { - return ((this.kernelTransport)[propertyName]); + return ((this.config.transport)[propertyName]); } catch { return null; @@ -134,7 +143,7 @@ export class InteractiveClient { case CommandFailedType: { const err = eventEnvelope.event; - const errorOutput = createErrorOutput(err.message, this.getNextOutputId()); + const errorOutput = this.config.createErrorOutput(err.message, this.getNextOutputId()); outputs.push(errorOutput); reportOutputs(); reject(err); @@ -190,7 +199,7 @@ export class InteractiveClient { break; } }, configuration?.token).catch(e => { - const errorOutput = createErrorOutput('' + e, this.getNextOutputId()); + const errorOutput = this.config.createErrorOutput('' + e, this.getNextOutputId()); outputs.push(errorOutput); reportOutputs(); reject(e); @@ -251,7 +260,7 @@ export class InteractiveClient { }; token = token || this.getNextToken(); let disposable = this.subscribeToKernelTokenEvents(token, observer); - await this.kernelTransport.submitCommand(command, SubmitCodeType, token); + await this.config.transport.submitCommand(command, SubmitCodeType, token); return disposable; } @@ -266,7 +275,7 @@ export class InteractiveClient { } dispose() { - this.kernelTransport.dispose(); + this.config.transport.dispose(); } private submitCommandAndGetResult(command: KernelCommand, commandType: KernelCommandType, expectedEventType: KernelEventType, token: string | undefined): Promise { @@ -300,7 +309,7 @@ export class InteractiveClient { break; } }); - await this.kernelTransport.submitCommand(command, commandType, token); + await this.config.transport.submitCommand(command, commandType, token); }); } @@ -322,7 +331,7 @@ export class InteractiveClient { break; } }); - await this.kernelTransport.submitCommand(command, commandType, token); + await this.config.transport.submitCommand(command, commandType, token); }); } diff --git a/src/dotnet-interactive-vscode/common/interactiveNotebook.ts b/src/dotnet-interactive-vscode/common/interactiveNotebook.ts index 173c79b251..1b3ae6a513 100644 --- a/src/dotnet-interactive-vscode/common/interactiveNotebook.ts +++ b/src/dotnet-interactive-vscode/common/interactiveNotebook.ts @@ -41,8 +41,10 @@ export function isDotnetInteractiveLanguage(language: string): boolean { return language.startsWith(notebookLanguagePrefix); } +export const jupyterViewType = 'jupyter-notebook'; + export function isJupyterNotebookViewType(viewType: string): boolean { - return viewType === 'jupyter-notebook'; + return viewType === jupyterViewType; } export function languageToCellKind(language: string): NotebookCellKind { diff --git a/src/dotnet-interactive-vscode/common/interfaces/vscode-like.ts b/src/dotnet-interactive-vscode/common/interfaces/vscode-like.ts index b362311773..5cb7a70755 100644 --- a/src/dotnet-interactive-vscode/common/interfaces/vscode-like.ts +++ b/src/dotnet-interactive-vscode/common/interfaces/vscode-like.ts @@ -8,17 +8,18 @@ export enum NotebookCellKind { Code = 2 } -export const ErrorOutputMimeType = 'application/x.notebook.error-traceback'; +export const ErrorOutputMimeType = 'application/vnd.code.notebook.error'; export interface NotebookCellOutputItem { readonly mime: string; - readonly value: unknown; - readonly metadata?: Record; + readonly value: Uint8Array | unknown; + readonly metadata?: { [key: string]: any }; } export interface NotebookCellOutput { - readonly id: string; - readonly outputs: NotebookCellOutputItem[]; + id: string; + outputs: NotebookCellOutputItem[]; + metadata?: { [key: string]: any }; } export enum NotebookCellRunState { @@ -45,6 +46,7 @@ export interface NotebookCellMetadata { export interface Uri { fsPath: string; + scheme: string; toString: () => string; } diff --git a/src/dotnet-interactive-vscode/common/ipynbUtilities.ts b/src/dotnet-interactive-vscode/common/ipynbUtilities.ts index 9a71bbcf00..64b1b58bfa 100644 --- a/src/dotnet-interactive-vscode/common/ipynbUtilities.ts +++ b/src/dotnet-interactive-vscode/common/ipynbUtilities.ts @@ -63,7 +63,7 @@ export function getLanguageInfoMetadata(metadata: any): LanguageInfoMetadata { return languageMetadata; } -function mapIpynbLanguageName(name: string | undefined): string | undefined { +export function mapIpynbLanguageName(name: string | undefined): string | undefined { if (name) { // The .NET Interactive Jupyter kernel serializes the language names as "C#", "F#", and "PowerShell"; these // need to be normalized to .NET Interactive kernel language names. diff --git a/src/dotnet-interactive-vscode/common/tests/unit/client.test.ts b/src/dotnet-interactive-vscode/common/tests/unit/client.test.ts index 2e6a1345f4..759ab95cf4 100644 --- a/src/dotnet-interactive-vscode/common/tests/unit/client.test.ts +++ b/src/dotnet-interactive-vscode/common/tests/unit/client.test.ts @@ -10,15 +10,16 @@ import { ClientMapper } from '../../clientMapper'; import { TestKernelTransport } from './testKernelTransport'; import { CallbackTestKernelTransport } from './callbackTestKernelTransport'; import { CodeSubmissionReceivedType, CompleteCodeSubmissionReceivedType, CommandSucceededType, DisplayedValueProducedType, ReturnValueProducedType, DisplayedValueUpdatedType, CommandFailedType } from '../../interfaces/contracts'; -import { debounce, wait } from '../../utilities'; +import { createUri, debounce, wait } from '../../utilities'; import * as vscodeLike from '../../interfaces/vscode-like'; +import { createKernelTransportConfig, decodeNotebookCellOutputs } from './utilities'; describe('InteractiveClient tests', () => { it('command execution returns deferred events', async () => { - let token = 'test-token'; - let code = '1 + 1'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = 'test-token'; + const code = '1 + 1'; + const config = createKernelTransportConfig(async (notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { // deferred event; unassociated with the original submission; has its own token @@ -70,7 +71,8 @@ describe('InteractiveClient tests', () => { } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); let result: Array = []; await client.execute(code, 'csharp', outputs => result = outputs, _ => { }, { token }); expect(result).to.deep.equal([ @@ -96,9 +98,9 @@ describe('InteractiveClient tests', () => { }); it('deferred events do not interfere with display update events', async () => { - let token = 'test-token'; - let code = '1 + 1'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = 'test-token'; + const code = '1 + 1'; + const config = createKernelTransportConfig(async (notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { // deferred event; unassociated with the original submission; has its own token @@ -150,7 +152,8 @@ describe('InteractiveClient tests', () => { } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); let result: Array = []; await client.execute(code, 'csharp', outputs => result = outputs, _ => { }, { token }); expect(result).to.deep.equal([ @@ -176,9 +179,9 @@ describe('InteractiveClient tests', () => { }); it('interleaved deferred events do not interfere with display update events', async () => { - let token = 'test-token'; - let code = '1 + 1'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = 'test-token'; + const code = '1 + 1'; + const config = createKernelTransportConfig(async (notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { // deferred event; unassociated with the original submission; has its own token @@ -245,7 +248,8 @@ describe('InteractiveClient tests', () => { } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); let result: Array = []; await client.execute(code, 'csharp', outputs => result = outputs, _ => { }, { token }); expect(result).to.deep.equal([ @@ -280,8 +284,8 @@ describe('InteractiveClient tests', () => { }); it('display update events from separate submissions trigger the correct observer', async () => { - let code = '1 + 1'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const code = '1 + 1'; + const config = createKernelTransportConfig(async (notebookPath) => new TestKernelTransport({ 'SubmitCode#1': [ { eventType: DisplayedValueProducedType, @@ -325,7 +329,8 @@ describe('InteractiveClient tests', () => { } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); // execute first command let result1: Array = []; @@ -363,7 +368,7 @@ describe('InteractiveClient tests', () => { it('CommandFailedEvent rejects the execution promise', (done) => { const token = 'token'; - const clientMapper = new ClientMapper(async (_notebookPath) => new TestKernelTransport({ + const config = createKernelTransportConfig(async (notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { eventType: CommandFailedType, @@ -372,7 +377,8 @@ describe('InteractiveClient tests', () => { } ] })); - clientMapper.getOrAddClient({ fsPath: 'test/path' }).then(client => { + const clientMapper = new ClientMapper(config); + clientMapper.getOrAddClient(createUri('test/path')).then(client => { client.execute('bad-code-that-will-fail', 'csharp', _ => { }, _ => { }, { token }).then(result => { done(`expected execution to fail promise, but passed with: ${result}`); }).catch(_err => { @@ -383,7 +389,7 @@ describe('InteractiveClient tests', () => { it('clientMapper can reassociate clients', (done) => { let transportCreated = false; - const clientMapper = new ClientMapper(async (_notebookPath) => { + const config = createKernelTransportConfig(async (_notebookPath) => { if (transportCreated) { done('transport already created; this function should not have been called again'); } @@ -391,9 +397,10 @@ describe('InteractiveClient tests', () => { transportCreated = true; return new TestKernelTransport({}); }); - clientMapper.getOrAddClient({ fsPath: 'test-path.dib' }).then(_client => { - clientMapper.reassociateClient({ fsPath: 'test-path.dib' }, { fsPath: 'updated-path.dib' }); - clientMapper.getOrAddClient({ fsPath: 'updated-path.dib' }).then(_reassociatedClient => { + const clientMapper = new ClientMapper(config); + clientMapper.getOrAddClient(createUri('test-path.dib')).then(_client => { + clientMapper.reassociateClient(createUri('test-path.dib'), createUri('updated-path.dib')); + clientMapper.getOrAddClient(createUri('updated-path.dib')).then(_reassociatedClient => { done(); }); }); @@ -401,7 +408,7 @@ describe('InteractiveClient tests', () => { it('clientMapper reassociate does nothing for an untracked file', async () => { let transportCreated = false; - const clientMapper = new ClientMapper(async (_notebookPath) => { + const config = createKernelTransportConfig(async (_notebookPath) => { if (transportCreated) { throw new Error('transport already created; this function should not have been called again'); } @@ -409,17 +416,17 @@ describe('InteractiveClient tests', () => { transportCreated = true; return new TestKernelTransport({}); }); - await clientMapper.getOrAddClient({ fsPath: 'test-path.dib' }); - clientMapper.reassociateClient({ fsPath: 'not-a-tracked-file.txt' }, { fsPath: 'also-not-a-tracked-file.txt' }); - const _existingClient = await clientMapper.getOrAddClient({ fsPath: 'test-path.dib' }); - expect(clientMapper.isDotNetClient({ fsPath: 'not-a-tracked-file.txt' })).to.be.false; - expect(clientMapper.isDotNetClient({ fsPath: 'also-not-a-tracked-file.txt' })).to.be.false; + const clientMapper = new ClientMapper(config); + await clientMapper.getOrAddClient(createUri('test-path.dib')); + clientMapper.reassociateClient(createUri('not-a-tracked-file.txt'), createUri('also-not-a-tracked-file.txt')); + const _existingClient = await clientMapper.getOrAddClient(createUri('test-path.dib')); + expect(clientMapper.isDotNetClient(createUri('not-a-tracked-file.txt'))).to.be.false; + expect(clientMapper.isDotNetClient(createUri('also-not-a-tracked-file.txt'))).to.be.false; }); it('execution prevents diagnostics request forwarding', async () => { - let token = 'test-token'; - - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = 'test-token'; + const config = createKernelTransportConfig(async (notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { @@ -429,13 +436,13 @@ describe('InteractiveClient tests', () => { } ] })); - + const clientMapper = new ClientMapper(config); let diagnosticsCallbackFired = false; debounce("id0", 500, () => { diagnosticsCallbackFired = true; }); - const client = await clientMapper.getOrAddClient({ fsPath: 'test-path.dib' }); + const client = await clientMapper.getOrAddClient(createUri('test-path.dib')); await client.execute("1+1", "csharp", (_outputs) => { }, (_diagnostics) => { }, { token: token, id: "id0" }); await wait(1000); expect(diagnosticsCallbackFired).to.be.false; @@ -443,32 +450,38 @@ describe('InteractiveClient tests', () => { it('exception in submit code properly rejects all promises', done => { const token = 'test-token'; - const clientMapper = new ClientMapper(async (_notebookPath) => new CallbackTestKernelTransport({ + const config = createKernelTransportConfig(async (_notebookPath) => new CallbackTestKernelTransport({ 'SubmitCode': () => { throw new Error('expected exception during submit'); }, })); - clientMapper.getOrAddClient({ fsPath: 'test-path.dib' }).then(client => { + const clientMapper = new ClientMapper(config); + clientMapper.getOrAddClient(createUri('test-path.dib')).then(client => { expect(client.execute("1+1", "csharp", _outputs => { }, _diagnostics => { }, { token, id: '' })).eventually.rejectedWith('expected exception during submit').notify(done); }); }); it('exception in submit code properly generates error outputs', done => { const token = 'test-token'; - const clientMapper = new ClientMapper(async (_notebookPath) => new CallbackTestKernelTransport({ + const config = createKernelTransportConfig(async (_notebookPath) => new CallbackTestKernelTransport({ 'SubmitCode': () => { throw new Error('expected exception during submit'); }, })); + const clientMapper = new ClientMapper(config); let seenOutputs: Array = []; - clientMapper.getOrAddClient({ fsPath: 'test-path.dib' }).then(client => { + clientMapper.getOrAddClient(createUri('test-path.dib')).then(client => { expect(client.execute("1+1", "csharp", outputs => { seenOutputs = outputs; }, _diagnostics => { }, { token, id: '' })).eventually.rejected.then(() => { try { - expect(seenOutputs).to.deep.equal([{ + const decodedOutputs = decodeNotebookCellOutputs(seenOutputs); + expect(decodedOutputs).to.deep.equal([{ id: '1', outputs: [{ mime: vscodeLike.ErrorOutputMimeType, - value: 'Error: expected exception during submit', + decodedValue: { + name: 'Error', + message: 'Error: expected exception during submit', + }, }] }]); done(); @@ -480,10 +493,11 @@ describe('InteractiveClient tests', () => { }); it('exception creating kernel transport gracefully fails', done => { - const clientMapper = new ClientMapper(async _notebookPath => { + const config = createKernelTransportConfig(async (_notebookPath) => { throw new Error('simulated error during transport creation'); }); - expect(clientMapper.getOrAddClient({ fsPath: 'fake-notebook' })).eventually.rejectedWith('simulated error during transport creation').notify(done); + const clientMapper = new ClientMapper(config); + expect(clientMapper.getOrAddClient(createUri('fake-notebook'))).eventually.rejectedWith('simulated error during transport creation').notify(done); }); }); diff --git a/src/dotnet-interactive-vscode/common/tests/unit/languageProvider.test.ts b/src/dotnet-interactive-vscode/common/tests/unit/languageProvider.test.ts index 5f4ef8444d..a76d3214fd 100644 --- a/src/dotnet-interactive-vscode/common/tests/unit/languageProvider.test.ts +++ b/src/dotnet-interactive-vscode/common/tests/unit/languageProvider.test.ts @@ -9,11 +9,13 @@ import { provideCompletion } from './../../languageServices/completion'; import { provideHover } from './../../languageServices/hover'; import { provideSignatureHelp } from '../../languageServices/signatureHelp'; import { CommandSucceededType, CompletionsProducedType, HoverTextProducedType, SignatureHelpProducedType } from '../../interfaces/contracts'; +import { createUri } from '../../utilities'; +import { createKernelTransportConfig } from './utilities'; describe('LanguageProvider tests', () => { it('CompletionProvider', async () => { - let token = '123'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = '123'; + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'RequestCompletions': [ { eventType: CompletionsProducedType, @@ -39,20 +41,21 @@ describe('LanguageProvider tests', () => { } ] })); - clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + clientMapper.getOrAddClient(createUri('test/path')); - let code = 'Math.'; - let document = { - uri: { fsPath: 'test/path' }, + const code = 'Math.'; + const document = { + uri: createUri('test/path'), getText: () => code }; - let position = { + const position = { line: 0, character: 5 }; // perform the completion request - let completion = await provideCompletion(clientMapper, 'csharp', document, position, 0, token); + const completion = await provideCompletion(clientMapper, 'csharp', document, position, 0, token); expect(completion).to.deep.equal({ linePositionSpan: null, completions: [ @@ -69,8 +72,8 @@ describe('LanguageProvider tests', () => { }); it('HoverProvider', async () => { - let token = '123'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = '123'; + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'RequestHoverText': [ { eventType: HoverTextProducedType, @@ -102,20 +105,21 @@ describe('LanguageProvider tests', () => { } ] })); - clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + clientMapper.getOrAddClient(createUri('test/path')); - let code = 'var x = 1234;'; - let document = { - uri: { fsPath: 'test/path' }, + const code = 'var x = 1234;'; + const document = { + uri: createUri('test/path'), getText: () => code, }; - let position = { + const position = { line: 0, character: 10 }; // perform the hover request - let hover = await provideHover(clientMapper, 'csharp', document, position, 0, token); + const hover = await provideHover(clientMapper, 'csharp', document, position, 0, token); expect(hover).to.deep.equal({ contents: 'readonly struct System.Int32', isMarkdown: true, @@ -133,8 +137,8 @@ describe('LanguageProvider tests', () => { }); it('SignatureHelpProvider', async () => { - let token = '123'; - let clientMapper = new ClientMapper(async (_notebookPath) => new TestKernelTransport({ + const token = '123'; + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'RequestSignatureHelp': [ { eventType: SignatureHelpProducedType, @@ -169,20 +173,21 @@ describe('LanguageProvider tests', () => { } ] })); - clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + clientMapper.getOrAddClient(createUri('test/path')); - let code = 'Console.WriteLine(true'; - let document = { - uri: { fsPath: 'test/path' }, + const code = 'Console.WriteLine(true'; + const document = { + uri: createUri('test/path'), getText: () => code, }; - let position = { + const position = { line: 0, character: 22 }; // perform the sig help request - let sigHelp = await provideSignatureHelp(clientMapper, 'csharp', document, position, 0, token); + const sigHelp = await provideSignatureHelp(clientMapper, 'csharp', document, position, 0, token); expect(sigHelp).to.deep.equal({ activeParameter: 0, activeSignature: 0, diff --git a/src/dotnet-interactive-vscode/common/tests/unit/misc.test.ts b/src/dotnet-interactive-vscode/common/tests/unit/misc.test.ts index 82bbcbcdbe..288c3d2817 100644 --- a/src/dotnet-interactive-vscode/common/tests/unit/misc.test.ts +++ b/src/dotnet-interactive-vscode/common/tests/unit/misc.test.ts @@ -5,7 +5,7 @@ import { expect } from 'chai'; import { NotebookCellDisplayOutput, NotebookCellErrorOutput, NotebookCellTextOutput } from '../../interfaces/contracts'; import { isDisplayOutput, isErrorOutput, isTextOutput, reshapeOutputValueForVsCode } from '../../interfaces/utilities'; import { isDotNetNotebookMetadata, isIpynbFile } from '../../ipynbUtilities'; -import { debounce, executeSafe, isDotNetUpToDate, parse, processArguments, stringify } from '../../utilities'; +import { createUri, debounce, executeSafe, getWorkingDirectoryForNotebook, isDotNetUpToDate, parse, processArguments, stringify } from '../../utilities'; import * as vscodeLike from '../../interfaces/vscode-like'; @@ -78,7 +78,7 @@ describe('Miscellaneous tests', () => { ], workingDirectory: '{global_storage_path}' }; - let actual = processArguments(template, 'replacement-working-dir/notebook-file.dib', 'unused-working-dir', 'replacement-dotnet-path', 'replacement-global-storage-path'); + let actual = processArguments(template, 'replacement-working-dir', 'replacement-dotnet-path', 'replacement-global-storage-path'); expect(actual).to.deep.equal({ command: 'replacement-dotnet-path', args: [ @@ -94,24 +94,34 @@ describe('Miscellaneous tests', () => { }); }); - it(`uses the fallback working directory when it can't be reasonably determined`, () => { - const template = { - args: [ - '{dotnet_path}', - '--working-dir', - '{working_dir}' - ], - workingDirectory: '{global_storage_path}' - }; - let actual = processArguments(template, 'notebook-file-with-no-dir.dib', 'fallback-working-dir', 'dotnet-path', 'global-storage-path'); - expect(actual).to.deep.equal({ - command: 'dotnet-path', - args: [ - '--working-dir', - 'fallback-working-dir' - ], - workingDirectory: 'global-storage-path' - }); + it('notebook working directory comes from notebook uri if local', () => { + const notebookUri = createUri('path/to/notebook.dib'); + const workspaceFolderUris = [ + createUri('not/used/1'), + createUri('not/used/2'), + ]; + const workingDir = getWorkingDirectoryForNotebook(notebookUri, workspaceFolderUris, 'fallback-not-used'); + expect(workingDir).to.equal('path/to'); + }); + + it('notebook working directory comes from local workspace if notebook is untitled', () => { + const notebookUri = createUri('path/to/notebook.dib', 'untitled'); + const workspaceFolderUris = [ + createUri('not/used', 'remote'), + createUri('this/is/local/and/used'), + ]; + const workingDir = getWorkingDirectoryForNotebook(notebookUri, workspaceFolderUris, 'fallback-not-used'); + expect(workingDir).to.equal('this/is/local/and/used'); + }); + + it('notebook working directory comes from fallback if notebook is remote', () => { + const notebookUri = createUri('path/to/notebook.dib', 'remote'); + const workspaceFolderUris = [ + createUri('not/used/1'), + createUri('not/used/2'), + ]; + const workingDir = getWorkingDirectoryForNotebook(notebookUri, workspaceFolderUris, 'fallback-is-used'); + expect(workingDir).to.equal('fallback-is-used'); }); it('debounce test', async () => { diff --git a/src/dotnet-interactive-vscode/common/tests/unit/notebook.test.ts b/src/dotnet-interactive-vscode/common/tests/unit/notebook.test.ts index 6124724943..bd89bb6ae4 100644 --- a/src/dotnet-interactive-vscode/common/tests/unit/notebook.test.ts +++ b/src/dotnet-interactive-vscode/common/tests/unit/notebook.test.ts @@ -22,16 +22,17 @@ import { ReturnValueProducedType, StandardOutputValueProducedType, } from '../../interfaces/contracts'; -import { withFakeGlobalStorageLocation } from './utilities'; +import { createKernelTransportConfig, withFakeGlobalStorageLocation } from './utilities'; +import { createUri } from '../../utilities'; import { backupNotebook, languageToCellKind } from '../../interactiveNotebook'; import * as vscodeLike from '../../interfaces/vscode-like'; describe('Notebook tests', () => { - for (let language of ['csharp', 'fsharp']) { + for (const language of ['csharp', 'fsharp']) { it(`executes and returns expected value: ${language}`, async () => { - let token = '123'; - let code = '1+1'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = '123'; + const code = '1+1'; + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { eventType: CodeSubmissionReceivedType, @@ -67,7 +68,8 @@ describe('Notebook tests', () => { } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); let result: Array = []; await client.execute(code, language, outputs => result = outputs, _ => { }, { token }); expect(result).to.deep.equal([ @@ -85,13 +87,13 @@ describe('Notebook tests', () => { } it('multiple stdout values cause the output to grow', async () => { - let token = '123'; - let code = ` + const token = '123'; + const code = ` Console.WriteLine(1); Console.WriteLine(1); Console.WriteLine(1); `; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { eventType: CodeSubmissionReceivedType, @@ -153,7 +155,8 @@ Console.WriteLine(1); } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); let result: Array = []; await client.execute(code, 'csharp', outputs => result = outputs, _ => { }, { token }); expect(result).to.deep.equal([ @@ -188,9 +191,9 @@ Console.WriteLine(1); }); it('updated values are replaced instead of added', async () => { - let token = '123'; - let code = '#r nuget:Newtonsoft.Json'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = '123'; + const code = '#r nuget:Newtonsoft.Json'; + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { eventType: CodeSubmissionReceivedType, @@ -247,7 +250,8 @@ Console.WriteLine(1); } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); let result: Array = []; await client.execute(code, 'csharp', outputs => result = outputs, _ => { }, { token }); expect(result).to.deep.equal([ @@ -273,9 +277,9 @@ Console.WriteLine(1); }); it('returned json is properly parsed', async () => { - let token = '123'; - let code = 'JObject.FromObject(new { a = 1, b = false })'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = '123'; + const code = 'JObject.FromObject(new { a = 1, b = false })'; + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { eventType: CodeSubmissionReceivedType, @@ -311,7 +315,8 @@ Console.WriteLine(1); } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); let result: Array = []; await client.execute(code, 'csharp', outputs => result = outputs, _ => { }, { token }); expect(result).to.deep.equal([ @@ -331,9 +336,9 @@ Console.WriteLine(1); }); it('diagnostics are reported on CommandFailed', (done) => { - let token = '123'; - let code = 'Console.WriteLin();'; - let clientMapper = new ClientMapper(async (_notebookPath) => new TestKernelTransport({ + const token = '123'; + const code = 'Console.WriteLin();'; + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { eventType: CodeSubmissionReceivedType, @@ -381,7 +386,8 @@ Console.WriteLine(1); } ] })); - clientMapper.getOrAddClient({ fsPath: 'test/path' }).then(client => { + const clientMapper = new ClientMapper(config); + clientMapper.getOrAddClient(createUri('test/path')).then(client => { let diagnostics: Array = []; client.execute(code, 'csharp', _ => { }, diags => diagnostics = diags, { token }).then(result => { done(`expected execution to fail, but it passed with: ${result}`); @@ -409,9 +415,9 @@ Console.WriteLine(1); }); it('diagnostics are reported on CommandSucceeded', async () => { - let token = '123'; - let code = 'Console.WriteLine();'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = '123'; + const code = 'Console.WriteLine();'; + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'SubmitCode': [ { eventType: CodeSubmissionReceivedType, @@ -457,7 +463,8 @@ Console.WriteLine(1); } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); let diagnostics: Array = []; await client.execute(code, 'csharp', _ => { }, diags => diagnostics = diags, { token }); expect(diagnostics).to.deep.equal([ @@ -480,9 +487,9 @@ Console.WriteLine(1); }); it('diagnostics are reported when directly requested', async () => { - let token = '123'; - let code = 'Console.WriteLine();'; - let clientMapper = new ClientMapper(async (notebookPath) => new TestKernelTransport({ + const token = '123'; + const code = 'Console.WriteLine();'; + const config = createKernelTransportConfig(async (_notebookPath) => new TestKernelTransport({ 'RequestDiagnostics': [ { eventType: DiagnosticsProducedType, @@ -514,7 +521,8 @@ Console.WriteLine(1); } ] })); - let client = await clientMapper.getOrAddClient({ fsPath: 'test/path' }); + const clientMapper = new ClientMapper(config); + const client = await clientMapper.getOrAddClient(createUri('test/path')); const diagnostics = await client.getDiagnostics('csharp', code, token); expect(diagnostics).to.deep.equal([ { diff --git a/src/dotnet-interactive-vscode/common/tests/unit/utilities.ts b/src/dotnet-interactive-vscode/common/tests/unit/utilities.ts index d1c003f871..a09278e8e7 100644 --- a/src/dotnet-interactive-vscode/common/tests/unit/utilities.ts +++ b/src/dotnet-interactive-vscode/common/tests/unit/utilities.ts @@ -1,9 +1,12 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +import * as contracts from '../../interfaces/contracts'; import * as fs from 'fs'; import * as path from 'path'; import * as tmp from 'tmp'; +import * as vscodeLike from '../../interfaces/vscode-like'; +import { createOutput } from '../../utilities'; export function withFakeGlobalStorageLocation(createLocation: boolean, callback: { (globalStoragePath: string): Promise }) { return new Promise((resolve, reject) => { @@ -25,3 +28,46 @@ export function withFakeGlobalStorageLocation(createLocation: boolean, callback: }); }); } + +export function createKernelTransportConfig(kernelTransportCreator: (notebookUri: vscodeLike.Uri) => Promise) { + function defaultKernelTransportCreator(notebookUri: vscodeLike.Uri): Promise { + throw new Error('Each test needs to override this.'); + } + + const encoder = new TextEncoder(); + + const defaultClientMapperConfig = { + kernelTransportCreator: defaultKernelTransportCreator, + createErrorOutput: (message: string, outputId?: string) => { + const errorItem = { + mime: 'application/vnd.code.notebook.error', + value: encoder.encode(JSON.stringify({ + name: 'Error', + message, + })), + }; + const cellOutput = createOutput([errorItem], outputId); + return cellOutput; + }, + diagnosticChannel: undefined, + }; + + return { + ...defaultClientMapperConfig, + kernelTransportCreator + }; +} + +export function decodeNotebookCellOutputs(outputs: vscodeLike.NotebookCellOutput[]): any[] { + const decoder = new TextDecoder('utf-8'); + return outputs.map(o => ({ + ...o, outputs: o.outputs.map(oi => { + let result = { + ...oi, + decodedValue: JSON.parse(decoder.decode(oi.value)) + }; + delete result.value; + return result; + }) + })); +} diff --git a/src/dotnet-interactive-vscode/common/utilities.ts b/src/dotnet-interactive-vscode/common/utilities.ts index 73070c7501..7ecc854e9b 100644 --- a/src/dotnet-interactive-vscode/common/utilities.ts +++ b/src/dotnet-interactive-vscode/common/utilities.ts @@ -7,7 +7,6 @@ import * as path from 'path'; import { v4 as uuid } from 'uuid'; import { InstallInteractiveArgs, ProcessStart } from "./interfaces"; import { ErrorOutputMimeType, NotebookCellOutput, NotebookCellOutputItem, ReportChannel, Uri } from './interfaces/vscode-like'; -import { isDotnetInteractiveLanguage } from './interactiveNotebook'; export function executeSafe(command: string, args: Array, workingDirectory?: string | undefined): Promise<{ code: number, output: string, error: string }> { return new Promise<{ code: number, output: string, error: string }>(resolve => { @@ -74,25 +73,11 @@ export function createOutput(outputItems: Array, outputI return output; } -export function createErrorOutput(message: string, outputId?: string): NotebookCellOutput { - const outputItem: NotebookCellOutputItem = { - mime: ErrorOutputMimeType, - value: message, - }; - const output = createOutput([outputItem], outputId); - return output; -} - export function isDotNetUpToDate(minVersion: string, commandResult: { code: number, output: string }): boolean { return commandResult.code === 0 && compareVersions.compare(commandResult.output, minVersion, '>='); } -export function processArguments(template: { args: Array, workingDirectory: string }, notebookPath: string, fallbackWorkingDirectory: string, dotnetPath: string, globalStoragePath: string): ProcessStart { - let workingDirectory = path.parse(notebookPath).dir; - if (workingDirectory === '') { - workingDirectory = fallbackWorkingDirectory; - } - +export function processArguments(template: { args: Array, workingDirectory: string }, workingDirectory: string, dotnetPath: string, globalStoragePath: string): ProcessStart { let map: { [key: string]: string } = { 'dotnet_path': dotnetPath, 'global_storage_path': globalStoragePath, @@ -203,13 +188,29 @@ export function debounceAndReject(key: string, timeout: number, callback: () return newPromise; } -export function createUri(fsPath: string): Uri { +export function createUri(fsPath: string, scheme?: string): Uri { return { fsPath, + scheme: scheme || 'file', toString: () => fsPath }; } +export function getWorkingDirectoryForNotebook(notebookUri: Uri, workspaceFolderUris: Uri[], fallackWorkingDirectory: string): string { + switch (notebookUri.scheme) { + case 'file': + // local file, use it's own directory + return path.dirname(notebookUri.fsPath); + case 'untitled': + // unsaved notebook, use first local workspace folder + const firstLocalWorkspaceFolderUri = workspaceFolderUris.find(uri => uri.scheme === 'file'); + return firstLocalWorkspaceFolderUri?.fsPath ?? fallackWorkingDirectory; + default: + // something else (e.g., remote notebook), use fallback + return fallackWorkingDirectory; + } +} + export function parse(text: string): any { return JSON.parse(text, (key, value) => { if (key === 'rawData' && typeof value === 'string') { diff --git a/src/dotnet-interactive-vscode/common/vscode/commands.ts b/src/dotnet-interactive-vscode/common/vscode/commands.ts index 8ac1207fa9..549b10b5d5 100644 --- a/src/dotnet-interactive-vscode/common/vscode/commands.ts +++ b/src/dotnet-interactive-vscode/common/vscode/commands.ts @@ -6,13 +6,14 @@ import * as path from 'path'; import { acquireDotnetInteractive } from '../acquisition'; import { InstallInteractiveArgs, InteractiveLaunchOptions } from '../interfaces'; import { ClientMapper } from '../clientMapper'; -import { getEol, isStableBuild, isUnsavedNotebook, toNotebookDocument } from './vscodeUtilities'; -import { DotNetPathManager, KernelId } from './extension'; +import { getEol, toNotebookDocument } from './vscodeUtilities'; +import { DotNetPathManager, KernelIdForJupyter } from './extension'; import { computeToolInstallArguments, executeSafe, executeSafeAndLog } from '../utilities'; import * as versionSpecificFunctions from '../../versionSpecificFunctions'; import { ReportChannel } from '../interfaces/vscode-like'; import { IJupyterExtensionApi } from '../../jupyter'; +import { isJupyterNotebookViewType, jupyterViewType } from '../interactiveNotebook'; export function registerAcquisitionCommands(context: vscode.ExtensionContext, diagnosticChannel: ReportChannel) { const config = vscode.workspace.getConfiguration('dotnet-interactive'); @@ -121,7 +122,7 @@ export function registerKernelCommands(context: vscode.ExtensionContext, clientM } if (document) { - for (const cell of versionSpecificFunctions.getCells(document)) { + for (const cell of document.getCells()) { versionSpecificFunctions.endExecution(cell, false); } @@ -145,20 +146,8 @@ export function registerFileCommands(context: vscode.ExtensionContext, clientMap 'Jupyter Notebooks': ['ipynb'], }; - function workspaceHasUnsavedNotebookWithName(fileName: string): boolean { - return vscode.workspace.textDocuments.findIndex(textDocument => { - if (textDocument.notebook) { - const notebookUri = textDocument.notebook.uri; - return isUnsavedNotebook(notebookUri) && path.basename(notebookUri.fsPath) === fileName; - } - - return false; - }) >= 0; - } - const newDibNotebookText = `Create as '.dib'`; const newIpynbNotebookText = `Create as '.ipynb'`; - context.subscriptions.push(vscode.commands.registerCommand('dotnet-interactive.newNotebook', async () => { const selected = await vscode.window.showQuickPick([newDibNotebookText, newIpynbNotebookText]); switch (selected) { @@ -179,26 +168,12 @@ export function registerFileCommands(context: vscode.ExtensionContext, clientMap context.subscriptions.push(vscode.commands.registerCommand('dotnet-interactive.newNotebookIpynb', async () => { // note, new .ipynb notebooks are currently affected by this bug: https://github.com/microsoft/vscode/issues/121974 - await newNotebook('.ipynb'); - - selectDotNetInteractiveKernel(); + await selectDotNetInteractiveKernelForJupyter(); })); async function newNotebook(extension: string): Promise { - const fileName = getNewNotebookName(extension); - const newUri = vscode.Uri.file(fileName).with({ scheme: 'untitled', path: fileName }); - await openNotebook(newUri); - } - - function getNewNotebookName(extension: string): string { - let suffix = 1; - let filename = ''; - do { - filename = `Untitled-${suffix++}${extension}`; - } while (workspaceHasUnsavedNotebookWithName(filename)); - - return filename; + versionSpecificFunctions.createNewBlankNotebook(extension, openNotebook); } context.subscriptions.push(vscode.commands.registerCommand('dotnet-interactive.openNotebook', async (notebookUri: vscode.Uri | undefined) => { @@ -222,14 +197,16 @@ export function registerFileCommands(context: vscode.ExtensionContext, clientMap })); async function openNotebook(uri: vscode.Uri): Promise { - const extension = path.extname(uri.fsPath); + const extension = path.extname(uri.toString()); const viewType = extension === '.dib' || extension === '.dotnet-interactive' ? 'dotnet-interactive' - : 'jupyter-notebook'; + : jupyterViewType; - if (viewType === 'jupyter-notebook' && uri.scheme === 'untitled') { + if (isJupyterNotebookViewType(viewType) && uri.scheme === 'untitled') { await openNewNotebookWithJupyterExtension(); } else { + // const notebook = await vscode.notebook.openNotebookDocument(uri); + // await vscode.window.showNotebookDocument(notebook); await vscode.commands.executeCommand('vscode.openWith', uri, viewType); } } @@ -263,9 +240,10 @@ export function registerFileCommands(context: vscode.ExtensionContext, clientMap const { document } = vscode.window.activeNotebookEditor; const notebook = toNotebookDocument(document); const client = await clientMapper.getOrAddClient(uri); - const buffer = await client.serializeNotebook(uri.fsPath, notebook, eol); + const uriPath = uri.toString(); + const buffer = await client.serializeNotebook(uriPath, notebook, eol); await vscode.workspace.fs.writeFile(uri, buffer); - switch (path.extname(uri.fsPath)) { + switch (path.extname(uriPath)) { case '.dib': case '.dotnet-interactive': await vscode.commands.executeCommand('dotnet-interactive.openNotebook', uri); @@ -275,9 +253,9 @@ export function registerFileCommands(context: vscode.ExtensionContext, clientMap })); } -export async function selectDotNetInteractiveKernel(): Promise { +export async function selectDotNetInteractiveKernelForJupyter(): Promise { const extension = 'ms-dotnettools.dotnet-interactive-vscode'; - const id = KernelId; + const id = KernelIdForJupyter; await vscode.commands.executeCommand('notebook.selectKernel', { extension, id }); } diff --git a/src/dotnet-interactive-vscode/common/vscode/extension.ts b/src/dotnet-interactive-vscode/common/vscode/extension.ts index b7d9edd8e3..0bc5828250 100644 --- a/src/dotnet-interactive-vscode/common/vscode/extension.ts +++ b/src/dotnet-interactive-vscode/common/vscode/extension.ts @@ -1,9 +1,12 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +import * as contracts from '../interfaces/contracts'; import * as fs from 'fs'; -import * as vscode from 'vscode'; +import * as os from 'os'; import * as path from 'path'; +import * as vscode from 'vscode'; +import * as vscodeLike from '../interfaces/vscode-like'; import { ClientMapper } from '../clientMapper'; import { StdioKernelTransport } from '../stdioKernelTransport'; @@ -13,15 +16,15 @@ import { registerAcquisitionCommands, registerKernelCommands, registerFileComman import { getSimpleLanguage, isDotnetInteractiveLanguage, isJupyterNotebookViewType } from '../interactiveNotebook'; import { InteractiveLaunchOptions, InstallInteractiveArgs } from '../interfaces'; -import { executeSafe, isDotNetUpToDate, processArguments } from '../utilities'; +import { executeSafe, getWorkingDirectoryForNotebook, isDotNetUpToDate, processArguments } from '../utilities'; import { OutputChannelAdapter } from './OutputChannelAdapter'; import * as versionSpecificFunctions from '../../versionSpecificFunctions'; -import { isInsidersBuild } from './vscodeUtilities'; +import { isInsidersBuild, isStableBuild } from './vscodeUtilities'; import { getDotNetMetadata } from '../ipynbUtilities'; -export const KernelId = 'dotnet-interactive'; +export const KernelIdForJupyter = 'dotnet-interactive-for-jupyter'; export class CachedDotNetPathManager { private dotNetPath: string = 'dotnet'; // default to global tool if possible @@ -89,8 +92,7 @@ export async function activate(context: vscode.ExtensionContext) { } }); - // register with VS Code - const clientMapper = new ClientMapper(async (notebookPath) => { + async function kernelTransportCreator(notebookUri: vscodeLike.Uri): Promise { if (!await checkForDotNetSdk(minDotNetSdkVersion!)) { const message = 'Unable to find appropriate .NET SDK.'; vscode.window.showErrorMessage(message); @@ -110,22 +112,25 @@ export async function activate(context: vscode.ExtensionContext) { workingDirectory: config.get('kernelTransportWorkingDirectory')! }; - // ensure a reasonable working directory is selected - const fallbackWorkingDirectory = (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) - ? vscode.workspace.workspaceFolders[0].uri.fsPath - : '.'; + // try to use $HOME/Downloads as a fallback for remote notebooks, but use the home directory if all else fails + const homeDir = os.homedir(); + const downloadsDir = path.join(homeDir, 'Downloads'); + const fallbackWorkingDirectory = fs.existsSync(downloadsDir) ? downloadsDir : homeDir; - const processStart = processArguments(argsTemplate, notebookPath, fallbackWorkingDirectory, DotNetPathManager.getDotNetPath(), launchOptions!.workingDirectory); + const workspaceFolderUris = vscode.workspace.workspaceFolders?.map(folder => folder.uri) || []; + const workingDirectory = getWorkingDirectoryForNotebook(notebookUri, workspaceFolderUris, fallbackWorkingDirectory); + const processStart = processArguments(argsTemplate, workingDirectory, DotNetPathManager.getDotNetPath(), launchOptions!.workingDirectory); let notification = { displayError: async (message: string) => { await vscode.window.showErrorMessage(message, { modal: false }); }, displayInfo: async (message: string) => { await vscode.window.showInformationMessage(message, { modal: false }); }, }; - const transport = new StdioKernelTransport(notebookPath, processStart, diagnosticsChannel, vscode.Uri.parse, notification, (pid, code, signal) => { - clientMapper.closeClient({ fsPath: notebookPath }, false); + const transport = new StdioKernelTransport(notebookUri.toString(), processStart, diagnosticsChannel, vscode.Uri.parse, notification, (pid, code, signal) => { + clientMapper.closeClient(notebookUri, false); }); await transport.waitForReady(); let externalUri = vscode.Uri.parse(`http://127.0.0.1:${transport.httpPort}`); + //let externalUri = vscode.Uri.parse(`http://localhost:${transport.httpPort}`); try { await transport.setExternalUri(externalUri); @@ -135,7 +140,15 @@ export async function activate(context: vscode.ExtensionContext) { } return transport; - }, diagnosticsChannel); + } + + // register with VS Code + const clientMapperConfig = { + kernelTransportCreator, + createErrorOutput: versionSpecificFunctions.createErrorOutput, + diagnosticsChannel, + }; + const clientMapper = new ClientMapper(clientMapperConfig); registerKernelCommands(context, clientMapper); @@ -149,7 +162,7 @@ export async function activate(context: vscode.ExtensionContext) { throw new Error(`Unable to find bootstrapper API expected at '${apiBootstrapperUri.fsPath}'.`); } - versionSpecificFunctions.registerWithVsCode(context, clientMapper, diagnosticsChannel, apiBootstrapperUri); + versionSpecificFunctions.registerWithVsCode(context, clientMapper, diagnosticsChannel, clientMapperConfig.createErrorOutput, apiBootstrapperUri); registerFileCommands(context, clientMapper); @@ -189,7 +202,7 @@ async function updateNotebookCellLanguageInMetadata(candidateNotebookCellDocumen if (notebook && isJupyterNotebookViewType(notebook.viewType) && isDotnetInteractiveLanguage(candidateNotebookCellDocument.languageId)) { - const cell = versionSpecificFunctions.getCells(notebook).find(c => c.document === candidateNotebookCellDocument); + const cell = notebook.getCells().find(c => c.document === candidateNotebookCellDocument); if (cell) { const cellLanguage = cell.kind === vscode.NotebookCellKind.Code ? getSimpleLanguage(candidateNotebookCellDocument.languageId) diff --git a/src/dotnet-interactive-vscode/common/vscode/languageProvider.ts b/src/dotnet-interactive-vscode/common/vscode/languageProvider.ts index f7c34da064..2b1b573e42 100644 --- a/src/dotnet-interactive-vscode/common/vscode/languageProvider.ts +++ b/src/dotnet-interactive-vscode/common/vscode/languageProvider.ts @@ -9,7 +9,6 @@ import { notebookCellLanguages, getSimpleLanguage, notebookCellChanged } from '. import { convertToRange, toVsCodeDiagnostic } from './vscodeUtilities'; import { getDiagnosticCollection } from './diagnostics'; import { provideSignatureHelp } from './../languageServices/signatureHelp'; -import * as versionSpecificFunctions from '../../versionSpecificFunctions'; export class CompletionItemProvider implements vscode.CompletionItemProvider { static readonly triggerCharacters = ['.']; @@ -132,7 +131,8 @@ export function registerLanguageProviders(clientMapper: ClientMapper, languageSe disposables.push(vscode.languages.registerSignatureHelpProvider(languages, new SignatureHelpProvider(clientMapper, languageServiceDelay), ...SignatureHelpProvider.triggerCharacters)); disposables.push(vscode.workspace.onDidChangeTextDocument(e => { if (vscode.languages.match(notebookCellLanguages, e.document)) { - const cell = versionSpecificFunctions.getCells(vscode.window.activeNotebookEditor?.document).find(cell => cell.document === e.document); + const cells = vscode.window.activeNotebookEditor?.document.getCells(); + const cell = cells?.find(cell => cell.document === e.document); if (cell) { notebookCellChanged(clientMapper, e.document, getSimpleLanguage(cell.document.languageId), languageServiceDelay, diagnostics => { const collection = getDiagnosticCollection(e.document.uri); diff --git a/src/dotnet-interactive-vscode/common/vscode/vscodeUtilities.ts b/src/dotnet-interactive-vscode/common/vscode/vscodeUtilities.ts index eee6f79b55..1791737c6e 100644 --- a/src/dotnet-interactive-vscode/common/vscode/vscodeUtilities.ts +++ b/src/dotnet-interactive-vscode/common/vscode/vscodeUtilities.ts @@ -6,7 +6,6 @@ import * as vscode from 'vscode'; import { Eol, WindowsEol, NonWindowsEol } from "../interfaces"; import { Diagnostic, DiagnosticSeverity, LinePosition, LinePositionSpan, NotebookCell, NotebookCellDisplayOutput, NotebookCellErrorOutput, NotebookCellOutput, NotebookDocument } from '../interfaces/contracts'; -import * as versionSpecificFunctions from '../../versionSpecificFunctions'; import { getSimpleLanguage } from '../interactiveNotebook'; import * as vscodeLike from '../interfaces/vscode-like'; @@ -72,12 +71,9 @@ export function getEol(): Eol { } } -export function isUnsavedNotebook(uri: vscode.Uri): boolean { - return uri.scheme === 'untitled'; -} export function toNotebookDocument(document: vscode.NotebookDocument): NotebookDocument { return { - cells: versionSpecificFunctions.getCells(document).map(toNotebookCell) + cells: document.getCells().map(toNotebookCell) }; } diff --git a/src/dotnet-interactive-vscode/insiders/package.json b/src/dotnet-interactive-vscode/insiders/package.json index d89f69b370..4eb7bd77a4 100644 --- a/src/dotnet-interactive-vscode/insiders/package.json +++ b/src/dotnet-interactive-vscode/insiders/package.json @@ -42,6 +42,7 @@ "activationEvents": [ "onUri", "onNotebook:dotnet-interactive", + "onNotebook:dotnet-interactive-legacy", "onNotebook:*", "onCommand:dotnet-interactive.acquire", "onCommand:dotnet-interactive.newNotebook", @@ -52,6 +53,12 @@ "extensionDependencies": [ "ms-toolsai.jupyter" ], + "capabilities": { + "untrustedWorkspaces": { + "supported": true + }, + "virtualWorkspaces": true + }, "contributes": { "notebooks": [ { @@ -59,7 +66,16 @@ "displayName": ".NET Interactive Notebook", "selector": [ { - "filenamePattern": "*.{dib,dotnet-interactive}" + "filenamePattern": "*.dib" + } + ] + }, + { + "viewType": "dotnet-interactive-legacy", + "displayName": ".NET Interactive Notebook", + "selector": [ + { + "filenamePattern": "*.dotnet-interactive" } ] } diff --git a/src/dotnet-interactive-vscode/insiders/src/notebookContentProviderWrapper.ts b/src/dotnet-interactive-vscode/insiders/src/notebookContentProviderWrapper.ts deleted file mode 100644 index d14f9123cf..0000000000 --- a/src/dotnet-interactive-vscode/insiders/src/notebookContentProviderWrapper.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -import * as vscode from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; -import { backupNotebook, defaultNotebookCellLanguage } from './common/interactiveNotebook'; -import { isUnsavedNotebook } from './common/vscode/vscodeUtilities'; -import { OutputChannelAdapter } from './common/vscode/OutputChannelAdapter'; - -// a thin wrapper around the new `vscode.NotebookSerializer` api -export class DotNetNotebookContentProviderWrapper implements vscode.NotebookContentProvider { - constructor(private readonly serializer: vscode.NotebookSerializer, private readonly outputChannel: OutputChannelAdapter) { - } - - async openNotebook(uri: vscode.Uri, openContext: vscode.NotebookDocumentOpenContext, token: vscode.CancellationToken): Promise { - let fileUri: vscode.Uri | undefined = isUnsavedNotebook(uri) - ? undefined - : uri; - if (openContext.backupId) { - // restoring a backed up notebook - - // N.B., when F5 debugging, the `backupId` property is _always_ `undefined`, so to properly test this you'll - // have to build and install a VSIX. - fileUri = vscode.Uri.file(openContext.backupId); - } - - let notebookData: vscode.NotebookData | undefined = undefined; - if (fileUri && fs.existsSync(fileUri.fsPath)) { - // file on disk - try { - const buffer = Buffer.from(await vscode.workspace.fs.readFile(fileUri)); - notebookData = await this.serializer.deserializeNotebook(buffer, token); - } catch (e) { - vscode.window.showErrorMessage(`Error opening file '${fileUri.fsPath}'; check the '${this.outputChannel.getName()}' output channel for details`); - this.outputChannel.appendLine(`Error opening file '${fileUri.fsPath}':\n${e?.message}`); - } - } else { - // new empty/blank notebook; nothing to do - } - - if (!notebookData) { - notebookData = new vscode.NotebookData([]); - } - - if (notebookData.cells.length === 0) { - // ensure at least one cell - notebookData = new vscode.NotebookData([{ - kind: vscode.NotebookCellKind.Code, - value: '', - languageId: defaultNotebookCellLanguage, - }]); - } - - return notebookData; - } - - saveNotebook(document: vscode.NotebookDocument, token: vscode.CancellationToken): Thenable { - return this.saveNotebookToUri(document, document.uri, token); - } - - saveNotebookAs(targetResource: vscode.Uri, document: vscode.NotebookDocument, token: vscode.CancellationToken): Thenable { - return this.saveNotebookToUri(document, targetResource, token); - } - - async backupNotebook(document: vscode.NotebookDocument, context: vscode.NotebookDocumentBackupContext, token: vscode.CancellationToken): Promise { - const extension = path.extname(document.uri.fsPath); - const content = await this.notebookAsUint8Array(document, token); - return backupNotebook(content, context.destination.fsPath + extension); - } - - private async notebookAsUint8Array(document: vscode.NotebookDocument, token: vscode.CancellationToken): Promise { - const notebookData: vscode.NotebookData = { - cells: document.getCells().map(cell => new vscode.NotebookCellData(cell.kind, cell.document.getText(), cell.document.languageId)), - metadata: new vscode.NotebookDocumentMetadata(), - }; - const content = await this.serializer.serializeNotebook(notebookData, token); - return content; - } - - private async saveNotebookToUri(document: vscode.NotebookDocument, uri: vscode.Uri, token: vscode.CancellationToken): Promise { - const content = await this.notebookAsUint8Array(document, token); - await vscode.workspace.fs.writeFile(uri, content); - } -} diff --git a/src/dotnet-interactive-vscode/insiders/src/notebookControllers.ts b/src/dotnet-interactive-vscode/insiders/src/notebookControllers.ts index b4b94cfa45..56ed41a523 100644 --- a/src/dotnet-interactive-vscode/insiders/src/notebookControllers.ts +++ b/src/dotnet-interactive-vscode/insiders/src/notebookControllers.ts @@ -7,25 +7,30 @@ import { ClientMapper } from './common/clientMapper'; import * as contracts from './common/interfaces/contracts'; import * as vscodeLike from './common/interfaces/vscode-like'; import * as diagnostics from './common/vscode/diagnostics'; -import * as utilities from './common/utilities'; -import * as versionSpecificFunctions from './versionSpecificFunctions'; import * as vscodeUtilities from './common/vscode/vscodeUtilities'; -import { getSimpleLanguage, isDotnetInteractiveLanguage, notebookCellLanguages } from './common/interactiveNotebook'; +import { getSimpleLanguage, isDotnetInteractiveLanguage, jupyterViewType, notebookCellLanguages } from './common/interactiveNotebook'; import { getCellLanguage, getDotNetMetadata, getLanguageInfoMetadata, isDotNetNotebookMetadata, withDotNetKernelMetadata } from './common/ipynbUtilities'; import { reshapeOutputValueForVsCode } from './common/interfaces/utilities'; -import { selectDotNetInteractiveKernel } from './common/vscode/commands'; +import { selectDotNetInteractiveKernelForJupyter } from './common/vscode/commands'; +import { ErrorOutputCreator } from './common/interactiveClient'; const executionTasks: Map = new Map(); const viewType = 'dotnet-interactive'; -const jupyterViewType = 'jupyter-notebook'; +const legacyViewType = 'dotnet-interactive-legacy'; + +export interface DotNetNotebookKernelConfiguration { + clientMapper: ClientMapper, + preloadUris: vscode.Uri[], + createErrorOutput: ErrorOutputCreator, +} export class DotNetNotebookKernel { private disposables: { dispose(): void }[] = []; - constructor(private readonly clientMapper: ClientMapper, preloadUris: vscode.Uri[]) { - const preloads = preloadUris.map(uri => new vscode.NotebookKernelPreload(uri)); + constructor(readonly config: DotNetNotebookKernelConfiguration) { + const preloads = config.preloadUris.map(uri => new vscode.NotebookKernelPreload(uri)); // .dib execution const dibController = vscode.notebook.createNotebookController( @@ -37,6 +42,16 @@ export class DotNetNotebookKernel { ); this.commonControllerInit(dibController); + // .dotnet-interactive execution + const legacyController = vscode.notebook.createNotebookController( + 'dotnet-interactive-legacy', + legacyViewType, + '.NET Interactive', + this.executeHandler.bind(this), + preloads + ); + this.commonControllerInit(legacyController); + // .ipynb execution via Jupyter extension (optional) const jupyterController = vscode.notebook.createNotebookController( 'dotnet-interactive-for-jupyter', @@ -48,15 +63,15 @@ export class DotNetNotebookKernel { jupyterController.onDidChangeNotebookAssociation(async e => { // update metadata if (e.selected) { - await updateNotebookMetadata(e.notebook, clientMapper); + await updateNotebookMetadata(e.notebook, this.config.clientMapper); } }); this.commonControllerInit(jupyterController); this.disposables.push(vscode.notebook.onDidOpenNotebookDocument(async notebook => { if (notebook.viewType === jupyterViewType && isDotNetNotebook(notebook)) { jupyterController.updateNotebookAffinity(notebook, vscode.NotebookControllerAffinity.Preferred); - await selectDotNetInteractiveKernel(); - await updateNotebookMetadata(notebook, clientMapper); + await selectDotNetInteractiveKernelForJupyter(); + await updateNotebookMetadata(notebook, this.config.clientMapper); } })); } @@ -71,12 +86,12 @@ export class DotNetNotebookKernel { const documentUri = e.editor.document.uri; switch (e.message.command) { case "getHttpApiEndpoint": - this.clientMapper.tryGetClient(documentUri).then(client => { + this.config.clientMapper.tryGetClient(documentUri).then(client => { if (client) { const uri = client.tryGetProperty("externalUri"); controller.postMessage({ command: "configureFactories", endpointUri: uri?.toString() }); - this.clientMapper.onClientCreate(documentUri, async (client) => { + this.config.clientMapper.onClientCreate(documentUri, async (client) => { const uri = client.tryGetProperty("externalUri"); await controller.postMessage({ command: "resetFactories", endpointUri: uri?.toString() }); }); @@ -103,10 +118,11 @@ export class DotNetNotebookKernel { startTime: Date.now(), }); + executionTask.clearOutput(cell.index); - const client = await this.clientMapper.getOrAddClient(cell.notebook.uri); + const client = await this.config.clientMapper.getOrAddClient(cell.notebook.uri); executionTask.token.onCancellationRequested(() => { - const errorOutput = utilities.createErrorOutput("Cell execution cancelled by user"); + const errorOutput = this.config.createErrorOutput("Cell execution cancelled by user"); const resultPromise = () => updateCellOutputs(executionTask, cell, [...cell.outputs, errorOutput]) .then(() => endExecution(cell, false)); client.cancel() @@ -128,7 +144,7 @@ export class DotNetNotebookKernel { endExecution(cell, true) ).catch(() => endExecution(cell, false)); } catch (err) { - const errorOutput = utilities.createErrorOutput(`Error executing cell: ${err}`); + const errorOutput = this.config.createErrorOutput(`Error executing cell: ${err}`); await updateCellOutputs(executionTask, cell, [errorOutput]); endExecution(cell, false); throw err; diff --git a/src/dotnet-interactive-vscode/insiders/src/notebookSerializers.ts b/src/dotnet-interactive-vscode/insiders/src/notebookSerializers.ts index b60607676c..59e5f90d66 100644 --- a/src/dotnet-interactive-vscode/insiders/src/notebookSerializers.ts +++ b/src/dotnet-interactive-vscode/insiders/src/notebookSerializers.ts @@ -11,7 +11,7 @@ import { defaultNotebookCellLanguage, getNotebookSpecificLanguage, getSimpleLang import { OutputChannelAdapter } from './common/vscode/OutputChannelAdapter'; import { getEol, vsCodeCellOutputToContractCellOutput } from './common/vscode/vscodeUtilities'; import { Eol } from './common/interfaces'; -import { DotNetNotebookContentProviderWrapper } from './notebookContentProviderWrapper'; +import { createUri } from './common/utilities'; abstract class DotNetNotebookSerializer implements vscode.NotebookSerializer { @@ -21,19 +21,12 @@ abstract class DotNetNotebookSerializer implements vscode.NotebookSerializer { constructor( notebookType: string, - registerAsSerializer: boolean, private readonly clientMapper: ClientMapper, private readonly outputChannel: OutputChannelAdapter, private readonly extension: string, ) { this.eol = getEol(); - if (registerAsSerializer) { - this.disposable = vscode.notebook.registerNotebookSerializer(notebookType, this); - } else { - // temporarly workaround for https://github.com/microsoft/vscode/issues/121974 - const contentProviderWrapper = new DotNetNotebookContentProviderWrapper(this, this.outputChannel); - this.disposable = vscode.notebook.registerNotebookContentProvider(notebookType, contentProviderWrapper); - } + this.disposable = vscode.notebook.registerNotebookSerializer(notebookType, this); } dispose(): void { @@ -76,7 +69,7 @@ abstract class DotNetNotebookSerializer implements vscode.NotebookSerializer { } private getClient(): Promise { - return this.clientMapper.getOrAddClient({ fsPath: this.serializerId }); + return this.clientMapper.getOrAddClient(createUri(this.serializerId)); } private getNotebookName(): string { @@ -95,7 +88,13 @@ function toNotebookCell(cell: vscode.NotebookCellData): contracts.NotebookCell { export class DotNetDibNotebookSerializer extends DotNetNotebookSerializer { constructor(clientMapper: ClientMapper, outputChannel: OutputChannelAdapter) { - super('dotnet-interactive', false, clientMapper, outputChannel, '.dib'); + super('dotnet-interactive', clientMapper, outputChannel, '.dib'); + } +} + +export class DotNetLegacyNotebookSerializer extends DotNetNotebookSerializer { + constructor(clientMapper: ClientMapper, outputChannel: OutputChannelAdapter) { + super('dotnet-interactive-legacy', clientMapper, outputChannel, '.dib'); } } diff --git a/src/dotnet-interactive-vscode/insiders/src/versionSpecificFunctions.ts b/src/dotnet-interactive-vscode/insiders/src/versionSpecificFunctions.ts index bccf7e1320..890f35bba4 100644 --- a/src/dotnet-interactive-vscode/insiders/src/versionSpecificFunctions.ts +++ b/src/dotnet-interactive-vscode/insiders/src/versionSpecificFunctions.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import * as contracts from './common/interfaces/contracts'; import * as vscodeLike from './common/interfaces/vscode-like'; import * as interactiveNotebook from './common/interactiveNotebook'; +import * as ipynbUtilities from './common/ipynbUtilities'; import * as utilities from './common/utilities'; import * as diagnostics from './common/vscode/diagnostics'; import * as vscodeUtilities from './common/vscode/vscodeUtilities'; @@ -12,28 +13,70 @@ import * as notebookControllers from './notebookControllers'; import * as notebookSerializers from './notebookSerializers'; import { ClientMapper } from './common/clientMapper'; import { OutputChannelAdapter } from './common/vscode/OutputChannelAdapter'; +import { ErrorOutputCreator } from './common/interactiveClient'; -export function cellAt(document: vscode.NotebookDocument, index: number): vscode.NotebookCell { - return document.cellAt(index); +export function registerWithVsCode(context: vscode.ExtensionContext, clientMapper: ClientMapper, outputChannel: OutputChannelAdapter, createErrorOutput: ErrorOutputCreator, ...preloadUris: vscode.Uri[]) { + const config = { + clientMapper, + preloadUris, + createErrorOutput, + }; + context.subscriptions.push(new notebookControllers.DotNetNotebookKernel(config)); + context.subscriptions.push(new notebookSerializers.DotNetDibNotebookSerializer(clientMapper, outputChannel)); + context.subscriptions.push(new notebookSerializers.DotNetLegacyNotebookSerializer(clientMapper, outputChannel)); } -export function cellCount(document: vscode.NotebookDocument): number { - return document.cellCount; +export function endExecution(cell: vscode.NotebookCell, success: boolean) { + notebookControllers.endExecution(cell, success); } -export function getCells(document: vscode.NotebookDocument | undefined): Array { - if (document) { - return [...document.getCells()]; - } - - return []; +export function createErrorOutput(message: string, outputId?: string): vscodeLike.NotebookCellOutput { + const error = { name: 'Error', message }; + const errorItem = vscode.NotebookCellOutputItem.error(error); + const cellOutput = utilities.createOutput([errorItem], outputId); + return cellOutput; } -export function registerWithVsCode(context: vscode.ExtensionContext, clientMapper: ClientMapper, outputChannel: OutputChannelAdapter, ...preloadUris: vscode.Uri[]) { - context.subscriptions.push(new notebookControllers.DotNetNotebookKernel(clientMapper, preloadUris)); - context.subscriptions.push(new notebookSerializers.DotNetDibNotebookSerializer(clientMapper, outputChannel)); -} +export async function createNewBlankNotebook(extension: string, _openNotebook: (uri: vscode.Uri) => Promise): Promise { + const viewType = extension === '.dib' || extension === '.dotnet-interactive' + ? 'dotnet-interactive' + : interactiveNotebook.jupyterViewType; -export function endExecution(cell: vscode.NotebookCell, success: boolean) { - notebookControllers.endExecution(cell, success); + // get language + const newNotebookCSharp = `C#`; + const newNotebookFSharp = `F#`; + const newNotebookPowerShell = `PowerShell`; + const notebookLanguage = await vscode.window.showQuickPick([newNotebookCSharp, newNotebookFSharp, newNotebookPowerShell], { title: 'Default Language' }); + if (!notebookLanguage) { + return; + } + + const ipynbLanguageName = ipynbUtilities.mapIpynbLanguageName(notebookLanguage); + const cellMetadata = new vscode.NotebookCellMetadata().with({ + custom: { + metadata: { + dotnet_interactive: { + language: ipynbLanguageName + } + } + } + }); + const cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, '', `dotnet-interactive.${ipynbLanguageName}`, undefined, cellMetadata); + const documentMetadata = new vscode.NotebookDocumentMetadata().with({ + custom: { + metadata: { + kernelspec: { + display_name: `.NET (${notebookLanguage})`, + language: notebookLanguage, + name: `.net-${ipynbLanguageName}` + }, + language_info: { + name: notebookLanguage + } + } + } + }); + const content = new vscode.NotebookData([cell], documentMetadata); + const notebook = await vscode.notebook.openNotebookDocument(viewType, content); + const _editor = await vscode.window.showNotebookDocument(notebook); } diff --git a/src/dotnet-interactive-vscode/insiders/src/vscode.d.ts b/src/dotnet-interactive-vscode/insiders/src/vscode.d.ts index f0b2bf92df..6ba49ea6a2 100644 --- a/src/dotnet-interactive-vscode/insiders/src/vscode.d.ts +++ b/src/dotnet-interactive-vscode/insiders/src/vscode.d.ts @@ -1154,7 +1154,9 @@ declare module 'vscode' { /** * Adds a set of decorations to the text editor. If a set of decorations already exists with - * the given {@link TextEditorDecorationType decoration type}, they will be replaced. + * the given {@link TextEditorDecorationType decoration type}, they will be replaced. If + * `rangesOrOptions` is empty, the existing decorations with the given {@link TextEditorDecorationType decoration type} + * will be removed. * * @see {@link window.createTextEditorDecorationType createTextEditorDecorationType}. * @@ -1312,6 +1314,15 @@ declare module 'vscode' { */ static joinPath(base: Uri, ...pathSegments: string[]): Uri; + /** + * Create an URI from its component parts + * + * @see {@link Uri.toString} + * @param components The component parts of an Uri. + * @return A new Uri instance. + */ + static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri; + /** * Use the `file` and `parse` factory functions to create new `Uri` objects. */ @@ -2050,7 +2061,7 @@ declare module 'vscode' { * * Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`. * - * Code action kinds are used by VS Code for UI elements such as the refactoring context menu. Users + * Code action kinds are used by the editor for UI elements such as the refactoring context menu. Users * can also trigger code actions with a specific kind with the `editor.action.codeAction` command. */ export class CodeActionKind { @@ -2236,7 +2247,7 @@ declare module 'vscode' { /** * A {@link Command} this code action executes. * - * If this command throws an exception, VS Code displays the exception message to users in the editor at the + * If this command throws an exception, the editor displays the exception message to users in the editor at the * current cursor position. */ command?: Command; @@ -2267,7 +2278,7 @@ declare module 'vscode' { * of code action, such as refactorings. * * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) - * that auto applies a code action and only a disabled code actions are returned, VS Code will show the user an + * that auto applies a code action and only a disabled code actions are returned, the editor will show the user an * error message with `reason` in the editor. */ disabled?: { @@ -2344,17 +2355,17 @@ declare module 'vscode' { * list of kinds may either be generic, such as `[CodeActionKind.Refactor]`, or list out every kind provided, * such as `[CodeActionKind.Refactor.Extract.append('function'), CodeActionKind.Refactor.Extract.append('constant'), ...]`. */ - readonly providedCodeActionKinds?: ReadonlyArray; + readonly providedCodeActionKinds?: readonly CodeActionKind[]; /** * Static documentation for a class of code actions. * * Documentation from the provider is shown in the code actions menu if either: * - * - Code actions of `kind` are requested by VS Code. In this case, VS Code will show the documentation that + * - Code actions of `kind` are requested by the editor. In this case, the editor will show the documentation that * most closely matches the requested code action kind. For example, if a provider has documentation for * both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`, - * VS Code will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. + * the editor will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. * * - Any code actions of `kind` are returned by the provider. * @@ -2373,7 +2384,7 @@ declare module 'vscode' { /** * Command that displays the documentation to the user. * - * This can display the documentation directly in VS Code or open a website using {@link env.openExternal `env.openExternal`}; + * This can display the documentation directly in the editor or open a website using {@link env.openExternal `env.openExternal`}; * * The title of this documentation code action is taken from {@link Command.title `Command.title`} */ @@ -2685,13 +2696,13 @@ declare module 'vscode' { /** * The evaluatable expression provider interface defines the contract between extensions and * the debug hover. In this contract the provider returns an evaluatable expression for a given position - * in a document and VS Code evaluates this expression in the active debug session and shows the result in a debug hover. + * in a document and the editor evaluates this expression in the active debug session and shows the result in a debug hover. */ export interface EvaluatableExpressionProvider { /** * Provide an evaluatable expression for the given document and position. - * VS Code will evaluate this expression in the active debug session and will show the result in the debug hover. + * The editor will evaluate this expression in the active debug session and will show the result in the debug hover. * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. * * @param document The document for which the debug hover is about to appear. @@ -5573,6 +5584,14 @@ declare module 'vscode' { */ export interface StatusBarItem { + /** + * The identifier of this item. + * + * *Note*: if no identifier was provided by the {@link window.createStatusBarItem `window.createStatusBarItem`} + * method, the identifier will match the {@link Extension.id extension identifier}. + */ + readonly id: string; + /** * The alignment of this item. */ @@ -5584,6 +5603,13 @@ declare module 'vscode' { */ readonly priority?: number; + /** + * The name of the entry, like 'Python Language Indicator', 'Git Status' etc. + * Try to keep the length of the name short, yet descriptive enough that + * users can understand what the status bar item is about. + */ + name: string | undefined; + /** * The text to show for the entry. You can embed icons in the text by leveraging the syntax: * @@ -5993,7 +6019,8 @@ declare module 'vscode' { }; /** - * A storage utility for secrets. + * A storage utility for secrets. Secrets are persisted across reloads and are independent of the + * current opened {@link workspace.workspaceFolders workspace}. */ readonly secrets: SecretStorage; @@ -7755,7 +7782,7 @@ declare module 'vscode' { /** * Event triggered by extensions to signal to VS Code that an edit has occurred on an {@link CustomDocument `CustomDocument`}. * - * @see {@link CustomDocumentProvider.onDidChangeCustomDocument `CustomDocumentProvider.onDidChangeCustomDocument`}. + * @see {@link CustomEditorProvider.onDidChangeCustomDocument `CustomEditorProvider.onDidChangeCustomDocument`}. */ interface CustomDocumentEditEvent { @@ -7794,7 +7821,7 @@ declare module 'vscode' { * Event triggered by extensions to signal to VS Code that the content of a {@link CustomDocument `CustomDocument`} * has changed. * - * @see {@link CustomDocumentProvider.onDidChangeCustomDocument `CustomDocumentProvider.onDidChangeCustomDocument`}. + * @see {@link CustomEditorProvider.onDidChangeCustomDocument `CustomEditorProvider.onDidChangeCustomDocument`}. */ interface CustomDocumentContentChangeEvent { /** @@ -8792,6 +8819,16 @@ declare module 'vscode' { */ export function createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem; + /** + * Creates a status bar {@link StatusBarItem item}. + * + * @param id The unique identifier of the item. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createStatusBarItem(id: string, alignment?: StatusBarAlignment, priority?: number): StatusBarItem; + /** * Creates a {@link Terminal} with a backing shell process. The cwd of the terminal will be the workspace * directory if it exists. @@ -8899,7 +8936,7 @@ declare module 'vscode' { * * Normally the webview's html context is created when the view becomes visible * and destroyed when it is hidden. Extensions that have complex state - * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview + * or UI can set the `retainContextWhenHidden` to make the editor keep the webview * context around, even when the webview moves to a background tab. When a webview using * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. * When the view becomes visible again, the context is automatically restored @@ -8916,7 +8953,7 @@ declare module 'vscode' { /** * Register a provider for custom editors for the `viewType` contributed by the `customEditors` extension point. * - * When a custom editor is opened, VS Code fires an `onCustomEditor:viewType` activation event. Your extension + * When a custom editor is opened, an `onCustomEditor:viewType` activation event is fired. Your extension * must register a {@link CustomTextEditorProvider `CustomTextEditorProvider`}, {@link CustomReadonlyEditorProvider `CustomReadonlyEditorProvider`}, * {@link CustomEditorProvider `CustomEditorProvider`}for `viewType` as part of activation. * @@ -8939,7 +8976,7 @@ declare module 'vscode' { * Indicates that the provider allows multiple editor instances to be open at the same time for * the same resource. * - * By default, VS Code only allows one editor instance to be open at a time for each resource. If the + * By default, the editor only allows one editor instance to be open at a time for each resource. If the * user tries to open a second editor instance for the resource, the first one is instead moved to where * the second one was to be opened. * @@ -10305,7 +10342,7 @@ declare module 'vscode' { export const rootPath: string | undefined; /** - * List of workspace folders that are open in VS Code. `undefined when no workspace + * List of workspace folders that are open in VS Code. `undefined` when no workspace * has been opened. * * Refer to https://code.visualstudio.com/docs/editor/workspaces for more information @@ -11570,6 +11607,12 @@ declare module 'vscode' { */ readonly type: string; + /** + * The parent session of this debug session, if it was created as a child. + * @see DebugSessionOptions.parentSession + */ + readonly parentSession?: DebugSession; + /** * The debug session's name is initially taken from the {@link DebugConfiguration debug configuration}. * Any changes will be properly reflected in the UI. @@ -12727,6 +12770,420 @@ declare module 'vscode' { */ export function registerAuthenticationProvider(id: string, label: string, provider: AuthenticationProvider, options?: AuthenticationProviderOptions): Disposable; } + + /** + * Namespace for testing functionality. + */ + export namespace test { + /** + * Registers a controller that can discover and + * run tests in workspaces and documents. + */ + export function registerTestController(testController: TestController): Disposable; + + /** + * Requests that tests be run by their controller. + * @param run Run options to use + * @param token Cancellation token for the test run + */ + export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable; + + /** + * Creates a {@link TestRun}. This should be called by the + * {@link TestRunner} when a request is made to execute tests, and may also + * be called if a test run is detected externally. Once created, tests + * that are included in the results will be moved into the + * {@link TestResultState.Pending} state. + * + * @param request Test run request. Only tests inside the `include` may be + * modified, and tests in its `exclude` are ignored. + * @param name The human-readable name of the run. This can be used to + * disambiguate multiple sets of results in a test run. It is useful if + * tests are run across multiple platforms, for example. + * @param persist Whether the results created by the run should be + * persisted in VS Code. This may be false if the results are coming from + * a file already saved externally, such as a coverage information file. + */ + export function createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun; + + /** + * Creates a new managed {@link TestItem} instance. + * @param options Initial/required options for the item + * @param data Custom data to be stored in {@link TestItem.data} + */ + export function createTestItem(options: TestItemOptions, data: T): TestItem; + + /** + * Creates a new managed {@link TestItem} instance. + * @param options Initial/required options for the item + */ + export function createTestItem(options: TestItemOptions): TestItem; + } + + /** + * Interface to discover and execute tests. + */ + export interface TestController { + /** + * Requests that tests be provided for the given workspace. This will + * be called when tests need to be enumerated for the workspace, such as + * when the user opens the test explorer. + * + * It's guaranteed that this method will not be called again while + * there is a previous uncancelled call for the given workspace folder. + * + * @param workspace The workspace in which to observe tests + * @param cancellationToken Token that signals the used asked to abort the test run. + * @returns the root test item for the workspace + */ + createWorkspaceTestRoot(workspace: WorkspaceFolder, token: CancellationToken): ProviderResult>; + + /** + * Requests that tests be provided for the given document. This will be + * called when tests need to be enumerated for a single open file, for + * instance by code lens UI. + * + * It's suggested that the provider listen to change events for the text + * document to provide information for tests that might not yet be + * saved. + * + * If the test system is not able to provide or estimate for tests on a + * per-file basis, this method may not be implemented. In that case, the + * editor will request and use the information from the workspace tree. + * + * @param document The document in which to observe tests + * @param cancellationToken Token that signals the used asked to abort the test run. + * @returns the root test item for the document + */ + createDocumentTestRoot?(document: TextDocument, token: CancellationToken): ProviderResult>; + + /** + * Starts a test run. When called, the controller should call + * {@link vscode.test.createTestRun}. All tasks associated with the + * run should be created before the function returns or the reutrned + * promise is resolved. + * + * @param options Options for this test run + * @param cancellationToken Token that signals the used asked to abort the test run. + */ + runTests(options: TestRunRequest, token: CancellationToken): Thenable | void; + } + + /** + * Options given to {@link test.runTests}. + */ + export interface TestRunRequest { + /** + * Array of specific tests to run. The controllers should run all of the + * given tests and all children of the given tests, excluding any tests + * that appear in {@link TestRunRequest.exclude}. + */ + tests: TestItem[]; + + /** + * An array of tests the user has marked as excluded in VS Code. May be + * omitted if no exclusions were requested. Test controllers should not run + * excluded tests or any children of excluded tests. + */ + exclude?: TestItem[]; + + /** + * Whether tests in this run should be debugged. + */ + debug: boolean; + } + + /** + * Options given to {@link TestController.runTests} + */ + export interface TestRun { + /** + * The human-readable name of the run. This can be used to + * disambiguate multiple sets of results in a test run. It is useful if + * tests are run across multiple platforms, for example. + */ + readonly name?: string; + + /** + * Updates the state of the test in the run. Calling with method with nodes + * outside the {@link TestRunRequest.tests} or in the + * {@link TestRunRequest.exclude} array will no-op. + * + * @param test The test to update + * @param state The state to assign to the test + * @param duration Optionally sets how long the test took to run + */ + setState(test: TestItem, state: TestResultState, duration?: number): void; + + /** + * Appends a message, such as an assertion error, to the test item. + * + * Calling with method with nodes outside the {@link TestRunRequest.tests} + * or in the {@link TestRunRequest.exclude} array will no-op. + * + * @param test The test to update + * @param state The state to assign to the test + * + */ + appendMessage(test: TestItem, message: TestMessage): void; + + /** + * Appends raw output from the test runner. On the user's request, the + * output will be displayed in a terminal. ANSI escape sequences, + * such as colors and text styles, are supported. + * + * @param output Output text to append + * @param associateTo Optionally, associate the given segment of output + */ + appendOutput(output: string): void; + + /** + * Signals that the end of the test run. Any tests whose states have not + * been updated will be moved into the {@link TestResultState.Unset} state. + */ + end(): void; + } + + /** + * Indicates the the activity state of the {@link TestItem}. + */ + export enum TestItemStatus { + /** + * All children of the test item, if any, have been discovered. + */ + Resolved = 1, + + /** + * The test item may have children who have not been discovered yet. + */ + Pending = 0, + } + + /** + * Options initially passed into `vscode.test.createTestItem` + */ + export interface TestItemOptions { + /** + * Unique identifier for the TestItem. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This cannot change for the lifetime of the TestItem. + */ + id: string; + + /** + * URI this TestItem is associated with. May be a file or directory. + */ + uri?: Uri; + + /** + * Display name describing the test item. + */ + label: string; + } + + /** + * A test item is an item shown in the "test explorer" view. It encompasses + * both a suite and a test, since they have almost or identical capabilities. + */ + export interface TestItem { + /** + * Unique identifier for the TestItem. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This must not change for the lifetime of the TestItem. + */ + readonly id: string; + + /** + * URI this TestItem is associated with. May be a file or directory. + */ + readonly uri?: Uri; + + /** + * A mapping of children by ID to the associated TestItem instances. + */ + readonly children: ReadonlyMap>; + + /** + * The parent of this item, if any. Assigned automatically when calling + * {@link TestItem.addChild}. + */ + readonly parent?: TestItem; + + /** + * Indicates the state of the test item's children. The editor will show + * TestItems in the `Pending` state and with a `resolveHandler` as being + * expandable, and will call the `resolveHandler` to request items. + * + * A TestItem in the `Resolved` state is assumed to have discovered and be + * watching for changes in its children if applicable. TestItems are in the + * `Resolved` state when initially created; if the editor should call + * the `resolveHandler` to discover children, set the state to `Pending` + * after creating the item. + */ + status: TestItemStatus; + + /** + * Display name describing the test case. + */ + label: string; + + /** + * Optional description that appears next to the label. + */ + description?: string; + + /** + * Location of the test item in its `uri`. This is only meaningful if the + * `uri` points to a file. + */ + range?: Range; + + /** + * May be set to an error associated with loading the test. Note that this + * is not a test result and should only be used to represent errors in + * discovery, such as syntax errors. + */ + error?: string | MarkdownString; + + /** + * Whether this test item can be run by providing it in the + * {@link TestRunRequest.tests} array. Defaults to `true`. + */ + runnable: boolean; + + /** + * Whether this test item can be debugged by providing it in the + * {@link TestRunRequest.tests} array. Defaults to `false`. + */ + debuggable: boolean; + + /** + * Custom extension data on the item. This data will never be serialized + * or shared outside the extenion who created the item. + */ + data: T; + + /** + * Marks the test as outdated. This can happen as a result of file changes, + * for example. In "auto run" mode, tests that are outdated will be + * automatically rerun after a short delay. Invoking this on a + * test with children will mark the entire subtree as outdated. + * + * Extensions should generally not override this method. + */ + invalidate(): void; + + /** + * A function provided by the extension that the editor may call to request + * children of the item, if the {@link TestItem.status} is `Pending`. + * + * When called, the item should discover tests and call {@link TestItem.addChild}. + * The items should set its {@link TestItem.status} to `Resolved` when + * discovery is finished. + * + * The item should continue watching for changes to the children and + * firing updates until the token is cancelled. The process of watching + * the tests may involve creating a file watcher, for example. After the + * token is cancelled and watching stops, the TestItem should set its + * {@link TestItem.status} back to `Pending`. + * + * The editor will only call this method when it's interested in refreshing + * the children of the item, and will not call it again while there's an + * existing, uncancelled discovery for an item. + * + * @param token Cancellation for the request. Cancellation will be + * requested if the test changes before the previous call completes. + */ + resolveHandler?: (token: CancellationToken) => void; + + /** + * Attaches a child, created from the {@link test.createTestItem} function, + * to this item. A `TestItem` may be a child of at most one other item. + */ + addChild(child: TestItem): void; + + /** + * Removes the test and its children from the tree. Any tokens passed to + * child `resolveHandler` methods will be cancelled. + */ + dispose(): void; + } + + /** + * Possible states of tests in a test run. + */ + export enum TestResultState { + // Initial state + Unset = 0, + // Test will be run, but is not currently running. + Queued = 1, + // Test is currently running + Running = 2, + // Test run has passed + Passed = 3, + // Test run has failed (on an assertion) + Failed = 4, + // Test run has been skipped + Skipped = 5, + // Test run failed for some other reason (compilation error, timeout, etc) + Errored = 6 + } + + /** + * Represents the severity of test messages. + */ + export enum TestMessageSeverity { + Error = 0, + Warning = 1, + Information = 2, + Hint = 3 + } + + /** + * Message associated with the test state. Can be linked to a specific + * source range -- useful for assertion failures, for example. + */ + export class TestMessage { + /** + * Human-readable message text to display. + */ + message: string | MarkdownString; + + /** + * Message severity. Defaults to "Error". + */ + severity: TestMessageSeverity; + + /** + * Expected test output. If given with `actualOutput`, a diff view will be shown. + */ + expectedOutput?: string; + + /** + * Actual test output. If given with `expectedOutput`, a diff view will be shown. + */ + actualOutput?: string; + + /** + * Associated file location. + */ + location?: Location; + + /** + * Creates a new TestMessage that will present as a diff in the editor. + * @param message Message to display to the user. + * @param expected Expected output. + * @param actual Actual output. + */ + static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage; + + /** + * Creates a new TestMessage instance. + * @param message The message to show to the user. + */ + constructor(message: string | MarkdownString); + } + } /** diff --git a/src/dotnet-interactive-vscode/insiders/src/vscode.proposed.d.ts b/src/dotnet-interactive-vscode/insiders/src/vscode.proposed.d.ts index c61cff5377..94096b84ec 100644 --- a/src/dotnet-interactive-vscode/insiders/src/vscode.proposed.d.ts +++ b/src/dotnet-interactive-vscode/insiders/src/vscode.proposed.d.ts @@ -80,16 +80,10 @@ declare module 'vscode' { constructor(host: string, port: number, connectionToken?: string); } - export enum RemoteTrustOption { - Unknown = 0, - DisableTrust = 1, - MachineTrusted = 2 - } - export interface ResolvedOptions { extensionHostEnv?: { [key: string]: string | null; }; - trust?: RemoteTrustOption; + isTrusted?: boolean; } export interface TunnelOptions { @@ -150,7 +144,24 @@ declare module 'vscode' { } export interface RemoteAuthorityResolver { + /** + * Resolve the authority part of the current opened `vscode-remote://` URI. + * + * This method will be invoked once during the startup of VS Code and again each time + * VS Code detects a disconnection. + * + * @param authority The authority part of the current opened `vscode-remote://` URI. + * @param context A context indicating if this is the first call or a subsequent call. + */ resolve(authority: string, context: RemoteAuthorityResolverContext): ResolverResult | Thenable; + + /** + * Get the canonical URI (if applicable) for a `vscode-remote://` URI. + * + * @returns The canonical URI or undefined if the uri is already canonical. + */ + getCanonicalURI?(uri: Uri): ProviderResult; + /** * Can be optionally implemented if the extension can forward ports better than the core. * When not implemented, the core will use its default forwarding logic. @@ -392,6 +403,25 @@ declare module 'vscode' { Warning = 2, } + /** + * A message regarding a completed search. + */ + export interface TextSearchCompleteMessage { + /** + * Markdown text of the message. + */ + text: string, + /** + * Whether the source of the message is trusted, command links are disabled for untrusted message sources. + * Messaged are untrusted by default. + */ + trusted?: boolean, + /** + * The message type, this affects how the message will be rendered. + */ + type: TextSearchCompleteMessageType, + } + /** * Information collected when text search is complete. */ @@ -411,8 +441,10 @@ declare module 'vscode' { * Messages with "Information" tyle support links in markdown syntax: * - Click to [run a command](command:workbench.action.OpenQuickPick) * - Click to [open a website](https://aka.ms) + * + * Commands may optionally return { triggerSearch: true } to signal to VS Code that the original search should run be agian. */ - message?: { text: string, type: TextSearchCompleteMessageType } | { text: string, type: TextSearchCompleteMessageType }[]; + message?: TextSearchCompleteMessage | TextSearchCompleteMessage[]; } /** @@ -870,9 +902,16 @@ declare module 'vscode' { export interface TerminalOptions { /** - * A codicon ID to associate with this terminal. + * The icon path or {@link ThemeIcon} for the terminal. */ - readonly icon?: string; + readonly iconPath?: Uri | { light: Uri; dark: Uri } | { id: string, color?: { id: string } }; + } + + export interface ExtensionTerminalOptions { + /** + * A themeIcon, Uri, or light and dark Uris to use as the terminal tab icon + */ + readonly iconPath?: Uri | { light: Uri; dark: Uri } | { id: string, color?: { id: string } }; } //#endregion @@ -894,24 +933,18 @@ declare module 'vscode' { //#region Custom Tree View Drag and Drop https://github.com/microsoft/vscode/issues/32592 export interface TreeViewOptions { - /** - * * Whether the tree supports drag and drop. - */ - canDragAndDrop?: boolean; + dragAndDropController?: DragAndDropController; } - export interface TreeDataProvider { + export interface DragAndDropController extends Disposable { /** - * Optional method to reparent an `element`. + * Extensions should fire `TreeDataProvider.onDidChangeTreeData` for any elements that need to be refreshed. * - * **NOTE:** This method should be implemented if the tree supports drag and drop. - * - * @param elements The selected elements that will be reparented. - * @param targetElement The new parent of the elements. + * @param source + * @param target */ - setParent?(elements: T[], targetElement: T): Thenable; + onDrop(source: T[], target: T): Thenable; } - //#endregion //#region Task presentation group: https://github.com/microsoft/vscode/issues/47265 @@ -923,59 +956,6 @@ declare module 'vscode' { } //#endregion - //#region Status bar item with ID and Name: https://github.com/microsoft/vscode/issues/74972 - - /** - * Options to configure the status bar item. - */ - export interface StatusBarItemOptions { - - /** - * A unique identifier of the status bar item. The identifier - * is for example used to allow a user to show or hide the - * status bar item in the UI. - */ - id: string; - - /** - * A human readable name of the status bar item. The name is - * for example used as a label in the UI to show or hide the - * status bar item. - */ - name: string; - - /** - * Accessibility information used when screen reader interacts with this status bar item. - */ - accessibilityInformation?: AccessibilityInformation; - - /** - * The alignment of the status bar item. - */ - alignment?: StatusBarAlignment; - - /** - * The priority of the status bar item. Higher value means the item should - * be shown more to the left. - */ - priority?: number; - } - - export namespace window { - - /** - * Creates a status bar {@link StatusBarItem item}. - * - * @param options The options of the item. If not provided, some default values - * will be assumed. For example, the `StatusBarItemOptions.id` will be the id - * of the extension and the `StatusBarItemOptions.name` will be the extension name. - * @return A new status bar item. - */ - export function createStatusBarItem(options?: StatusBarItemOptions): StatusBarItem; - } - - //#endregion - //#region Custom editor move https://github.com/microsoft/vscode/issues/86146 // TODO: Also for custom editor @@ -1036,7 +1016,6 @@ declare module 'vscode' { * * NotebookCell instances are immutable and are kept in sync for as long as they are part of their notebook. */ - // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md export interface NotebookCell { /** @@ -1071,8 +1050,10 @@ declare module 'vscode' { */ readonly outputs: ReadonlyArray; - // todo@API maybe just executionSummary or lastExecutionSummary? - readonly latestExecutionSummary: NotebookCellExecutionSummary | undefined; + /** + * The most recent {@link NotebookCellExecutionSummary excution summary} for this cell. + */ + readonly executionSummary?: NotebookCellExecutionSummary; } /** @@ -1257,26 +1238,97 @@ declare module 'vscode' { with(change: { start?: number, end?: number }): NotebookRange; } - // code specific mime types - // application/x.notebook.error-traceback - // application/x.notebook.stdout - // application/x.notebook.stderr - // application/x.notebook.stream + // todo@API document which mime types are supported out of the box and + // which are considered secure export class NotebookCellOutputItem { - // todo@API - // add factory functions for common mime types - // static textplain(value:string): NotebookCellOutputItem; - // static errortrace(value:any): NotebookCellOutputItem; + /** + * Factory function to create a `NotebookCellOutputItem` from a string. + * + * *Note* that an UTF-8 encoder is used to create bytes for the string. + * + * @param value A string/ + * @param mime Optional MIME type, defaults to `text/plain`. + * @returns A new output item object. + */ + static text(value: string, mime?: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` from + * a JSON object. + * + * *Note* that this function is not expecting "stringified JSON" but + * an object that can be stringified. This function will throw an error + * when the passed value cannot be JSON-stringified. + * + * @param value A JSON-stringifyable value. + * @param mime Optional MIME type, defaults to `application/json` + * @returns A new output item object. + */ + static json(value: any, mime?: string): NotebookCellOutputItem; + /** + * Factory function to create a `NotebookCellOutputItem` from bytes. + * + * @param value An array of unsigned 8-bit integers. + * @param mime Optional MIME type, defaults to `application/octet-stream`. + * @returns A new output item object. + */ + //todo@API better names: bytes, raw, buffer? + static bytes(value: Uint8Array, mime?: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.stdout` mime type. + * + * @param value A string. + * @returns A new output item object. + */ + static stdout(value: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.stderr` mime type. + * + * @param value A string. + * @returns A new output item object. + */ + static stderr(value: string): NotebookCellOutputItem; + + /** + * Factory function to create a `NotebookCellOutputItem` that uses + * uses the `application/vnd.code.notebook.error` mime type. + * + * @param value An error object. + * @returns A new output item object. + */ + static error(value: Error): NotebookCellOutputItem; + + /** + * The mime type which determines how the {@link NotebookCellOutputItem.value `value`}-property + * is interpreted. + * + * Notebooks have built-in support for certain mime-types, extensions can add support for new + * types and override existing types. + */ mime: string; - //todo@API string or Unit8Array? - value: unknown; + /** + * The value of this output item. Must always be an array of unsigned 8-bit integers. + */ + //todo@API only Unit8Array + value: Uint8Array | unknown; metadata?: { [key: string]: any }; - constructor(mime: string, value: unknown, metadata?: { [key: string]: any }); + /** + * Create a new notbook cell output item. + * + * @param mime The mime type of the output item. + * @param value The value of the output item. + * @param metadata Optional metadata for this output item. + */ + constructor(mime: string, value: Uint8Array | unknown, metadata?: { [key: string]: any }); } // @jrieken transient @@ -1291,7 +1343,6 @@ declare module 'vscode' { /** * NotebookCellData is the raw representation of notebook cells. Its is part of {@link NotebookData `NotebookData`}. */ - // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md export class NotebookCellData { /** @@ -1320,8 +1371,10 @@ declare module 'vscode' { */ metadata?: NotebookCellMetadata; - // todo@API just executionSummary or lastExecutionSummary - latestExecutionSummary?: NotebookCellExecutionSummary; + /** + * The execution summary of this cell data. + */ + executionSummary?: NotebookCellExecutionSummary; /** * Create new cell data. Minimal cell data specifies its kind, its source value, and the @@ -1332,9 +1385,9 @@ declare module 'vscode' { * @param languageId The language identifier of the source value. * @param outputs //TODO@API remove ctor? * @param metadata //TODO@API remove ctor? - * @param latestExecutionSummary //TODO@API remove ctor? + * @param executionSummary //TODO@API remove ctor? */ - constructor(kind: NotebookCellKind, value: string, languageId: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata, latestExecutionSummary?: NotebookCellExecutionSummary); + constructor(kind: NotebookCellKind, value: string, languageId: string, outputs?: NotebookCellOutput[], metadata?: NotebookCellMetadata, executionSummary?: NotebookCellExecutionSummary); } /** @@ -1705,6 +1758,17 @@ declare module 'vscode' { */ export function openNotebookDocument(uri: Uri): Thenable; + /** + * Open an untitled notebook. The editor will prompt the user for a file + * path when the document is to be saved. + * + * @see {@link openNotebookDocument} + * @param viewType The notebook view type that should be used. + * @param content The initial contents of the notebook. + * @returns A promise that resolves to a {@link NotebookDocument notebook}. + */ + export function openNotebookDocument(viewType: string, content?: NotebookData): Thenable; + /** * An event that is emitted when a {@link NotebookDocument notebook} is opened. */ @@ -2122,6 +2186,52 @@ declare module 'vscode' { //#endregion + //#region @connor4312 - notebook messaging: https://github.com/microsoft/vscode/issues/123601 + + export interface NotebookRendererMessage { + /** + * Editor that sent the message. + */ + editor: NotebookEditor; + + /** + * Message sent from the webview. + */ + message: T; + } + + /** + * Renderer messaging is used to communicate with a single renderer. It's + * returned from {@link notebook.createRendererMessaging}. + */ + export interface NotebookRendererMessaging { + /** + * Events that fires when a message is received from a renderer. + */ + onDidReceiveMessage: Event>; + + /** + * Sends a message to the renderer. + * @param editor Editor to target with the message + * @param message Message to send + */ + postMessage(editor: NotebookEditor, message: TSend): void; + } + + export namespace notebook { + /** + * Creates a new messaging instance used to communicate with a specific + * renderer. The renderer only has access to messaging if `requiresMessaging` + * is set in its contribution. + * + * @see https://github.com/microsoft/vscode/issues/123601 + * @param rendererId The renderer ID to communicate with + */ + export function createRendererMessaging(rendererId: string): NotebookRendererMessaging; + } + + //#endregion + //#region @eamodio - timeline: https://github.com/microsoft/vscode/issues/84297 export class TimelineItem { @@ -2414,19 +2524,6 @@ declare module 'vscode' { //#region https://github.com/microsoft/vscode/issues/107467 export namespace test { - /** - * Registers a controller that can discover and - * run tests in workspaces and documents. - */ - export function registerTestController(testController: TestController): Disposable; - - /** - * Requests that tests be run by their controller. - * @param run Run options to use - * @param token Cancellation token for the test run - */ - export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable; - /** * Returns an observer that retrieves tests in the given workspace folder. * @stability experimental @@ -2439,37 +2536,6 @@ declare module 'vscode' { */ export function createDocumentTestObserver(document: TextDocument): TestObserver; - /** - * Creates a {@link TestRun}. This should be called by the - * {@link TestRunner} when a request is made to execute tests, and may also - * be called if a test run is detected externally. Once created, tests - * that are included in the results will be moved into the - * {@link TestResultState.Pending} state. - * - * @param request Test run request. Only tests inside the `include` may be - * modified, and tests in its `exclude` are ignored. - * @param name The human-readable name of the run. This can be used to - * disambiguate multiple sets of results in a test run. It is useful if - * tests are run across multiple platforms, for example. - * @param persist Whether the results created by the run should be - * persisted in VS Code. This may be false if the results are coming from - * a file already saved externally, such as a coverage information file. - */ - export function createTestRun(request: TestRunRequest, name?: string, persist?: boolean): TestRun; - - /** - * Creates a new managed {@link TestItem} instance. - * @param options Initial/required options for the item - * @param data Custom data to be stored in {@link TestItem.data} - */ - export function createTestItem(options: TestItemOptions, data: T): TestItem; - - /** - * Creates a new managed {@link TestItem} instance. - * @param options Initial/required options for the item - */ - export function createTestItem(options: TestItemOptions): TestItem; - /** * List of test results stored by VS Code, sorted in descnding * order by their `completedAt` time. @@ -2537,370 +2603,6 @@ declare module 'vscode' { readonly removed: ReadonlyArray>; } - /** - * Interface to discover and execute tests. - */ - export interface TestController { - /** - * Requests that tests be provided for the given workspace. This will - * be called when tests need to be enumerated for the workspace, such as - * when the user opens the test explorer. - * - * It's guaranteed that this method will not be called again while - * there is a previous uncancelled call for the given workspace folder. - * - * @param workspace The workspace in which to observe tests - * @param cancellationToken Token that signals the used asked to abort the test run. - * @returns the root test item for the workspace - */ - createWorkspaceTestRoot(workspace: WorkspaceFolder, token: CancellationToken): ProviderResult>; - - /** - * Requests that tests be provided for the given document. This will be - * called when tests need to be enumerated for a single open file, for - * instance by code lens UI. - * - * It's suggested that the provider listen to change events for the text - * document to provide information for tests that might not yet be - * saved. - * - * If the test system is not able to provide or estimate for tests on a - * per-file basis, this method may not be implemented. In that case, the - * editor will request and use the information from the workspace tree. - * - * @param document The document in which to observe tests - * @param cancellationToken Token that signals the used asked to abort the test run. - * @returns the root test item for the document - */ - createDocumentTestRoot?(document: TextDocument, token: CancellationToken): ProviderResult>; - - /** - * Starts a test run. When called, the controller should call - * {@link vscode.test.createTestRun}. All tasks associated with the - * run should be created before the function returns or the reutrned - * promise is resolved. - * - * @param options Options for this test run - * @param cancellationToken Token that signals the used asked to abort the test run. - */ - runTests(options: TestRunRequest, token: CancellationToken): Thenable | void; - } - - /** - * Options given to {@link test.runTests}. - */ - export interface TestRunRequest { - /** - * Array of specific tests to run. The controllers should run all of the - * given tests and all children of the given tests, excluding any tests - * that appear in {@link TestRunRequest.exclude}. - */ - tests: TestItem[]; - - /** - * An array of tests the user has marked as excluded in VS Code. May be - * omitted if no exclusions were requested. Test controllers should not run - * excluded tests or any children of excluded tests. - */ - exclude?: TestItem[]; - - /** - * Whether tests in this run should be debugged. - */ - debug: boolean; - } - - /** - * Options given to {@link TestController.runTests} - */ - export interface TestRun { - /** - * The human-readable name of the run. This can be used to - * disambiguate multiple sets of results in a test run. It is useful if - * tests are run across multiple platforms, for example. - */ - readonly name?: string; - - /** - * Updates the state of the test in the run. Calling with method with nodes - * outside the {@link TestRunRequest.tests} or in the - * {@link TestRunRequest.exclude} array will no-op. - * - * @param test The test to update - * @param state The state to assign to the test - * @param duration Optionally sets how long the test took to run - */ - setState(test: TestItem, state: TestResultState, duration?: number): void; - - /** - * Appends a message, such as an assertion error, to the test item. - * - * Calling with method with nodes outside the {@link TestRunRequest.tests} - * or in the {@link TestRunRequest.exclude} array will no-op. - * - * @param test The test to update - * @param state The state to assign to the test - * - */ - appendMessage(test: TestItem, message: TestMessage): void; - - /** - * Appends raw output from the test runner. On the user's request, the - * output will be displayed in a terminal. ANSI escape sequences, - * such as colors and text styles, are supported. - * - * @param output Output text to append - * @param associateTo Optionally, associate the given segment of output - */ - appendOutput(output: string): void; - - /** - * Signals that the end of the test run. Any tests whose states have not - * been updated will be moved into the {@link TestResultState.Unset} state. - */ - end(): void; - } - - /** - * Indicates the the activity state of the {@link TestItem}. - */ - export enum TestItemStatus { - /** - * All children of the test item, if any, have been discovered. - */ - Resolved = 1, - - /** - * The test item may have children who have not been discovered yet. - */ - Pending = 0, - } - - /** - * Options initially passed into `vscode.test.createTestItem` - */ - export interface TestItemOptions { - /** - * Unique identifier for the TestItem. This is used to correlate - * test results and tests in the document with those in the workspace - * (test explorer). This cannot change for the lifetime of the TestItem. - */ - id: string; - - /** - * URI this TestItem is associated with. May be a file or directory. - */ - uri?: Uri; - - /** - * Display name describing the test item. - */ - label: string; - } - - /** - * A test item is an item shown in the "test explorer" view. It encompasses - * both a suite and a test, since they have almost or identical capabilities. - */ - export interface TestItem { - /** - * Unique identifier for the TestItem. This is used to correlate - * test results and tests in the document with those in the workspace - * (test explorer). This must not change for the lifetime of the TestItem. - */ - readonly id: string; - - /** - * URI this TestItem is associated with. May be a file or directory. - */ - readonly uri?: Uri; - - /** - * A mapping of children by ID to the associated TestItem instances. - */ - readonly children: ReadonlyMap>; - - /** - * The parent of this item, if any. Assigned automatically when calling - * {@link TestItem.addChild}. - */ - readonly parent?: TestItem; - - /** - * Indicates the state of the test item's children. The editor will show - * TestItems in the `Pending` state and with a `resolveHandler` as being - * expandable, and will call the `resolveHandler` to request items. - * - * A TestItem in the `Resolved` state is assumed to have discovered and be - * watching for changes in its children if applicable. TestItems are in the - * `Resolved` state when initially created; if the editor should call - * the `resolveHandler` to discover children, set the state to `Pending` - * after creating the item. - */ - status: TestItemStatus; - - /** - * Display name describing the test case. - */ - label: string; - - /** - * Optional description that appears next to the label. - */ - description?: string; - - /** - * Location of the test item in its `uri`. This is only meaningful if the - * `uri` points to a file. - */ - range?: Range; - - /** - * May be set to an error associated with loading the test. Note that this - * is not a test result and should only be used to represent errors in - * discovery, such as syntax errors. - */ - error?: string | MarkdownString; - - /** - * Whether this test item can be run by providing it in the - * {@link TestRunRequest.tests} array. Defaults to `true`. - */ - runnable: boolean; - - /** - * Whether this test item can be debugged by providing it in the - * {@link TestRunRequest.tests} array. Defaults to `false`. - */ - debuggable: boolean; - - /** - * Custom extension data on the item. This data will never be serialized - * or shared outside the extenion who created the item. - */ - data: T; - - /** - * Marks the test as outdated. This can happen as a result of file changes, - * for example. In "auto run" mode, tests that are outdated will be - * automatically rerun after a short delay. Invoking this on a - * test with children will mark the entire subtree as outdated. - * - * Extensions should generally not override this method. - */ - invalidate(): void; - - /** - * A function provided by the extension that the editor may call to request - * children of the item, if the {@link TestItem.status} is `Pending`. - * - * When called, the item should discover tests and call {@link TestItem.addChild}. - * The items should set its {@link TestItem.status} to `Resolved` when - * discovery is finished. - * - * The item should continue watching for changes to the children and - * firing updates until the token is cancelled. The process of watching - * the tests may involve creating a file watcher, for example. After the - * token is cancelled and watching stops, the TestItem should set its - * {@link TestItem.status} back to `Pending`. - * - * The editor will only call this method when it's interested in refreshing - * the children of the item, and will not call it again while there's an - * existing, uncancelled discovery for an item. - * - * @param token Cancellation for the request. Cancellation will be - * requested if the test changes before the previous call completes. - */ - resolveHandler?: (token: CancellationToken) => void; - - /** - * Attaches a child, created from the {@link test.createTestItem} function, - * to this item. A `TestItem` may be a child of at most one other item. - */ - addChild(child: TestItem): void; - - /** - * Removes the test and its children from the tree. Any tokens passed to - * child `resolveHandler` methods will be cancelled. - */ - dispose(): void; - } - - /** - * Possible states of tests in a test run. - */ - export enum TestResultState { - // Initial state - Unset = 0, - // Test will be run, but is not currently running. - Queued = 1, - // Test is currently running - Running = 2, - // Test run has passed - Passed = 3, - // Test run has failed (on an assertion) - Failed = 4, - // Test run has been skipped - Skipped = 5, - // Test run failed for some other reason (compilation error, timeout, etc) - Errored = 6 - } - - /** - * Represents the severity of test messages. - */ - export enum TestMessageSeverity { - Error = 0, - Warning = 1, - Information = 2, - Hint = 3 - } - - /** - * Message associated with the test state. Can be linked to a specific - * source range -- useful for assertion failures, for example. - */ - export class TestMessage { - /** - * Human-readable message text to display. - */ - message: string | MarkdownString; - - /** - * Message severity. Defaults to "Error". - */ - severity: TestMessageSeverity; - - /** - * Expected test output. If given with `actualOutput`, a diff view will be shown. - */ - expectedOutput?: string; - - /** - * Actual test output. If given with `expectedOutput`, a diff view will be shown. - */ - actualOutput?: string; - - /** - * Associated file location. - */ - location?: Location; - - /** - * Creates a new TestMessage that will present as a diff in the editor. - * @param message Message to display to the user. - * @param expected Expected output. - * @param actual Actual output. - */ - static diff(message: string | MarkdownString, expected: string, actual: string): TestMessage; - - /** - * Creates a new TestMessage instance. - * @param message The message to show to the user. - */ - constructor(message: string | MarkdownString); - } - /** * TestResults can be provided to VS Code in {@link test.publishTestResult}, * or read from it in {@link test.testResults}. @@ -3154,6 +2856,69 @@ declare module 'vscode' { //#endregion + //#region @joaomoreno https://github.com/microsoft/vscode/issues/124263 + // This API change only affects behavior and documentation, not API surface. + + namespace env { + + /** + * Resolves a uri to form that is accessible externally. + * + * #### `http:` or `https:` scheme + * + * Resolves an *external* uri, such as a `http:` or `https:` link, from where the extension is running to a + * uri to the same resource on the client machine. + * + * This is a no-op if the extension is running on the client machine. + * + * If the extension is running remotely, this function automatically establishes a port forwarding tunnel + * from the local machine to `target` on the remote and returns a local uri to the tunnel. The lifetime of + * the port forwarding tunnel is managed by VS Code and the tunnel can be closed by the user. + * + * *Note* that uris passed through `openExternal` are automatically resolved and you should not call `asExternalUri` on them. + * + * #### `vscode.env.uriScheme` + * + * Creates a uri that - if opened in a browser (e.g. via `openExternal`) - will result in a registered {@link UriHandler} + * to trigger. + * + * Extensions should not make any assumptions about the resulting uri and should not alter it in anyway. + * Rather, extensions can e.g. use this uri in an authentication flow, by adding the uri as callback query + * argument to the server to authenticate to. + * + * *Note* that if the server decides to add additional query parameters to the uri (e.g. a token or secret), it + * will appear in the uri that is passed to the {@link UriHandler}. + * + * **Example** of an authentication flow: + * ```typescript + * vscode.window.registerUriHandler({ + * handleUri(uri: vscode.Uri): vscode.ProviderResult { + * if (uri.path === '/did-authenticate') { + * console.log(uri.toString()); + * } + * } + * }); + * + * const callableUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://my.extension/did-authenticate`)); + * await vscode.env.openExternal(callableUri); + * ``` + * + * *Note* that extensions should not cache the result of `asExternalUri` as the resolved uri may become invalid due to + * a system or user action — for example, in remote cases, a user may close a port forwarding tunnel that was opened by + * `asExternalUri`. + * + * #### Any other scheme + * + * Any other scheme will be handled as if the provided URI is a workspace URI. In that case, the method will return + * a URI which, when handled, will make VS Code open the workspace. + * + * @return A uri that can be used on the client machine. + */ + export function asExternalUri(target: Uri): Thenable; + } + + //#endregion + //#region https://github.com/Microsoft/vscode/issues/15178 // TODO@API must be a class @@ -3262,11 +3027,74 @@ declare module 'vscode' { //#endregion + //#region https://github.com/microsoft/vscode/issues/124024 @hediet @alexdima + + export class InlineCompletionItem { + /** + * The text to insert. + * If the text contains a line break, the range must end at the end of a line. + * If existing text should be replaced, the existing text must be a prefix of the text to insert. + */ + text: string; + + /** + * The range to replace. + * Must begin and end on the same line. + */ + range?: Range; + + constructor(text: string); + } + + export class InlineCompletionList { + items: InlineCompletionItem[]; + + constructor(items: InlineCompletionItem[]); + } + + /** + * How an {@link InlineCompletionItemProvider inline completion provider} was triggered. + */ + export enum InlineCompletionTriggerKind { + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 0, + + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Explicit = 1, + } + export interface InlineCompletionContext { + /** + * How the completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKind; + } + + export interface InlineCompletionItemProvider { + provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + } + + export namespace languages { + export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable; + } + + //#endregion + //#region FileSystemProvider stat readonly - https://github.com/microsoft/vscode/issues/73122 export enum FilePermission { /** * The file is readonly. + * + * *Note:* All `FileStat` from a `FileSystemProvider` that is registered with + * the option `isReadonly: true` will be implicitly handled as if `FilePermission.Readonly` + * is set. As a consequence, it is not possible to have a readonly file system provider + * registered where some `FileStat` are not readonly. */ Readonly = 1 } @@ -3285,4 +3113,16 @@ declare module 'vscode' { } //#endregion + + //#region https://github.com/microsoft/vscode/issues/87110 @eamodio + + export interface Memento { + + /** + * The stored keys. + */ + readonly keys: readonly string[]; + } + + //#endregion } diff --git a/src/dotnet-interactive-vscode/stable/package.json b/src/dotnet-interactive-vscode/stable/package.json index a24a720502..149e72e742 100644 --- a/src/dotnet-interactive-vscode/stable/package.json +++ b/src/dotnet-interactive-vscode/stable/package.json @@ -52,6 +52,12 @@ "extensionDependencies": [ "ms-toolsai.jupyter" ], + "capabilities": { + "untrustedWorkspaces": { + "supported": true + }, + "virtualWorkspaces": false + }, "contributes": { "notebookProvider": [ { @@ -398,4 +404,4 @@ "node-fetch": "2.6.1", "uuid": "8.3.2" } -} +} \ No newline at end of file diff --git a/src/dotnet-interactive-vscode/stable/src/notebookContentProviderWrapper.ts b/src/dotnet-interactive-vscode/stable/src/notebookContentProviderWrapper.ts index 4299c665f5..955c047176 100644 --- a/src/dotnet-interactive-vscode/stable/src/notebookContentProviderWrapper.ts +++ b/src/dotnet-interactive-vscode/stable/src/notebookContentProviderWrapper.ts @@ -5,7 +5,6 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import { backupNotebook, defaultNotebookCellLanguage } from './common/interactiveNotebook'; -import { isUnsavedNotebook } from './common/vscode/vscodeUtilities'; import { OutputChannelAdapter } from './common/vscode/OutputChannelAdapter'; // a thin wrapper around the new `vscode.NotebookSerializer` api @@ -14,7 +13,7 @@ export class DotNetNotebookContentProviderWrapper implements vscode.NotebookCont } async openNotebook(uri: vscode.Uri, openContext: vscode.NotebookDocumentOpenContext, token: vscode.CancellationToken): Promise { - let fileUri: vscode.Uri | undefined = isUnsavedNotebook(uri) + let fileUri: vscode.Uri | undefined = uri.scheme === 'untitled' ? undefined : uri; if (openContext.backupId) { diff --git a/src/dotnet-interactive-vscode/stable/src/notebookControllers.ts b/src/dotnet-interactive-vscode/stable/src/notebookControllers.ts index de6dc75001..3ddb0a485d 100644 --- a/src/dotnet-interactive-vscode/stable/src/notebookControllers.ts +++ b/src/dotnet-interactive-vscode/stable/src/notebookControllers.ts @@ -13,7 +13,8 @@ import * as vscodeUtilities from './common/vscode/vscodeUtilities'; import { getSimpleLanguage, isDotnetInteractiveLanguage, notebookCellLanguages } from './common/interactiveNotebook'; import { getCellLanguage, getDotNetMetadata, getLanguageInfoMetadata, isDotNetNotebookMetadata, withDotNetKernelMetadata } from './common/ipynbUtilities'; import { reshapeOutputValueForVsCode } from './common/interfaces/utilities'; -import { selectDotNetInteractiveKernel } from './common/vscode/commands'; +import { selectDotNetInteractiveKernelForJupyter } from './common/vscode/commands'; +import { ErrorOutputCreator } from './common/interactiveClient'; const executionTasks: Map = new Map(); @@ -24,7 +25,7 @@ export class DotNetNotebookKernel { private disposables: { dispose(): void }[] = []; - constructor(private readonly clientMapper: ClientMapper, preloadUris: vscode.Uri[]) { + constructor(private readonly clientMapper: ClientMapper, private readonly createErrorOutput: ErrorOutputCreator, preloadUris: vscode.Uri[]) { const preloads = preloadUris.map(uri => ({ uri })); // .dib execution @@ -55,7 +56,7 @@ export class DotNetNotebookKernel { this.disposables.push(vscode.notebook.onDidOpenNotebookDocument(async notebook => { if (notebook.viewType === jupyterViewType && isDotNetNotebook(notebook)) { jupyterController.updateNotebookAffinity(notebook, vscode.NotebookControllerAffinity.Preferred); - await selectDotNetInteractiveKernel(); + await selectDotNetInteractiveKernelForJupyter(); await updateNotebookMetadata(notebook, clientMapper); } })); @@ -106,7 +107,7 @@ export class DotNetNotebookKernel { executionTask.clearOutput(cell.index); const client = await this.clientMapper.getOrAddClient(cell.notebook.uri); executionTask.token.onCancellationRequested(() => { - const errorOutput = utilities.createErrorOutput("Cell execution cancelled by user"); + const errorOutput = this.createErrorOutput("Cell execution cancelled by user"); const resultPromise = () => updateCellOutputs(executionTask, cell, [...cell.outputs, errorOutput]) .then(() => endExecution(cell, false)); client.cancel() @@ -128,7 +129,7 @@ export class DotNetNotebookKernel { endExecution(cell, true) ).catch(() => endExecution(cell, false)); } catch (err) { - const errorOutput = utilities.createErrorOutput(`Error executing cell: ${err}`); + const errorOutput = this.createErrorOutput(`Error executing cell: ${err}`); await updateCellOutputs(executionTask, cell, [errorOutput]); endExecution(cell, false); throw err; diff --git a/src/dotnet-interactive-vscode/stable/src/notebookSerializers.ts b/src/dotnet-interactive-vscode/stable/src/notebookSerializers.ts index ab9bea97b3..bd7e0669b9 100644 --- a/src/dotnet-interactive-vscode/stable/src/notebookSerializers.ts +++ b/src/dotnet-interactive-vscode/stable/src/notebookSerializers.ts @@ -11,6 +11,7 @@ import { defaultNotebookCellLanguage, getNotebookSpecificLanguage, getSimpleLang import { OutputChannelAdapter } from './common/vscode/OutputChannelAdapter'; import { getEol, vsCodeCellOutputToContractCellOutput } from './common/vscode/vscodeUtilities'; import { Eol } from './common/interfaces'; +import { createUri } from './common/utilities'; import { DotNetNotebookContentProviderWrapper } from './notebookContentProviderWrapper'; abstract class DotNetNotebookSerializer implements vscode.NotebookSerializer { @@ -76,7 +77,7 @@ abstract class DotNetNotebookSerializer implements vscode.NotebookSerializer { } private getClient(): Promise { - return this.clientMapper.getOrAddClient({ fsPath: this.serializerId }); + return this.clientMapper.getOrAddClient(createUri(this.serializerId)); } private getNotebookName(): string { diff --git a/src/dotnet-interactive-vscode/stable/src/versionSpecificFunctions.ts b/src/dotnet-interactive-vscode/stable/src/versionSpecificFunctions.ts index bccf7e1320..64a0d37378 100644 --- a/src/dotnet-interactive-vscode/stable/src/versionSpecificFunctions.ts +++ b/src/dotnet-interactive-vscode/stable/src/versionSpecificFunctions.ts @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +import * as path from 'path'; import * as vscode from 'vscode'; import * as contracts from './common/interfaces/contracts'; import * as vscodeLike from './common/interfaces/vscode-like'; @@ -12,28 +13,52 @@ import * as notebookControllers from './notebookControllers'; import * as notebookSerializers from './notebookSerializers'; import { ClientMapper } from './common/clientMapper'; import { OutputChannelAdapter } from './common/vscode/OutputChannelAdapter'; +import { ErrorOutputCreator } from './common/interactiveClient'; -export function cellAt(document: vscode.NotebookDocument, index: number): vscode.NotebookCell { - return document.cellAt(index); +export function registerWithVsCode(context: vscode.ExtensionContext, clientMapper: ClientMapper, outputChannel: OutputChannelAdapter, createErrorOutput: ErrorOutputCreator, ...preloadUris: vscode.Uri[]) { + context.subscriptions.push(new notebookControllers.DotNetNotebookKernel(clientMapper, createErrorOutput, preloadUris)); + context.subscriptions.push(new notebookSerializers.DotNetDibNotebookSerializer(clientMapper, outputChannel)); } -export function cellCount(document: vscode.NotebookDocument): number { - return document.cellCount; +export function endExecution(cell: vscode.NotebookCell, success: boolean) { + notebookControllers.endExecution(cell, success); } -export function getCells(document: vscode.NotebookDocument | undefined): Array { - if (document) { - return [...document.getCells()]; - } +export function createErrorOutput(message: string, outputId?: string): vscodeLike.NotebookCellOutput { + const errorItem: vscodeLike.NotebookCellOutputItem = { + mime: 'application/x.notebook.error-traceback', + value: { + ename: 'Error', + evalue: message, + traceback: [], + }, + }; + const cellOutput = utilities.createOutput([errorItem], outputId); + return cellOutput; +} - return []; +export async function createNewBlankNotebook(extension: string, openNotebook: (uri: vscode.Uri) => Promise): Promise { + const fileName = getNewNotebookName(extension); + const newUri = vscode.Uri.file(fileName).with({ scheme: 'untitled', path: fileName }); + await openNotebook(newUri); } -export function registerWithVsCode(context: vscode.ExtensionContext, clientMapper: ClientMapper, outputChannel: OutputChannelAdapter, ...preloadUris: vscode.Uri[]) { - context.subscriptions.push(new notebookControllers.DotNetNotebookKernel(clientMapper, preloadUris)); - context.subscriptions.push(new notebookSerializers.DotNetDibNotebookSerializer(clientMapper, outputChannel)); +function workspaceHasUnsavedNotebookWithName(fileName: string): boolean { + return vscode.workspace.textDocuments.findIndex(textDocument => { + if (textDocument.notebook) { + const notebookUri = textDocument.notebook.uri; + return notebookUri.scheme === 'untitled' && path.basename(notebookUri.fsPath) === fileName; + } + + return false; + }) >= 0; } -export function endExecution(cell: vscode.NotebookCell, success: boolean) { - notebookControllers.endExecution(cell, success); +function getNewNotebookName(extension: string): string { + let suffix = 1; + let filename = ''; + do { + filename = `Untitled-${suffix++}${extension}`; + } while (workspaceHasUnsavedNotebookWithName(filename)); + return filename; }