Path: blob/main/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts
5238 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Event } from '../../../base/common/event.js';6import { URI, UriComponents } from '../../../base/common/uri.js';7import { illegalState } from '../../../base/common/errors.js';8import { ExtHostDocumentSaveParticipantShape, IWorkspaceEditDto, MainThreadBulkEditsShape } from './extHost.protocol.js';9import { TextEdit } from './extHostTypes.js';10import { Range, TextDocumentSaveReason, EndOfLine } from './extHostTypeConverters.js';11import { ExtHostDocuments } from './extHostDocuments.js';12import { SaveReason } from '../../common/editor.js';13import type * as vscode from 'vscode';14import { LinkedList } from '../../../base/common/linkedList.js';15import { ILogService } from '../../../platform/log/common/log.js';16import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';17import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js';1819type Listener = [Function, unknown, IExtensionDescription];2021export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSaveParticipantShape {2223private readonly _callbacks = new LinkedList<Listener>();24private readonly _badListeners = new WeakMap<Function, number>();2526constructor(27private readonly _logService: ILogService,28private readonly _documents: ExtHostDocuments,29private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape,30private readonly _thresholds: { timeout: number; errors: number } = { timeout: 1500, errors: 3 }31) {32//33}3435dispose(): void {36this._callbacks.clear();37}3839getOnWillSaveTextDocumentEvent(extension: IExtensionDescription): Event<vscode.TextDocumentWillSaveEvent> {40return (listener, thisArg, disposables) => {41const remove = this._callbacks.push([listener, thisArg, extension]);42const result = { dispose: remove };43if (Array.isArray(disposables)) {44disposables.push(result);45}46return result;47};48}4950async $participateInSave(data: UriComponents, reason: SaveReason): Promise<boolean[]> {51const resource = URI.revive(data);5253let didTimeout = false;54const didTimeoutHandle = setTimeout(() => didTimeout = true, this._thresholds.timeout);5556const results: boolean[] = [];57try {58for (const listener of [...this._callbacks]) { // copy to prevent concurrent modifications59if (didTimeout) {60// timeout - no more listeners61break;62}63const document = this._documents.getDocument(resource);6465const success = await this._deliverEventAsyncAndBlameBadListeners(listener, { document, reason: TextDocumentSaveReason.to(reason) });66results.push(success);67}68} finally {69clearTimeout(didTimeoutHandle);70}71return results;72}7374private _deliverEventAsyncAndBlameBadListeners([listener, thisArg, extension]: Listener, stubEvent: Pick<vscode.TextDocumentWillSaveEvent, 'document' | 'reason'>): Promise<boolean> {75const errors = this._badListeners.get(listener);76if (typeof errors === 'number' && errors > this._thresholds.errors) {77// bad listener - ignore78return Promise.resolve(false);79}8081return this._deliverEventAsync(extension, listener, thisArg, stubEvent).then(() => {82// don't send result across the wire83return true;8485}, err => {8687this._logService.error(`onWillSaveTextDocument-listener from extension '${extension.identifier.value}' threw ERROR`);88this._logService.error(err);8990if (!(err instanceof Error) || (<Error>err).message !== 'concurrent_edits') {91const errors = this._badListeners.get(listener);92this._badListeners.set(listener, !errors ? 1 : errors + 1);9394if (typeof errors === 'number' && errors > this._thresholds.errors) {95this._logService.info(`onWillSaveTextDocument-listener from extension '${extension.identifier.value}' will now be IGNORED because of timeouts and/or errors`);96}97}98return false;99});100}101102private _deliverEventAsync(extension: IExtensionDescription, listener: Function, thisArg: unknown, stubEvent: Pick<vscode.TextDocumentWillSaveEvent, 'document' | 'reason'>): Promise<boolean | undefined> {103104const promises: Promise<vscode.TextEdit[]>[] = [];105106const t1 = Date.now();107const { document, reason } = stubEvent;108const { version } = document;109110const event = Object.freeze<vscode.TextDocumentWillSaveEvent>({111document,112reason,113// eslint-disable-next-line @typescript-eslint/no-explicit-any114waitUntil(p: Promise<any | vscode.TextEdit[]>) {115if (Object.isFrozen(promises)) {116throw illegalState('waitUntil can not be called async');117}118promises.push(Promise.resolve(p));119}120});121122try {123// fire event124listener.apply(thisArg, [event]);125} catch (err) {126return Promise.reject(err);127}128129// freeze promises after event call130Object.freeze(promises);131132return new Promise<vscode.TextEdit[][]>((resolve, reject) => {133// join on all listener promises, reject after timeout134const handle = setTimeout(() => reject(new Error('timeout')), this._thresholds.timeout);135136return Promise.all(promises).then(edits => {137this._logService.debug(`onWillSaveTextDocument-listener from extension '${extension.identifier.value}' finished after ${(Date.now() - t1)}ms`);138clearTimeout(handle);139resolve(edits);140}).catch(err => {141clearTimeout(handle);142reject(err);143});144145}).then(values => {146const dto: IWorkspaceEditDto = { edits: [] };147for (const value of values) {148if (Array.isArray(value) && (<vscode.TextEdit[]>value).every(e => e instanceof TextEdit)) {149for (const { newText, newEol, range } of value) {150dto.edits.push({151resource: document.uri,152versionId: undefined,153textEdit: {154range: range && Range.from(range),155text: newText,156eol: newEol && EndOfLine.from(newEol),157}158});159}160}161}162163// apply edits if any and if document164// didn't change somehow in the meantime165if (dto.edits.length === 0) {166return undefined;167}168169if (version === document.version) {170return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers(dto));171}172173return Promise.reject(new Error('concurrent_edits'));174});175}176}177178179