Skip to content

Commit 352e7da

Browse files
authored
Merge pull request microsoft#186123 from microsoft/rebornix/remote-notebook-save
Experimental notebook save delegate on remote extension host
2 parents fb13a28 + 5dd084f commit 352e7da

11 files changed

+134
-18
lines changed

src/vs/workbench/api/browser/mainThreadNotebook.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,15 @@ export class MainThreadNotebooks implements MainThreadNotebookShape {
7272
extensionId: extension.id.value,
7373
});
7474
return result;
75-
}
75+
},
76+
save: async (uri, versionId, options, token) => {
77+
const stat = await this._proxy.$saveNotebook(handle, uri, versionId, options, token);
78+
return {
79+
...stat,
80+
children: undefined,
81+
resource: uri
82+
};
83+
},
7684
}));
7785

7886
if (data) {

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
170170
const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors));
171171
const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService));
172172
const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits)));
173-
const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments));
173+
const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem));
174174
const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook));
175175
const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, extHostNotebook));
176176
const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService));

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,12 +2274,15 @@ export interface NotebookCellDto {
22742274
internalMetadata?: notebookCommon.NotebookCellInternalMetadata;
22752275
}
22762276

2277+
export type INotebookPartialFileStatsWithMetadata = Omit<files.IFileStatWithMetadata, 'resource' | 'children'>;
2278+
22772279
export interface ExtHostNotebookShape extends ExtHostNotebookDocumentsAndEditorsShape {
22782280
$provideNotebookCellStatusBarItems(handle: number, uri: UriComponents, index: number, token: CancellationToken): Promise<INotebookCellStatusBarListDto | undefined>;
22792281
$releaseNotebookCellStatusBarItems(id: number): void;
22802282

22812283
$dataToNotebook(handle: number, data: VSBuffer, token: CancellationToken): Promise<SerializableObjectWithBuffers<NotebookDataDto>>;
22822284
$notebookToData(handle: number, data: SerializableObjectWithBuffers<NotebookDataDto>, token: CancellationToken): Promise<VSBuffer>;
2285+
$saveNotebook(handle: number, uri: UriComponents, versionId: number, options: files.IWriteFileOptions, token: CancellationToken): Promise<INotebookPartialFileStatsWithMetadata>;
22832286
}
22842287

