Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts
5238 views
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 { Event } from '../../../base/common/event.js';
7
import { URI, UriComponents } from '../../../base/common/uri.js';
8
import { illegalState } from '../../../base/common/errors.js';
9
import { ExtHostDocumentSaveParticipantShape, IWorkspaceEditDto, MainThreadBulkEditsShape } from './extHost.protocol.js';
10
import { TextEdit } from './extHostTypes.js';
11
import { Range, TextDocumentSaveReason, EndOfLine } from './extHostTypeConverters.js';
12
import { ExtHostDocuments } from './extHostDocuments.js';
13
import { SaveReason } from '../../common/editor.js';
14
import type * as vscode from 'vscode';
15
import { LinkedList } from '../../../base/common/linkedList.js';
16
import { ILogService } from '../../../platform/log/common/log.js';
17
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
18
import { SerializableObjectWithBuffers } from '../../services/extensions/common/proxyIdentifier.js';
19
20
type Listener = [Function, unknown, IExtensionDescription];
21
22
export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSaveParticipantShape {
23
24
private readonly _callbacks = new LinkedList<Listener>();
25
private readonly _badListeners = new WeakMap<Function, number>();
26
27
constructor(
28
private readonly _logService: ILogService,
29
private readonly _documents: ExtHostDocuments,
30
private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape,
31
private readonly _thresholds: { timeout: number; errors: number } = { timeout: 1500, errors: 3 }
32
) {
33
//
34
}
35
36
dispose(): void {
37
this._callbacks.clear();
38
}
39
40
getOnWillSaveTextDocumentEvent(extension: IExtensionDescription): Event<vscode.TextDocumentWillSaveEvent> {
41
return (listener, thisArg, disposables) => {
42
const remove = this._callbacks.push([listener, thisArg, extension]);
43
const result = { dispose: remove };
44
if (Array.isArray(disposables)) {
45
disposables.push(result);
46
}
47
return result;
48
};
49
}
50
51
async $participateInSave(data: UriComponents, reason: SaveReason): Promise<boolean[]> {
52
const resource = URI.revive(data);
53
54
let didTimeout = false;
55
const didTimeoutHandle = setTimeout(() => didTimeout = true, this._thresholds.timeout);
56
57
const results: boolean[] = [];
58
try {
59
for (const listener of [...this._callbacks]) { // copy to prevent concurrent modifications
60
if (didTimeout) {
61
// timeout - no more listeners
62
break;
63
}
64
const document = this._documents.getDocument(resource);
65
66
const success = await this._deliverEventAsyncAndBlameBadListeners(listener, { document, reason: TextDocumentSaveReason.to(reason) });
67
results.push(success);
68
}
69
} finally {
70
clearTimeout(didTimeoutHandle);
71
}
72
return results;
73
}
74
75
private _deliverEventAsyncAndBlameBadListeners([listener, thisArg, extension]: Listener, stubEvent: Pick<vscode.TextDocumentWillSaveEvent, 'document' | 'reason'>): Promise<boolean> {
76
const errors = this._badListeners.get(listener);
77
if (typeof errors === 'number' && errors > this._thresholds.errors) {
78
// bad listener - ignore
79
return Promise.resolve(false);
80
}
81
82
return this._deliverEventAsync(extension, listener, thisArg, stubEvent).then(() => {
83
// don't send result across the wire
84
return true;
85
86
}, err => {
87
88
this._logService.error(`onWillSaveTextDocument-listener from extension '${extension.identifier.value}' threw ERROR`);
89
this._logService.error(err);
90
91
if (!(err instanceof Error) || (<Error>err).message !== 'concurrent_edits') {
92
const errors = this._badListeners.get(listener);
93
this._badListeners.set(listener, !errors ? 1 : errors + 1);
94
95
if (typeof errors === 'number' && errors > this._thresholds.errors) {
96
this._logService.info(`onWillSaveTextDocument-listener from extension '${extension.identifier.value}' will now be IGNORED because of timeouts and/or errors`);
97
}
98
}
99
return false;
100
});
101
}
102
103
private _deliverEventAsync(extension: IExtensionDescription, listener: Function, thisArg: unknown, stubEvent: Pick<vscode.TextDocumentWillSaveEvent, 'document' | 'reason'>): Promise<boolean | undefined> {
104
105
const promises: Promise<vscode.TextEdit[]>[] = [];
106
107
const t1 = Date.now();
108
const { document, reason } = stubEvent;
109
const { version } = document;
110
111
const event = Object.freeze<vscode.TextDocumentWillSaveEvent>({
112
document,
113
reason,
114
// eslint-disable-next-line @typescript-eslint/no-explicit-any
115
waitUntil(p: Promise<any | vscode.TextEdit[]>) {
116
if (Object.isFrozen(promises)) {
117
throw illegalState('waitUntil can not be called async');
118
}
119
promises.push(Promise.resolve(p));
120
}
121
});
122
123
try {
124
// fire event
125
listener.apply(thisArg, [event]);
126
} catch (err) {
127
return Promise.reject(err);
128
}
129
130
// freeze promises after event call
131
Object.freeze(promises);
132
133
return new Promise<vscode.TextEdit[][]>((resolve, reject) => {
134
// join on all listener promises, reject after timeout
135
const handle = setTimeout(() => reject(new Error('timeout')), this._thresholds.timeout);
136
137
return Promise.all(promises).then(edits => {
138
this._logService.debug(`onWillSaveTextDocument-listener from extension '${extension.identifier.value}' finished after ${(Date.now() - t1)}ms`);
139
clearTimeout(handle);
140
resolve(edits);
141
}).catch(err => {
142
clearTimeout(handle);
143
reject(err);
144
});
145
146
}).then(values => {
147
const dto: IWorkspaceEditDto = { edits: [] };
148
for (const value of values) {
149
if (Array.isArray(value) && (<vscode.TextEdit[]>value).every(e => e instanceof TextEdit)) {
150
for (const { newText, newEol, range } of value) {
151
dto.edits.push({
152
resource: document.uri,
153
versionId: undefined,
154
textEdit: {
155
range: range && Range.from(range),
156
text: newText,
157
eol: newEol && EndOfLine.from(newEol),
158
}
159
});
160
}
161
}
162
}
163
164
// apply edits if any and if document
165
// didn't change somehow in the meantime
166
if (dto.edits.length === 0) {
167
return undefined;
168
}
169
170
if (version === document.version) {
171
return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(new SerializableObjectWithBuffers(dto));
172
}
173
174
return Promise.reject(new Error('concurrent_edits'));
175
});
176
}
177
}
178
179