-
Notifications
You must be signed in to change notification settings - Fork 29.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
📓 OnWillSaveNotebookDocument (#177421)
* 📓 OnWillSaveNotebookDocument * Proposed api check
- Loading branch information
Showing
9 changed files
with
327 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
src/vs/workbench/api/browser/mainThreadNotebookSaveParticipant.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import { CancellationToken } from 'vs/base/common/cancellation'; | ||
import { localize } from 'vs/nls'; | ||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; | ||
import { IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; | ||
import { extHostCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; | ||
import { SaveReason } from 'vs/workbench/common/editor'; | ||
import { ExtHostContext, ExtHostNotebookDocumentSaveParticipantShape } from '../common/extHost.protocol'; | ||
import { IDisposable } from 'vs/base/common/lifecycle'; | ||
import { raceCancellationError } from 'vs/base/common/async'; | ||
import { IStoredFileWorkingCopySaveParticipant, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; | ||
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; | ||
import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; | ||
|
||
class ExtHostNotebookDocumentSaveParticipant implements IStoredFileWorkingCopySaveParticipant { | ||
|
||
private readonly _proxy: ExtHostNotebookDocumentSaveParticipantShape; | ||
|
||
constructor(extHostContext: IExtHostContext) { | ||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookDocumentSaveParticipant); | ||
} | ||
|
||
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, env: { reason: SaveReason }, _progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> { | ||
|
||
if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) { | ||
return undefined; | ||
} | ||
|
||
let _warningTimeout: any; | ||
|
||
const p = new Promise<any>((resolve, reject) => { | ||
|
||
_warningTimeout = setTimeout( | ||
() => reject(new Error(localize('timeout.onWillSave', "Aborted onWillSaveTextDocument-event after 1750ms"))), | ||
1750 | ||
); | ||
this._proxy.$participateInSave(workingCopy.resource, env.reason).then(values => { | ||
clearTimeout(_warningTimeout); | ||
if (!values.every(success => success)) { | ||
return Promise.reject(new Error('listener failed')); | ||
} | ||
return undefined; | ||
}).then(resolve, reject); | ||
}); | ||
|
||
return raceCancellationError(p, token); | ||
} | ||
} | ||
|
||
@extHostCustomer | ||
export class SaveParticipant { | ||
|
||
private _saveParticipantDisposable: IDisposable; | ||
|
||
constructor( | ||
extHostContext: IExtHostContext, | ||
@IInstantiationService instantiationService: IInstantiationService, | ||
@IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService | ||
) { | ||
this._saveParticipantDisposable = this.workingCopyFileService.addSaveParticipant(instantiationService.createInstance(ExtHostNotebookDocumentSaveParticipant, extHostContext)); | ||
} | ||
|
||
dispose(): void { | ||
this._saveParticipantDisposable.dispose(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
165 changes: 165 additions & 0 deletions
165
src/vs/workbench/api/common/extHostNotebookDocumentSaveParticipant.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import { Event } from 'vs/base/common/event'; | ||
import { URI, UriComponents } from 'vs/base/common/uri'; | ||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; | ||
import { ExtHostNotebookDocumentSaveParticipantShape, IWorkspaceEditDto, MainThreadBulkEditsShape } from 'vs/workbench/api/common/extHost.protocol'; | ||
import { LinkedList } from 'vs/base/common/linkedList'; | ||
import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; | ||
import { ILogService } from 'vs/platform/log/common/log'; | ||
import { NotebookDocumentWillSaveEvent, WorkspaceEdit } from 'vscode'; | ||
import { illegalState } from 'vs/base/common/errors'; | ||
import { NotebookDocumentSaveReason, WorkspaceEdit as WorksapceEditConverter } from 'vs/workbench/api/common/extHostTypeConverters'; | ||
import { SaveReason } from 'vs/workbench/common/editor'; | ||
|
||
type Listener = [Function, any, IExtensionDescription]; | ||
export class ExtHostNotebookDocumentSaveParticipant implements ExtHostNotebookDocumentSaveParticipantShape { | ||
private readonly _callbacks = new LinkedList<Listener>(); | ||
private readonly _badListeners = new WeakMap<Function, number>(); | ||
|
||
constructor( | ||
private readonly _logService: ILogService, | ||
private readonly _notebooksAndEditors: ExtHostNotebookController, | ||
private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape, | ||
private readonly _thresholds: { timeout: number; errors: number } = { timeout: 1500, errors: 3 }) { | ||
|
||
} | ||
|
||
dispose(): void { | ||
this._callbacks.clear(); | ||
} | ||
|
||
getOnWillSaveNotebookDocumentEvent(extension: IExtensionDescription): Event<NotebookDocumentWillSaveEvent> { | ||
return (listener, thisArg, disposables) => { | ||
const remove = this._callbacks.push([listener, thisArg, extension]); | ||
const result = { dispose: remove }; | ||
if (Array.isArray(disposables)) { | ||
disposables.push(result); | ||
} | ||
return result; | ||
}; | ||
} | ||
|
||
async $participateInSave(resource: UriComponents, reason: SaveReason): Promise<boolean[]> { | ||
const revivedUri = URI.revive(resource); | ||
let didTimeout = false; | ||
const didTimeoutHandle = setTimeout(() => didTimeout = true, this._thresholds.timeout); | ||
|
||
const results: boolean[] = []; | ||
|
||
try { | ||
for (const listener of [...this._callbacks]) { // copy to prevent concurrent modifications | ||
if (didTimeout) { | ||
break; | ||
} | ||
const document = this._notebooksAndEditors.getNotebookDocument(revivedUri); | ||
const success = await this._deliverEventAsyncAndBlameBadListeners(listener, <any>{ document, reason: NotebookDocumentSaveReason.to(reason) }); | ||
results.push(success); | ||
} | ||
} finally { | ||
clearTimeout(didTimeoutHandle); | ||
} | ||
|
||
return results; | ||
} | ||
|
||
private _deliverEventAsyncAndBlameBadListeners([listener, thisArg, extension]: Listener, stubEvent: NotebookDocumentWillSaveEvent): Promise<any> { | ||
const errors = this._badListeners.get(listener); | ||
if (typeof errors === 'number' && errors > this._thresholds.errors) { | ||
// bad listener - ignore | ||
return Promise.resolve(false); | ||
} | ||
|
||
return this._deliverEventAsync(extension, listener, thisArg, stubEvent).then(() => { | ||
// don't send result across the wire | ||
return true; | ||
|
||
}, err => { | ||
|
||
this._logService.error(`onWillSaveNotebookDocument-listener from extension '${extension.identifier.value}' threw ERROR`); | ||
this._logService.error(err); | ||
|
||
if (!(err instanceof Error) || (<Error>err).message !== 'concurrent_edits') { | ||
const errors = this._badListeners.get(listener); | ||
this._badListeners.set(listener, !errors ? 1 : errors + 1); | ||
|
||
if (typeof errors === 'number' && errors > this._thresholds.errors) { | ||
this._logService.info(`onWillSaveNotebookDocument-listener from extension '${extension.identifier.value}' will now be IGNORED because of timeouts and/or errors`); | ||
} | ||
} | ||
return false; | ||
}); | ||
} | ||
|
||
private _deliverEventAsync(extension: IExtensionDescription, listener: Function, thisArg: any, stubEvent: NotebookDocumentWillSaveEvent): Promise<any> { | ||
|
||
const promises: Promise<WorkspaceEdit[]>[] = []; | ||
|
||
const t1 = Date.now(); | ||
const { document, reason } = stubEvent; | ||
const { version } = document; | ||
|
||
const event = Object.freeze<NotebookDocumentWillSaveEvent>({ | ||
document, | ||
reason, | ||
waitUntil(p: Promise<any | WorkspaceEdit[]>) { | ||
if (Object.isFrozen(promises)) { | ||
throw illegalState('waitUntil can not be called async'); | ||
} | ||
promises.push(Promise.resolve(p)); | ||
} | ||
}); | ||
|
||
try { | ||
// fire event | ||
listener.apply(thisArg, [event]); | ||
} catch (err) { | ||
return Promise.reject(err); | ||
} | ||
|
||
// freeze promises after event call | ||
Object.freeze(promises); | ||
|
||
return new Promise<WorkspaceEdit[][]>((resolve, reject) => { | ||
// join on all listener promises, reject after timeout | ||
const handle = setTimeout(() => reject(new Error('timeout')), this._thresholds.timeout); | ||
|
||
return Promise.all(promises).then(edits => { | ||
this._logService.debug(`onWillSaveNotebookDocument-listener from extension '${extension.identifier.value}' finished after ${(Date.now() - t1)}ms`); | ||
clearTimeout(handle); | ||
resolve(edits); | ||
}).catch(err => { | ||
clearTimeout(handle); | ||
reject(err); | ||
}); | ||
|
||
}).then(values => { | ||
const dto: IWorkspaceEditDto = { edits: [] }; | ||
for (const value of values) { | ||
if (Array.isArray(value)) { | ||
for (const edit of value) { | ||
if (edit) { | ||
const editDto = WorksapceEditConverter.from(edit); | ||
dto.edits.push(...editDto.edits); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// apply edits if any and if document | ||
// didn't change somehow in the meantime | ||
if (dto.edits.length === 0) { | ||
return undefined; | ||
} | ||
|
||
if (version === document.version) { | ||
return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(dto); | ||
} | ||
|
||
return Promise.reject(new Error('concurrent_edits')); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
src/vscode-dts/vscode.proposed.notebokDocumentWillSave.d.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
declare module 'vscode' { | ||
/** | ||
* Represents reasons why a notebook document is saved. | ||
*/ | ||
export enum NotebookDocumentSaveReason { | ||
|
||
/** | ||
* Manually triggered, e.g. by the user pressing save, by starting debugging, | ||
* or by an API call. | ||
*/ | ||
Manual = 1, | ||
|
||
/** | ||
* Automatic after a delay. | ||
*/ | ||
AfterDelay = 2, | ||
|
||
/** | ||
* When the editor lost focus. | ||
*/ | ||
FocusOut = 3 | ||
} | ||
|
||
/** | ||
* An event that is fired when a {@link NotebookDocument document} will be saved. | ||
* | ||
* To make modifications to the document before it is being saved, call the | ||
* {@linkcode NotebookDocumentWillSaveEvent.waitUntil waitUntil}-function with a thenable | ||
* that resolves to an array of {@link TextEdit text edits}. | ||
*/ | ||
export interface NotebookDocumentWillSaveEvent { | ||
|
||
/** | ||
* The document that will be saved. | ||
*/ | ||
readonly document: NotebookDocument; | ||
|
||
/** | ||
* The reason why save was triggered. | ||
*/ | ||
readonly reason: NotebookDocumentSaveReason; | ||
|
||
waitUntil(thenable: Thenable<readonly WorkspaceEdit[]>): void; | ||
|
||
waitUntil(thenable: Thenable<any>): void; | ||
} | ||
|
||
export namespace workspace { | ||
|
||
export const onWillSaveNotebookDocument: Event<NotebookDocumentWillSaveEvent>; | ||
} | ||
} |