22852288
export interface ExtHostNotebookDocumentSaveParticipantShape {

src/vs/workbench/api/common/extHostNotebook.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import { isFalsyOrWhitespace } from 'vs/base/common/strings';
1414
import { assertIsDefined } from 'vs/base/common/types';
1515
import { URI, UriComponents } from 'vs/base/common/uri';
1616
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
17+
import * as files from 'vs/platform/files/common/files';
1718
import { Cache } from 'vs/workbench/api/common/cache';
18-
import { ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookCellStatusBarListDto, INotebookDocumentsAndEditorsDelta, INotebookDocumentShowOptions, INotebookEditorAddData, MainContext, MainThreadNotebookDocumentsShape, MainThreadNotebookEditorsShape, MainThreadNotebookShape, NotebookDataDto } from 'vs/workbench/api/common/extHost.protocol';
19+
import { ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookCellStatusBarListDto, INotebookDocumentsAndEditorsDelta, INotebookDocumentShowOptions, INotebookEditorAddData, INotebookPartialFileStatsWithMetadata, MainContext, MainThreadNotebookDocumentsShape, MainThreadNotebookEditorsShape, MainThreadNotebookShape, NotebookDataDto } from 'vs/workbench/api/common/extHost.protocol';
1920
import { ApiCommand, ApiCommandArgument, ApiCommandResult, CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
2021
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
2122
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
@@ -27,6 +28,9 @@ import type * as vscode from 'vscode';
2728
import { ExtHostCell, ExtHostNotebookDocument } from './extHostNotebookDocument';
2829
import { ExtHostNotebookEditor } from './extHostNotebookEditor';
2930
import { onUnexpectedExternalError } from 'vs/base/common/errors';
31+
import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
32+
import { basename } from 'vs/base/common/resources';
33+
import { filter } from 'vs/base/common/objects';
3034

3135

3236

@@ -69,6 +73,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
6973
commands: ExtHostCommands,
7074
private _textDocumentsAndEditors: ExtHostDocumentsAndEditors,
7175
private _textDocuments: ExtHostDocuments,
76+
private _extHostFileSystem: IExtHostConsumerFileSystem
7277
) {
7378
this._notebookProxy = mainContext.getProxy(MainContext.MainThreadNotebook);
7479
this._notebookDocumentsProxy = mainContext.getProxy(MainContext.MainThreadNotebookDocuments);
@@ -263,14 +268,14 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
263268
// --- serialize/deserialize
264269

265270
private _handlePool = 0;
266-
private readonly _notebookSerializer = new Map<number, vscode.NotebookSerializer>();
271+
private readonly _notebookSerializer = new Map<number, { viewType: string; serializer: vscode.NotebookSerializer; options: vscode.NotebookDocumentContentOptions | undefined }>();
267272

268273
registerNotebookSerializer(extension: IExtensionDescription, viewType: string, serializer: vscode.NotebookSerializer, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData): vscode.Disposable {
269274
if (isFalsyOrWhitespace(viewType)) {
270275
throw new Error(`viewType cannot be empty or just whitespace`);
271276
}
272277
const handle = this._handlePool++;
273-
this._notebookSerializer.set(handle, serializer);
278+
this._notebookSerializer.set(handle, { viewType, serializer, options });
274279
this._notebookProxy.$registerNotebookSerializer(
275280
handle,
276281
{ id: extension.identifier, location: extension.extensionLocation },
@@ -288,7 +293,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
288293
if (!serializer) {
289294
throw new Error('NO serializer found');
290295
}
291-
const data = await serializer.deserializeNotebook(bytes.buffer, token);
296+
const data = await serializer.serializer.deserializeNotebook(bytes.buffer, token);
292297
return new SerializableObjectWithBuffers(typeConverters.NotebookData.from(data));
293298
}
294299

@@ -297,10 +302,67 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
297302
if (!serializer) {
298303
throw new Error('NO serializer found');
299304
}
300-
const bytes = await serializer.serializeNotebook(typeConverters.NotebookData.to(data.value), token);
305+
const bytes = await serializer.serializer.serializeNotebook(typeConverters.NotebookData.to(data.value), token);
301306
return VSBuffer.wrap(bytes);
302307
}
303308

309+
async $saveNotebook(handle: number, uriComponents: UriComponents, versionId: number, options: files.IWriteFileOptions, token: CancellationToken): Promise<INotebookPartialFileStatsWithMetadata> {
310+
const uri = URI.revive(uriComponents);
311+
const serializer = this._notebookSerializer.get(handle);
312+
if (!serializer) {
313+
throw new Error('NO serializer found');
314+
}
315+
316+
const document = this._documents.get(uri);
317+
if (!document) {
318+
throw new Error('Document NOT found');
319+
}
320+
321+
if (document.versionId !== versionId) {
322+
throw new Error('Document version mismatch');
323+
}
324+
325+
const data: vscode.NotebookData = {
326+
metadata: filter(document.apiNotebook.metadata, key => !(serializer.options?.transientDocumentMetadata ?? {})[key]),
327+
cells: [],
328+
};
329+
330+
for (const cell of document.apiNotebook.getCells()) {
331+
const cellData = new extHostTypes.NotebookCellData(
332+
cell.kind,
333+
cell.document.getText(),
334+
cell.document.languageId,
335+
cell.mime,
336+
!(serializer.options?.transientOutputs) ? [...cell.outputs] : [],
337+
cell.metadata,
338+
cell.executionSummary
339+
);
340+
341+
cellData.metadata = filter(cell.metadata, key => !(serializer.options?.transientCellMetadata ?? {})[key]);
342+
data.cells.push(cellData);
343+
}
344+
345+
const bytes = await serializer.serializer.serializeNotebook(data, token);
346+
await this._extHostFileSystem.value.writeFile(uri, bytes);
347+
const stat = await this._extHostFileSystem.value.stat(uri);
348+
349+
const fileStats = {
350+
name: basename(uri), // providerExtUri.basename(resource)
351+
isFile: (stat.type & files.FileType.File) !== 0,
352+
isDirectory: (stat.type & files.FileType.Directory) !== 0,
353+
isSymbolicLink: (stat.type & files.FileType.SymbolicLink) !== 0,
354+
mtime: stat.mtime,
355+
ctime: stat.ctime,
356+
size: stat.size,
357+
readonly: Boolean((stat.permissions ?? 0) & files.FilePermission.Readonly) || !this._extHostFileSystem.value.isWritableFileSystem(uri.scheme),
358+
locked: Boolean((stat.permissions ?? 0) & files.FilePermission.Locked),
359+
etag: files.etag({ mtime: stat.mtime, size: stat.size }),
360+
children: undefined
361+
};
362+
363+
return fileStats;
364+
}
365+
304366
// --- open, save, saveAs, backup
305367

306368

src/vs/workbench/api/common/extHostNotebookDocument.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ export class ExtHostNotebookDocument {
186186
this._disposed = true;
187187
}
188188

189+
get versionId(): number {
190+
return this._versionId;
191+
}
192+
189193
get apiNotebook(): vscode.NotebookDocument {
190194
if (!this._notebook) {
191195
const that = this;

src/vs/workbench/api/test/browser/extHostNotebook.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNoteboo
2424
import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier';
2525
import { VSBuffer } from 'vs/base/common/buffer';
2626
import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry';
27+
import { ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
28+
import { ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
2729

2830
suite('NotebookCell#Document', function () {
2931

@@ -34,6 +36,7 @@ suite('NotebookCell#Document', function () {
3436
let extHostDocuments: ExtHostDocuments;
3537
let extHostNotebooks: ExtHostNotebookController;
3638
let extHostNotebookDocuments: ExtHostNotebookDocuments;
39+
let extHostConsumerFileSystem: ExtHostConsumerFileSystem;
3740

3841
const notebookUri = URI.parse('test:///notebook.file');
3942
const disposables = new DisposableStore();
@@ -53,11 +56,12 @@ suite('NotebookCell#Document', function () {
5356
});
5457
extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService());
5558
extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors);
59+
extHostConsumerFileSystem = new ExtHostConsumerFileSystem(rpcProtocol, new ExtHostFileSystemInfo());
5660
extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService(), new class extends mock<IExtHostTelemetry>() {
5761
override onExtensionError(): boolean {
5862
return true;
5963
}
60-
}), extHostDocumentsAndEditors, extHostDocuments);
64+
}), extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem);
6165
extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks);
6266

6367
const reg = extHostNotebooks.registerNotebookSerializer(nullExtensionDescription, 'test', new class extends mock<vscode.NotebookSerializer>() { });

src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/
2626
import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol';
2727
import { mock } from 'vs/workbench/test/common/workbenchTestServices';
2828
import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry';
29+
import { ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
30+
import { ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
2931

3032
suite('NotebookKernel', function () {
3133

@@ -37,6 +39,7 @@ suite('NotebookKernel', function () {
3739
let extHostNotebooks: ExtHostNotebookController;
3840
let extHostNotebookDocuments: ExtHostNotebookDocuments;
3941
let extHostCommands: ExtHostCommands;
42+
let extHostConsumerFileSystem: ExtHostConsumerFileSystem;
4043

4144
const notebookUri = URI.parse('test:///notebook.file');
4245
const kernelData = new Map<number, INotebookKernelDto2>();
@@ -94,7 +97,8 @@ suite('NotebookKernel', function () {
9497
return true;
9598
}
9699
});
97-
extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments);
100+
extHostConsumerFileSystem = new ExtHostConsumerFileSystem(rpcProtocol, new ExtHostFileSystemInfo());
101+
extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem);
98102

99103
extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks);
100104

src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { Schemas } from 'vs/base/common/network';
1313
import { filter } from 'vs/base/common/objects';
1414
import { assertType } from 'vs/base/common/types';
1515
import { URI } from 'vs/base/common/uri';
16+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
17+
import { IWriteFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files';
1618
import { IRevertOptions, ISaveOptions, IUntypedEditorInput } from 'vs/workbench/common/editor';
1719
import { EditorModel } from 'vs/workbench/common/editor/editorModel';
1820
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
@@ -175,10 +177,12 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF
175177
readonly onWillDispose: Event<void>;
176178

177179
readonly configuration: IFileWorkingCopyModelConfiguration | undefined = undefined;
180+
save: ((options: IWriteFileOptions, token: CancellationToken) => Promise<IFileStatWithMetadata>) | undefined;
178181

179182
constructor(
180183
private readonly _notebookModel: NotebookTextModel,
181-
private readonly _notebookService: INotebookService
184+
private readonly _notebookService: INotebookService,
185+
private readonly _configurationService: IConfigurationService
182186
) {
183187
super();
184188

@@ -209,6 +213,20 @@ export class NotebookFileWorkingCopyModel extends Disposable implements IStoredF
209213
// remote hosts the extension of the notebook with the contents truth
210214
backupDelay: 10000
211215
};
216+
217+
// Override save behavior to avoid transferring the buffer across the wire 3 times
218+
if (this._configurationService.getValue('notebook.experimental.remoteSave')) {
219+
this.save = async (options: IWriteFileOptions, token: CancellationToken) => {
220+
const serializer = await this.getNotebookSerializer();
221+
222+
if (token.isCancellationRequested) {
223+
throw new CancellationError();
224+
}
225+
226+
const stat = await serializer.save(this._notebookModel.uri, this._notebookModel.versionId, options, token);
227+
return stat;
228+
};
229+
}
212230
}
213231
}
214232

@@ -287,6 +305,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo
287305
constructor(
288306
private readonly _viewType: string,
289307
@INotebookService private readonly _notebookService: INotebookService,
308+
@IConfigurationService private readonly _configurationService: IConfigurationService,
290309
) { }
291310

292311
async createModel(resource: URI, stream: VSBufferReadableStream, token: CancellationToken): Promise<NotebookFileWorkingCopyModel> {
@@ -304,7 +323,7 @@ export class NotebookFileWorkingCopyModelFactory implements IStoredFileWorkingCo
304323
}
305324

306325
const notebookModel = this._notebookService.createNotebookTextModel(info.viewType, resource, data, info.serializer.options);
307-
return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService);
326+
return new NotebookFileWorkingCopyModel(notebookModel, this._notebookService, this._configurationService);
308327
}
309328
}
310329

