|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import * as assert from 'assert'; |
| 7 | +import 'mocha'; |
| 8 | +import { TextDecoder, TextEncoder } from 'util'; |
| 9 | +import * as vscode from 'vscode'; |
| 10 | +import { asPromise, assertNoRpc, closeAllEditors, createRandomFile, disposeAll, revertAllDirty, saveAllEditors } from '../utils'; |
| 11 | + |
| 12 | +async function createRandomNotebookFile() { |
| 13 | + return createRandomFile('', undefined, '.vsctestnb'); |
| 14 | +} |
| 15 | + |
| 16 | +async function openRandomNotebookDocument() { |
| 17 | + const uri = await createRandomNotebookFile(); |
| 18 | + return vscode.workspace.openNotebookDocument(uri); |
| 19 | +} |
| 20 | + |
| 21 | +export async function saveAllFilesAndCloseAll() { |
| 22 | + await saveAllEditors(); |
| 23 | + await closeAllEditors(); |
| 24 | +} |
| 25 | + |
| 26 | + |
| 27 | +function sleep(ms: number): Promise<void> { |
| 28 | + return new Promise(resolve => { |
| 29 | + setTimeout(resolve, ms); |
| 30 | + }); |
| 31 | +} |
| 32 | + |
| 33 | +export class Kernel { |
| 34 | + |
| 35 | + readonly controller: vscode.NotebookController; |
| 36 | + |
| 37 | + readonly associatedNotebooks = new Set<string>(); |
| 38 | + |
| 39 | + constructor(id: string, label: string, viewType: string = 'notebookCoreTest') { |
| 40 | + this.controller = vscode.notebooks.createNotebookController(id, viewType, label); |
| 41 | + this.controller.executeHandler = this._execute.bind(this); |
| 42 | + this.controller.supportsExecutionOrder = true; |
| 43 | + this.controller.supportedLanguages = ['typescript', 'javascript']; |
| 44 | + this.controller.onDidChangeSelectedNotebooks(e => { |
| 45 | + if (e.selected) { |
| 46 | + this.associatedNotebooks.add(e.notebook.uri.toString()); |
| 47 | + } else { |
| 48 | + this.associatedNotebooks.delete(e.notebook.uri.toString()); |
| 49 | + } |
| 50 | + }); |
| 51 | + } |
| 52 | + |
| 53 | + protected async _execute(cells: vscode.NotebookCell[]): Promise<void> { |
| 54 | + for (const cell of cells) { |
| 55 | + await this._runCell(cell); |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + protected async _runCell(cell: vscode.NotebookCell) { |
| 60 | + // create a single output with exec order 1 and output is plain/text |
| 61 | + // of either the cell itself or (iff empty) the cell's document's uri |
| 62 | + const task = this.controller.createNotebookCellExecution(cell); |
| 63 | + task.start(Date.now()); |
| 64 | + task.executionOrder = 1; |
| 65 | + await sleep(10); // Force to be take some time |
| 66 | + await task.replaceOutput([new vscode.NotebookCellOutput([ |
| 67 | + vscode.NotebookCellOutputItem.text(cell.document.getText() || cell.document.uri.toString(), 'text/plain') |
| 68 | + ])]); |
| 69 | + task.end(true); |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | + |
| 74 | +function getFocusedCell(editor?: vscode.NotebookEditor) { |
| 75 | + return editor ? editor.notebook.cellAt(editor.selections[0].start) : undefined; |
| 76 | +} |
| 77 | + |
| 78 | +const apiTestContentProvider: vscode.NotebookContentProvider = { |
| 79 | + openNotebook: async (resource: vscode.Uri): Promise<vscode.NotebookData> => { |
| 80 | + if (/.*empty\-.*\.vsctestnb$/.test(resource.path)) { |
| 81 | + return { |
| 82 | + metadata: {}, |
| 83 | + cells: [] |
| 84 | + }; |
| 85 | + } |
| 86 | + |
| 87 | + const dto: vscode.NotebookData = { |
| 88 | + metadata: { custom: { testMetadata: false } }, |
| 89 | + cells: [ |
| 90 | + { |
| 91 | + value: 'test', |
| 92 | + languageId: 'typescript', |
| 93 | + kind: vscode.NotebookCellKind.Code, |
| 94 | + outputs: [], |
| 95 | + metadata: { custom: { testCellMetadata: 123 } }, |
| 96 | + executionSummary: { timing: { startTime: 10, endTime: 20 } } |
| 97 | + }, |
| 98 | + { |
| 99 | + value: 'test2', |
| 100 | + languageId: 'typescript', |
| 101 | + kind: vscode.NotebookCellKind.Code, |
| 102 | + outputs: [ |
| 103 | + new vscode.NotebookCellOutput([ |
| 104 | + vscode.NotebookCellOutputItem.text('Hello World', 'text/plain') |
| 105 | + ], |
| 106 | + { |
| 107 | + testOutputMetadata: true, |
| 108 | + ['text/plain']: { testOutputItemMetadata: true } |
| 109 | + }) |
| 110 | + ], |
| 111 | + executionSummary: { executionOrder: 5, success: true }, |
| 112 | + metadata: { custom: { testCellMetadata: 456 } } |
| 113 | + } |
| 114 | + ] |
| 115 | + }; |
| 116 | + return dto; |
| 117 | + }, |
| 118 | + saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { |
| 119 | + return; |
| 120 | + }, |
| 121 | + saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { |
| 122 | + return; |
| 123 | + }, |
| 124 | + backupNotebook: async (_document: vscode.NotebookDocument, _context: vscode.NotebookDocumentBackupContext, _cancellation: vscode.CancellationToken) => { |
| 125 | + return { |
| 126 | + id: '1', |
| 127 | + delete: () => { } |
| 128 | + }; |
| 129 | + } |
| 130 | +}; |
| 131 | + |
| 132 | +(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('Notebook API tests', function () { |
| 133 | + |
| 134 | + const testDisposables: vscode.Disposable[] = []; |
| 135 | + const suiteDisposables: vscode.Disposable[] = []; |
| 136 | + |
| 137 | + suiteTeardown(async function () { |
| 138 | + |
| 139 | + assertNoRpc(); |
| 140 | + |
| 141 | + await revertAllDirty(); |
| 142 | + await closeAllEditors(); |
| 143 | + |
| 144 | + disposeAll(suiteDisposables); |
| 145 | + suiteDisposables.length = 0; |
| 146 | + }); |
| 147 | + |
| 148 | + suiteSetup(function () { |
| 149 | + suiteDisposables.push(vscode.workspace.registerNotebookContentProvider('notebookCoreTest', apiTestContentProvider)); |
| 150 | + }); |
| 151 | + |
| 152 | + let defaultKernel: Kernel; |
| 153 | + |
| 154 | + setup(async function () { |
| 155 | + // there should be ONE default kernel in this suite |
| 156 | + defaultKernel = new Kernel('mainKernel', 'Notebook Default Kernel'); |
| 157 | + testDisposables.push(defaultKernel.controller); |
| 158 | + await saveAllFilesAndCloseAll(); |
| 159 | + }); |
| 160 | + |
| 161 | + teardown(async function () { |
| 162 | + disposeAll(testDisposables); |
| 163 | + testDisposables.length = 0; |
| 164 | + await saveAllFilesAndCloseAll(); |
| 165 | + }); |
| 166 | + |
| 167 | + test('edit API batch edits', async function () { |
| 168 | + const notebook = await openRandomNotebookDocument(); |
| 169 | + |
| 170 | + const edit = new vscode.WorkspaceEdit(); |
| 171 | + const metdataEdit = vscode.NotebookEdit.updateNotebookMetadata({ ...notebook.metadata, custom: { ...(notebook.metadata.custom || {}), extraNotebookMetadata: true } }); |
| 172 | + edit.set(notebook.uri, [metdataEdit]); |
| 173 | + const success = await vscode.workspace.applyEdit(edit); |
| 174 | + assert.equal(success, true); |
| 175 | + assert.ok(notebook.metadata.custom.extraNotebookMetadata, `Test metadata not found`); |
| 176 | + }); |
| 177 | + |
| 178 | + test('notebook open', async function () { |
| 179 | + const notebook = await openRandomNotebookDocument(); |
| 180 | + const editor = await vscode.window.showNotebookDocument(notebook); |
| 181 | + assert.strictEqual(getFocusedCell(editor)?.document.getText(), 'test'); |
| 182 | + assert.strictEqual(getFocusedCell(editor)?.document.languageId, 'typescript'); |
| 183 | + |
| 184 | + const secondCell = editor.notebook.cellAt(1); |
| 185 | + assert.strictEqual(secondCell.outputs.length, 1); |
| 186 | + assert.deepStrictEqual(secondCell.outputs[0].metadata, { testOutputMetadata: true, ['text/plain']: { testOutputItemMetadata: true } }); |
| 187 | + assert.strictEqual(secondCell.outputs[0].items.length, 1); |
| 188 | + assert.strictEqual(secondCell.outputs[0].items[0].mime, 'text/plain'); |
| 189 | + assert.strictEqual(new TextDecoder().decode(secondCell.outputs[0].items[0].data), 'Hello World'); |
| 190 | + assert.strictEqual(secondCell.executionSummary?.executionOrder, 5); |
| 191 | + assert.strictEqual(secondCell.executionSummary?.success, true); |
| 192 | + }); |
| 193 | + |
| 194 | + test('multiple tabs: different editors with same document', async function () { |
| 195 | + const notebook = await openRandomNotebookDocument(); |
| 196 | + const firstNotebookEditor = await vscode.window.showNotebookDocument(notebook, { viewColumn: vscode.ViewColumn.One }); |
| 197 | + const secondNotebookEditor = await vscode.window.showNotebookDocument(notebook, { viewColumn: vscode.ViewColumn.Beside }); |
| 198 | + assert.notStrictEqual(firstNotebookEditor, secondNotebookEditor); |
| 199 | + assert.strictEqual(firstNotebookEditor?.notebook, secondNotebookEditor?.notebook, 'split notebook editors share the same document'); |
| 200 | + }); |
| 201 | + |
| 202 | + test.skip('#106657. Opening a notebook from markers view is broken ', async function () { |
| 203 | + |
| 204 | + const document = await openRandomNotebookDocument(); |
| 205 | + const [cell] = document.getCells(); |
| 206 | + |
| 207 | + assert.strictEqual(vscode.window.activeNotebookEditor, undefined); |
| 208 | + |
| 209 | + // opening a cell-uri opens a notebook editor |
| 210 | + await vscode.window.showTextDocument(cell.document, { viewColumn: vscode.ViewColumn.Active }); |
| 211 | + // await vscode.commands.executeCommand('vscode.open', cell.document.uri, vscode.ViewColumn.Active); |
| 212 | + |
| 213 | + assert.strictEqual(!!vscode.window.activeNotebookEditor, true); |
| 214 | + assert.strictEqual(vscode.window.activeNotebookEditor!.notebook.uri.toString(), document.uri.toString()); |
| 215 | + }); |
| 216 | + |
| 217 | + test('Cannot open notebook from cell-uri with vscode.open-command', async function () { |
| 218 | + |
| 219 | + const document = await openRandomNotebookDocument(); |
| 220 | + const [cell] = document.getCells(); |
| 221 | + |
| 222 | + await saveAllFilesAndCloseAll(); |
| 223 | + assert.strictEqual(vscode.window.activeNotebookEditor, undefined); |
| 224 | + |
| 225 | + // BUG is that the editor opener (https://github.com/microsoft/vscode/blob/8e7877bdc442f1e83a7fec51920d82b696139129/src/vs/editor/browser/services/openerService.ts#L69) |
| 226 | + // removes the fragment if it matches something numeric. For notebooks that's not wanted... |
| 227 | + // opening a cell-uri opens a notebook editor |
| 228 | + await vscode.commands.executeCommand('vscode.open', cell.document.uri); |
| 229 | + |
| 230 | + assert.strictEqual(vscode.window.activeNotebookEditor!.notebook.uri.toString(), document.uri.toString()); |
| 231 | + }); |
| 232 | + |
| 233 | + test('#97830, #97764. Support switch to other editor types', async function () { |
| 234 | + const notebook = await openRandomNotebookDocument(); |
| 235 | + const editor = await vscode.window.showNotebookDocument(notebook); |
| 236 | + const edit = new vscode.WorkspaceEdit(); |
| 237 | + const focusedCell = getFocusedCell(editor); |
| 238 | + assert.ok(focusedCell); |
| 239 | + edit.replace(focusedCell.document.uri, focusedCell.document.lineAt(0).range, 'var abc = 0;'); |
| 240 | + await vscode.workspace.applyEdit(edit); |
| 241 | + |
| 242 | + assert.strictEqual(getFocusedCell(editor)?.document.getText(), 'var abc = 0;'); |
| 243 | + |
| 244 | + // no kernel -> no default language |
| 245 | + assert.strictEqual(getFocusedCell(editor)?.document.languageId, 'typescript'); |
| 246 | + |
| 247 | + await vscode.commands.executeCommand('vscode.openWith', notebook.uri, 'default'); |
| 248 | + assert.strictEqual(vscode.window.activeTextEditor?.document.uri.path, notebook.uri.path); |
| 249 | + }); |
| 250 | + |
| 251 | + test('#102411 - untitled notebook creation failed', async function () { |
| 252 | + await vscode.commands.executeCommand('workbench.action.files.newUntitledFile', { viewType: 'notebookCoreTest' }); |
| 253 | + assert.notStrictEqual(vscode.window.activeNotebookEditor, undefined, 'untitled notebook editor is not undefined'); |
| 254 | + |
| 255 | + await closeAllEditors(); |
| 256 | + }); |
| 257 | + |
| 258 | + test('#115855 onDidSaveNotebookDocument', async function () { |
| 259 | + const resource = await createRandomNotebookFile(); |
| 260 | + const notebook = await vscode.workspace.openNotebookDocument(resource); |
| 261 | + |
| 262 | + const notebookEdit = new vscode.NotebookEdit(new vscode.NotebookRange(1, 1), [new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'test 2', 'javascript')]); |
| 263 | + const edit = new vscode.WorkspaceEdit(); |
| 264 | + edit.set(notebook.uri, [notebookEdit]); |
| 265 | + await vscode.workspace.applyEdit(edit); |
| 266 | + assert.strictEqual(notebook.isDirty, true); |
| 267 | + |
| 268 | + const saveEvent = asPromise(vscode.workspace.onDidSaveNotebookDocument); |
| 269 | + await notebook.save(); |
| 270 | + await saveEvent; |
| 271 | + |
| 272 | + assert.strictEqual(notebook.isDirty, false); |
| 273 | + }); |
| 274 | +}); |
| 275 | + |
| 276 | +(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('statusbar', () => { |
| 277 | + const emitter = new vscode.EventEmitter<vscode.NotebookCell>(); |
| 278 | + const onDidCallProvide = emitter.event; |
| 279 | + const suiteDisposables: vscode.Disposable[] = []; |
| 280 | + suiteTeardown(async function () { |
| 281 | + assertNoRpc(); |
| 282 | + |
| 283 | + await revertAllDirty(); |
| 284 | + await closeAllEditors(); |
| 285 | + |
| 286 | + disposeAll(suiteDisposables); |
| 287 | + suiteDisposables.length = 0; |
| 288 | + }); |
| 289 | + |
| 290 | + suiteSetup(() => { |
| 291 | + suiteDisposables.push(vscode.notebooks.registerNotebookCellStatusBarItemProvider('notebookCoreTest', { |
| 292 | + async provideCellStatusBarItems(cell: vscode.NotebookCell, _token: vscode.CancellationToken): Promise<vscode.NotebookCellStatusBarItem[]> { |
| 293 | + emitter.fire(cell); |
| 294 | + return []; |
| 295 | + } |
| 296 | + })); |
| 297 | + |
| 298 | + suiteDisposables.push(vscode.workspace.registerNotebookContentProvider('notebookCoreTest', apiTestContentProvider)); |
| 299 | + }); |
| 300 | + |
| 301 | + test.skip('provideCellStatusBarItems called on metadata change', async function () { // TODO@roblourens https://github.com/microsoft/vscode/issues/139324 |
| 302 | + const provideCalled = asPromise(onDidCallProvide); |
| 303 | + const notebook = await openRandomNotebookDocument(); |
| 304 | + await vscode.window.showNotebookDocument(notebook); |
| 305 | + await provideCalled; |
| 306 | + |
| 307 | + const edit = new vscode.WorkspaceEdit(); |
| 308 | + edit.replaceNotebookCellMetadata(notebook.uri, 0, { inputCollapsed: true }); |
| 309 | + vscode.workspace.applyEdit(edit); |
| 310 | + await provideCalled; |
| 311 | + }); |
| 312 | +}); |
| 313 | + |
| 314 | +suite('Notebook & LiveShare', function () { |
| 315 | + |
| 316 | + const suiteDisposables: vscode.Disposable[] = []; |
| 317 | + const notebookType = 'vsls-testing'; |
| 318 | + |
| 319 | + suiteTeardown(() => { |
| 320 | + vscode.Disposable.from(...suiteDisposables).dispose(); |
| 321 | + }); |
| 322 | + |
| 323 | + suiteSetup(function () { |
| 324 | + |
| 325 | + suiteDisposables.push(vscode.workspace.registerNotebookSerializer(notebookType, new class implements vscode.NotebookSerializer { |
| 326 | + deserializeNotebook(content: Uint8Array, _token: vscode.CancellationToken): vscode.NotebookData | Thenable<vscode.NotebookData> { |
| 327 | + const value = new TextDecoder().decode(content); |
| 328 | + const cell1 = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, value, 'fooLang'); |
| 329 | + cell1.outputs = [new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(value)])]; |
| 330 | + return new vscode.NotebookData([cell1]); |
| 331 | + } |
| 332 | + serializeNotebook(data: vscode.NotebookData, _token: vscode.CancellationToken): Uint8Array | Thenable<Uint8Array> { |
| 333 | + return new TextEncoder().encode(data.cells[0].value); |
| 334 | + } |
| 335 | + }, {}, { |
| 336 | + displayName: 'LS', |
| 337 | + filenamePattern: ['*'], |
| 338 | + })); |
| 339 | + }); |
| 340 | + |
| 341 | + test('command: vscode.resolveNotebookContentProviders', async function () { |
| 342 | + |
| 343 | + type Info = { viewType: string; displayName: string; filenamePattern: string[] }; |
| 344 | + |
| 345 | + const info = await vscode.commands.executeCommand<Info[]>('vscode.resolveNotebookContentProviders'); |
| 346 | + assert.strictEqual(Array.isArray(info), true); |
| 347 | + |
| 348 | + const item = info.find(item => item.viewType === notebookType); |
| 349 | + assert.ok(item); |
| 350 | + assert.strictEqual(item?.viewType, notebookType); |
| 351 | + }); |
| 352 | + |
| 353 | + test('command: vscode.executeDataToNotebook', async function () { |
| 354 | + const value = 'dataToNotebook'; |
| 355 | + const data = await vscode.commands.executeCommand<vscode.NotebookData>('vscode.executeDataToNotebook', notebookType, new TextEncoder().encode(value)); |
| 356 | + assert.ok(data instanceof vscode.NotebookData); |
| 357 | + assert.strictEqual(data.cells.length, 1); |
| 358 | + assert.strictEqual(data.cells[0].value, value); |
| 359 | + assert.strictEqual(new TextDecoder().decode(data.cells[0].outputs![0].items[0].data), value); |
| 360 | + }); |
| 361 | + |
| 362 | + test('command: vscode.executeNotebookToData', async function () { |
| 363 | + const value = 'notebookToData'; |
| 364 | + const notebook = new vscode.NotebookData([new vscode.NotebookCellData(vscode.NotebookCellKind.Code, value, 'fooLang')]); |
| 365 | + const data = await vscode.commands.executeCommand<Uint8Array>('vscode.executeNotebookToData', notebookType, notebook); |
| 366 | + assert.ok(data instanceof Uint8Array); |
| 367 | + assert.deepStrictEqual(new TextDecoder().decode(data), value); |
| 368 | + }); |
| 369 | +}); |
0 commit comments