Files
azuredatastudio/src/sql/workbench/api/common/extHostNotebookEditor.ts
Charles Gagnon 4a2b31f3ba Hook up Notebook execution edits (#17943)
* Start rerouting VSCode cell execution APIs.

* Add more conversion code.

* Convert VSCode notebook registrations into ADS equivalents.

* Update vscode notebook provider kernels when notebook controller's supportedLanguages are set.

* Update an error message.

* Add another session argument.

* Add base classes for converting notebook serializers.

* Disable some vscode notebook methods.

* Disable more vscode APIs.

* Disable more stuff.

* Start implementing serializer notebook data conversions.

* Use direct references to extension host notebook methods, rather than azdata ones.

* Add a comment.

* Remove a space.

* Use import type to fix module loading errors.

* Use internal cancellation token class.

* Start adding cell output conversion.

* Convert data from byte array to a string.

* More output work.

* Use a Set for proxy filtering.

* Start adding tests.

* Include metadata in cell conversion. Fix other test failures.

* Fix serialize tests.

* Add more tests.

* Remove wildcard characters from vscode filenames.

* Start implementing session details.

* Add more kernel info.

* Add kernel spec.

* Add Future callback wrapper class.

* Start implementing execute conversion.

* Pass notebook URI to requestExecute.

* Start working on CellExecution methods.

* Move some code around to fix layering issues.

* Use proxy to access browser code, rather than direct imports.

* Move files around to fix layering issues.

* Remove unused imports.

* Start implementing some notebook cell execution behaviors.

* Revert some unnecessary extHost API changes.

* Check for nbformat.

* Also handle nbformat in serialize case.

* Active notebook extensions when resolving NotebookInput.

* Fix nbformat handling.

* Disable VSCode notebooks code.

* Filter out notebook services from registration assertion.

* Wait for providers to load before calling canResolve.

* Use controller's viewType for notebook provider ID, instead of controller ID.

* Start adding extHostNotebook tests for new APIs.

* Re-order proxy calls.

* Remove commented code.

* Move vscode provider files to browser folder. Fix RPC serialization issues by using readonly field instead of getter for providerId.

* Add a comment.

* Remove unnecessary dispose call.

* Handle disposable from registerExecuteProvider.

* Remove a comment.

* Remove unnecessary provider fields.

* Remove reference to notebook service to fix circular reference issue in stringify.

* Add object types for methods in ADSNotebookController.

* Wait for controller languages to be ready before marking session manager as ready.

* Add correct promise.

* Add undefined return type for optional supportedLanguages property.

* Refine promise logic.

* Move vscode functionality back to ExtHostNotebook, since the NotebookService can't be passed back over RPC (some kind of circular reference error).

* Fix remaining issues from last commit.

* Replace "not implemented" methods with placeholder return types in order to enable testing.

* Also wait for execution handler to be set before marking session manager as ready.

* Fix usage of NotebookRegistry when updating provider description languages.

* Refine file extension conversion.

* Fix file extension conversion to match ADS extension behavior.

* Emit new provider registration event when adding supported languages.

* Remove checks for duplicate file providers and kernels.

* Fix a test failure.

* Fix file extension parsing.

* Use default executeManager if one isn't defined for provider in notebookModel.

* Add descriptors for waiting on standardKernels registration.

* Increase timeout

* Add an error message.

* Start working on retrieving default kernel from registered providers, rather than always falling back to SQL.

* Revert "Start working on retrieving default kernel from registered providers, rather than always falling back to SQL."

This reverts commit 1916ea1ce3a0072f51bec683116dc7bb6c7aefdc.

* Emit activation events after provider registration.

* Wait on standard kernels availability when getting an execute provider.

* Throw an error if session manager isn't ready yet.

* Actually resolve language promise correctly.

* Add some checks for undefined notebook data objects.

* Create kernel spec data correctly.

* Add extension changes for local testing only.

* Clean up test class.

* Add a reminder comment.

* Undo commented out notebook stuff

* Temporarily hard code default kernel.

* Retrieve default kernel in notebookModel if it's not already provided.

* Revert an import change.

* Remove unnecessary method from extHostNotebook.

* Move an interface around.

* wip

* Check for proposed API for some VSCode extHost methods.

* Remove a comment.

* Fix notebookUtils tests.

* Fix notebookModel tests.

* Fix notebookFindModel tests.

* Fix notebookViewsExtension tests.

* Fix remaining notebookView tests.

* Refactor output conversion functionality into separate methods.

* Update some unit tests for output conversion.

* Move a method.

* Rename conversion methods to fit acronym styling.

* Add another conversion test case.

* Revert local testing changes.

* Remove old method.

* cleanup

* Remove some comments.

* Move localized string to locConstants.

* Add a space to loc string.

* Add comments to new SQL Carbon Edit tags.

* Create constants for nbformat and nbformat_minor.

* Move some vscode-only fields to proposed APIs.

* Check for valid state

* Properly null check

* Adding logging for provider wait timeouts.

* wip update

* Fix compile

* Switch to cell edits

* Update docs

* Remove custom output type

* cleanup

* fix

* cleanup

* more cleanup

* Fixes

* Fix tests and lint errors

Co-authored-by: Cory Rivera <corivera@microsoft.com>
2022-01-04 16:35:16 -08:00

249 lines
7.7 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { ok } from 'vs/base/common/assert';
import { IDisposable } from 'vs/base/common/lifecycle';
import { readonly } from 'vs/base/common/errors';
import { MainThreadNotebookDocumentsAndEditorsShape } from 'sql/workbench/api/common/sqlExtHost.protocol';
import { ExtHostNotebookDocumentData } from 'sql/workbench/api/common/extHostNotebookDocumentData';
import { CellRange, INotebookEditOperation, ICellRange, NotebookEditOperationType } from 'sql/workbench/api/common/sqlExtHostTypes';
import { HideInputTag } from 'sql/platform/notebooks/common/outputRegistry';
export interface INotebookEditData {
documentVersionId: number;
edits: INotebookEditOperation[];
undoStopBefore: boolean;
undoStopAfter: boolean;
}
function toICellRange(range: azdata.nb.CellRange): ICellRange {
return {
start: range.start,
end: range.end
};
}
export class NotebookEditorEdit {
private readonly _document: azdata.nb.NotebookDocument;
private readonly _documentVersionId: number;
private _collectedEdits: INotebookEditOperation[];
private readonly _undoStopBefore: boolean;
private readonly _undoStopAfter: boolean;
constructor(document: azdata.nb.NotebookDocument, options: { undoStopBefore: boolean; undoStopAfter: boolean; }) {
this._document = document;
// TODO add version handling
this._documentVersionId = 0;
// this._documentVersionId = document.version;
this._collectedEdits = [];
this._undoStopBefore = options ? options.undoStopBefore : true;
this._undoStopAfter = options ? options.undoStopAfter : false;
}
finalize(): INotebookEditData {
return {
documentVersionId: this._documentVersionId,
edits: this._collectedEdits,
undoStopBefore: this._undoStopBefore,
undoStopAfter: this._undoStopAfter
};
}
replace(location: number | CellRange, value: Partial<azdata.nb.ICellContents>): void {
let range: CellRange = this.getAsRange(location);
this._pushEdit(NotebookEditOperationType.ReplaceCells, range, value);
}
private getAsRange(location: number | CellRange): CellRange {
let range: CellRange = null;
if (typeof (location) === 'number') {
range = new CellRange(location, location + 1);
}
else if (location instanceof CellRange) {
range = location;
}
else {
throw new Error('Unrecognized location');
}
return range;
}
setTrusted(isTrusted: boolean) {
this._document.setTrusted(isTrusted);
}
insertCell(value: Partial<azdata.nb.ICellContents>, index?: number, collapsed?: boolean): void {
if (index === null || index === undefined) {
// If not specified, assume adding to end of list
index = this._document.cells.length;
}
if (!!collapsed) {
if (!value.metadata) {
value.metadata = { tags: [HideInputTag] };
} else if (!value.metadata.tags) {
value.metadata.tags = [HideInputTag];
} else if (!value.metadata.tags.find(x => x === HideInputTag)) {
value.metadata.tags.push(HideInputTag);
}
}
this._pushEdit(NotebookEditOperationType.InsertCell, new CellRange(index, index), value);
}
deleteCell(index: number): void {
let range: CellRange = null;
if (typeof (index) === 'number') {
// Currently only allowing single-cell deletion.
// Do this by saying the range extends over 1 cell so on the main thread
// we can delete that cell, then handle insertions
range = new CellRange(index, index + 1);
} else {
throw new Error('Unrecognized index');
}
this._pushEdit(NotebookEditOperationType.DeleteCell, range, null);
}
updateCell(index: number, updatedContent: Partial<azdata.nb.ICellContents>, append: boolean): void {
this._pushEdit(NotebookEditOperationType.UpdateCell, new CellRange(index, index + 1), updatedContent, append);
}
updateCellOutput(cellIndex: number, updatedContent: Partial<azdata.nb.ICellContents>, append: boolean): void {
this._pushEdit(NotebookEditOperationType.UpdateCellOutput, new CellRange(cellIndex, cellIndex + 1), updatedContent, append);
}
private _pushEdit(type: NotebookEditOperationType, range: azdata.nb.CellRange, cell: Partial<azdata.nb.ICellContents>, append?: boolean): void {
let validRange = this._document.validateCellRange(range);
this._collectedEdits.push({
type: type,
range: validRange,
cell: cell,
append: append
});
}
}
export class ExtHostNotebookEditor implements azdata.nb.NotebookEditor, IDisposable {
private _disposed: boolean = false;
constructor(
private _proxy: MainThreadNotebookDocumentsAndEditorsShape,
private _id: string,
private readonly _documentData: ExtHostNotebookDocumentData,
private _viewColumn: vscode.ViewColumn
) {
}
dispose() {
ok(!this._disposed);
this._disposed = true;
}
get document(): azdata.nb.NotebookDocument {
return this._documentData.document;
}
set document(value) {
throw readonly('document');
}
get viewColumn(): vscode.ViewColumn {
return this._viewColumn;
}
set viewColumn(value) {
throw readonly('viewColumn');
}
get id(): string {
return this._id;
}
public runCell(cell: azdata.nb.NotebookCell): Thenable<boolean> {
let uri = cell ? cell.uri : undefined;
return this._proxy.$runCell(this._id, uri);
}
public runAllCells(startCell?: azdata.nb.NotebookCell, endCell?: azdata.nb.NotebookCell): Thenable<boolean> {
let startCellUri = startCell ? startCell.uri : undefined;
let endCellUri = endCell ? endCell.uri : undefined;
return this._proxy.$runAllCells(this._id, startCellUri, endCellUri);
}
public clearOutput(cell: azdata.nb.NotebookCell): Thenable<boolean> {
let uri = cell ? cell.uri : undefined;
return this._proxy.$clearOutput(this._id, uri);
}
public clearAllOutputs(): Thenable<boolean> {
return this._proxy.$clearAllOutputs(this._id);
}
public changeKernel(kernel: azdata.nb.IKernelSpec): Thenable<boolean> {
return this._proxy.$changeKernel(this._id, kernel);
}
public edit(callback: (editBuilder: NotebookEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable<boolean> {
if (this._disposed) {
return Promise.reject(new Error('NotebookEditor#edit not possible on closed editors'));
}
let edit = new NotebookEditorEdit(this._documentData.document, options);
callback(edit);
return this._applyEdit(edit);
}
private _applyEdit(editBuilder: NotebookEditorEdit): Promise<boolean> {
let editData = editBuilder.finalize();
// return when there is nothing to do
if (editData.edits.length === 0) {
return Promise.resolve(true);
}
// check that the edits are not overlapping (i.e. illegal)
let editRanges = editData.edits.map(edit => edit.range);
// sort ascending (by end and then by start)
editRanges.sort((a, b) => {
if (a.end === b.end) {
return a.start - b.start;
}
return a.end - b.end;
});
// check that no edits are overlapping
for (let i = 0, count = editRanges.length - 1; i < count; i++) {
const rangeEnd = editRanges[i].end;
const nextRangeStart = editRanges[i + 1].start;
if (nextRangeStart < rangeEnd) {
// overlapping ranges
return Promise.reject(new Error('Overlapping ranges are not allowed!'));
}
}
// prepare data for serialization
let edits: INotebookEditOperation[] = editData.edits.map((edit) => {
return {
type: edit.type,
range: toICellRange(edit.range),
cell: edit.cell,
append: edit.append
};
});
return this._proxy.$tryApplyEdits(this._id, editData.documentVersionId, edits, {
undoStopBefore: editData.undoStopBefore,
undoStopAfter: editData.undoStopAfter
});
}
}