src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Schemas } from 'vs/base/common/network';
2020
import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider';
2121
import { assertIsDefined } from 'vs/base/common/types';
2222
import { CancellationToken } from 'vs/base/common/cancellation';
23+
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
2324

2425
class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IResolvedNotebookEditorModel>> {
2526

@@ -40,6 +41,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IReso
4041
@IInstantiationService private readonly _instantiationService: IInstantiationService,
4142
@INotebookService private readonly _notebookService: INotebookService,
4243
@ILogService private readonly _logService: ILogService,
44+
@IConfigurationService private readonly _configurationService: IConfigurationService,
4345
) {
4446
super();
4547
}
@@ -65,7 +67,7 @@ class NotebookModelReferenceCollection extends ReferenceCollection<Promise<IReso
6567
const workingCopyTypeId = NotebookWorkingCopyTypeIdentifier.create(viewType);
6668
let workingCopyManager = this._workingCopyManagers.get(workingCopyTypeId);
6769
if (!workingCopyManager) {
68-
const factory = new NotebookFileWorkingCopyModelFactory(viewType, this._notebookService);
70+
const factory = new NotebookFileWorkingCopyModelFactory(viewType, this._notebookService, this._configurationService);
6971
workingCopyManager = <IFileWorkingCopyManager<NotebookFileWorkingCopyModel, NotebookFileWorkingCopyModel>><any>this._instantiationService.createInstance(
7072
FileWorkingCopyManager,
7173
workingCopyTypeId,

src/vs/workbench/contrib/notebook/common/notebookService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/mode
1414
import { IDisposable } from 'vs/base/common/lifecycle';
1515
import { VSBuffer } from 'vs/base/common/buffer';
1616
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
17+
import { IFileStatWithMetadata, IWriteFileOptions } from 'vs/platform/files/common/files';
1718

1819

1920
export const INotebookService = createDecorator<INotebookService>('notebookService');
@@ -29,6 +30,7 @@ export interface INotebookSerializer {
2930
options: TransientOptions;
3031
dataToNotebook(data: VSBuffer): Promise<NotebookData>;
3132
notebookToData(data: NotebookData): Promise<VSBuffer>;
33+
save(uri: URI, versionId: number, options: IWriteFileOptions, token: CancellationToken): Promise<IFileStatWithMetadata>;
3234
}
3335

3436
export interface INotebookRawData {

0 commit comments

Comments
 (0)