Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/clipboard/browser/clipboardService.ts
3296 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 { isSafari, isWebkitWebView } from '../../../base/browser/browser.js';
7
import { $, addDisposableListener, getActiveDocument, getActiveWindow, isHTMLElement, onDidRegisterWindow } from '../../../base/browser/dom.js';
8
import { mainWindow } from '../../../base/browser/window.js';
9
import { DeferredPromise } from '../../../base/common/async.js';
10
import { Event } from '../../../base/common/event.js';
11
import { hash } from '../../../base/common/hash.js';
12
import { Disposable } from '../../../base/common/lifecycle.js';
13
import { URI } from '../../../base/common/uri.js';
14
import { IClipboardService } from '../common/clipboardService.js';
15
import { ILayoutService } from '../../layout/browser/layoutService.js';
16
import { ILogService } from '../../log/common/log.js';
17
18
/**
19
* Custom mime type used for storing a list of uris in the clipboard.
20
*
21
* Requires support for custom web clipboards https://github.com/w3c/clipboard-apis/pull/175
22
*/
23
const vscodeResourcesMime = 'application/vnd.code.resources';
24
25
export class BrowserClipboardService extends Disposable implements IClipboardService {
26
27
declare readonly _serviceBrand: undefined;
28
29
constructor(
30
@ILayoutService private readonly layoutService: ILayoutService,
31
@ILogService protected readonly logService: ILogService
32
) {
33
super();
34
35
if (isSafari || isWebkitWebView) {
36
this.installWebKitWriteTextWorkaround();
37
}
38
39
// Keep track of copy operations to reset our set of
40
// copied resources: since we keep resources in memory
41
// and not in the clipboard, we have to invalidate
42
// that state when the user copies other data.
43
this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {
44
disposables.add(addDisposableListener(window.document, 'copy', () => this.clearResourcesState()));
45
}, { window: mainWindow, disposables: this._store }));
46
}
47
48
triggerPaste(): Promise<void> | undefined {
49
this.logService.trace('BrowserClipboardService#triggerPaste');
50
return undefined;
51
}
52
53
async readImage(): Promise<Uint8Array> {
54
try {
55
const clipboardItems = await navigator.clipboard.read();
56
const clipboardItem = clipboardItems[0];
57
58
const supportedImageTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/tiff', 'image/bmp'];
59
const mimeType = supportedImageTypes.find(type => clipboardItem.types.includes(type));
60
61
if (mimeType) {
62
const blob = await clipboardItem.getType(mimeType);
63
const buffer = await blob.arrayBuffer();
64
return new Uint8Array(buffer);
65
} else {
66
console.error('No supported image type found in the clipboard');
67
}
68
} catch (error) {
69
console.error('Error reading image from clipboard:', error);
70
}
71
72
// Return an empty Uint8Array if no image is found or an error occurs
73
return new Uint8Array(0);
74
}
75
76
private webKitPendingClipboardWritePromise: DeferredPromise<string> | undefined;
77
78
// In Safari, it has the following note:
79
//
80
// "The request to write to the clipboard must be triggered during a user gesture.
81
// A call to clipboard.write or clipboard.writeText outside the scope of a user
82
// gesture(such as "click" or "touch" event handlers) will result in the immediate
83
// rejection of the promise returned by the API call."
84
// From: https://webkit.org/blog/10855/async-clipboard-api/
85
//
86
// Since extensions run in a web worker, and handle gestures in an asynchronous way,
87
// they are not classified by Safari as "in response to a user gesture" and will reject.
88
//
89
// This function sets up some handlers to work around that behavior.
90
private installWebKitWriteTextWorkaround(): void {
91
const handler = () => {
92
const currentWritePromise = new DeferredPromise<string>();
93
94
// Cancel the previous promise since we just created a new one in response to this new event
95
if (this.webKitPendingClipboardWritePromise && !this.webKitPendingClipboardWritePromise.isSettled) {
96
this.webKitPendingClipboardWritePromise.cancel();
97
}
98
this.webKitPendingClipboardWritePromise = currentWritePromise;
99
100
// The ctor of ClipboardItem allows you to pass in a promise that will resolve to a string.
101
// This allows us to pass in a Promise that will either be cancelled by another event or
102
// resolved with the contents of the first call to this.writeText.
103
// see https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem/ClipboardItem#parameters
104
getActiveWindow().navigator.clipboard.write([new ClipboardItem({
105
'text/plain': currentWritePromise.p,
106
})]).catch(async err => {
107
if (!(err instanceof Error) || err.name !== 'NotAllowedError' || !currentWritePromise.isRejected) {
108
this.logService.error(err);
109
}
110
});
111
};
112
113
114
this._register(Event.runAndSubscribe(this.layoutService.onDidAddContainer, ({ container, disposables }) => {
115
disposables.add(addDisposableListener(container, 'click', handler));
116
disposables.add(addDisposableListener(container, 'keydown', handler));
117
}, { container: this.layoutService.mainContainer, disposables: this._store }));
118
}
119
120
private readonly mapTextToType = new Map<string, string>(); // unsupported in web (only in-memory)
121
122
async writeText(text: string, type?: string): Promise<void> {
123
this.logService.trace('BrowserClipboardService#writeText called with type:', type, ' text.length:', text.length);
124
// Clear resources given we are writing text
125
this.clearResourcesState();
126
127
// With type: only in-memory is supported
128
if (type) {
129
this.mapTextToType.set(type, text);
130
this.logService.trace('BrowserClipboardService#writeText');
131
return;
132
}
133
134
if (this.webKitPendingClipboardWritePromise) {
135
// For Safari, we complete this Promise which allows the call to `navigator.clipboard.write()`
136
// above to resolve and successfully copy to the clipboard. If we let this continue, Safari
137
// would throw an error because this call stack doesn't appear to originate from a user gesture.
138
return this.webKitPendingClipboardWritePromise.complete(text);
139
}
140
141
// Guard access to navigator.clipboard with try/catch
142
// as we have seen DOMExceptions in certain browsers
143
// due to security policies.
144
try {
145
this.logService.trace('before navigator.clipboard.writeText');
146
return await getActiveWindow().navigator.clipboard.writeText(text);
147
} catch (error) {
148
console.error(error);
149
}
150
151
// Fallback to textarea and execCommand solution
152
this.fallbackWriteText(text);
153
}
154
155
private fallbackWriteText(text: string): void {
156
this.logService.trace('BrowserClipboardService#fallbackWriteText');
157
const activeDocument = getActiveDocument();
158
const activeElement = activeDocument.activeElement;
159
160
const textArea: HTMLTextAreaElement = activeDocument.body.appendChild($('textarea', { 'aria-hidden': true }));
161
textArea.style.height = '1px';
162
textArea.style.width = '1px';
163
textArea.style.position = 'absolute';
164
165
textArea.value = text;
166
textArea.focus();
167
textArea.select();
168
169
activeDocument.execCommand('copy');
170
171
if (isHTMLElement(activeElement)) {
172
activeElement.focus();
173
}
174
175
textArea.remove();
176
}
177
178
async readText(type?: string): Promise<string> {
179
this.logService.trace('BrowserClipboardService#readText called with type:', type);
180
// With type: only in-memory is supported
181
if (type) {
182
const readText = this.mapTextToType.get(type) || '';
183
this.logService.trace('BrowserClipboardService#readText text.length:', readText.length);
184
return readText;
185
}
186
187
// Guard access to navigator.clipboard with try/catch
188
// as we have seen DOMExceptions in certain browsers
189
// due to security policies.
190
try {
191
const readText = await getActiveWindow().navigator.clipboard.readText();
192
this.logService.trace('BrowserClipboardService#readText text.length:', readText.length);
193
return readText;
194
} catch (error) {
195
console.error(error);
196
}
197
198
return '';
199
}
200
201
private findText = ''; // unsupported in web (only in-memory)
202
203
async readFindText(): Promise<string> {
204
return this.findText;
205
}
206
207
async writeFindText(text: string): Promise<void> {
208
this.findText = text;
209
}
210
211
private resources: URI[] = []; // unsupported in web (only in-memory)
212
private resourcesStateHash: number | undefined = undefined;
213
214
private static readonly MAX_RESOURCE_STATE_SOURCE_LENGTH = 1000;
215
216
async writeResources(resources: URI[]): Promise<void> {
217
// Guard access to navigator.clipboard with try/catch
218
// as we have seen DOMExceptions in certain browsers
219
// due to security policies.
220
try {
221
await getActiveWindow().navigator.clipboard.write([
222
new ClipboardItem({
223
[`web ${vscodeResourcesMime}`]: new Blob([
224
JSON.stringify(resources.map(x => x.toJSON()))
225
], {
226
type: vscodeResourcesMime
227
})
228
})
229
]);
230
231
// Continue to write to the in-memory clipboard as well.
232
// This is needed because some browsers allow the paste but then can't read the custom resources.
233
} catch (error) {
234
// Noop
235
}
236
237
if (resources.length === 0) {
238
this.clearResourcesState();
239
} else {
240
this.resources = resources;
241
this.resourcesStateHash = await this.computeResourcesStateHash();
242
}
243
}
244
245
async readResources(): Promise<URI[]> {
246
// Guard access to navigator.clipboard with try/catch
247
// as we have seen DOMExceptions in certain browsers
248
// due to security policies.
249
try {
250
const items = await getActiveWindow().navigator.clipboard.read();
251
for (const item of items) {
252
if (item.types.includes(`web ${vscodeResourcesMime}`)) {
253
const blob = await item.getType(`web ${vscodeResourcesMime}`);
254
const resources = (JSON.parse(await blob.text()) as URI[]).map(x => URI.from(x));
255
return resources;
256
}
257
}
258
} catch (error) {
259
// Noop
260
}
261
262
const resourcesStateHash = await this.computeResourcesStateHash();
263
if (this.resourcesStateHash !== resourcesStateHash) {
264
this.clearResourcesState(); // state mismatch, resources no longer valid
265
}
266
267
return this.resources;
268
}
269
270
private async computeResourcesStateHash(): Promise<number | undefined> {
271
if (this.resources.length === 0) {
272
return undefined; // no resources, no hash needed
273
}
274
275
// Resources clipboard is managed in-memory only and thus
276
// fails to invalidate when clipboard data is changing.
277
// As such, we compute the hash of the current clipboard
278
// and use that to later validate the resources clipboard.
279
280
const clipboardText = await this.readText();
281
return hash(clipboardText.substring(0, BrowserClipboardService.MAX_RESOURCE_STATE_SOURCE_LENGTH));
282
}
283
284
async hasResources(): Promise<boolean> {
285
// Guard access to navigator.clipboard with try/catch
286
// as we have seen DOMExceptions in certain browsers
287
// due to security policies.
288
try {
289
const items = await getActiveWindow().navigator.clipboard.read();
290
for (const item of items) {
291
if (item.types.includes(`web ${vscodeResourcesMime}`)) {
292
return true;
293
}
294
}
295
} catch (error) {
296
// Noop
297
}
298
299
return this.resources.length > 0;
300
}
301
302
public clearInternalState(): void {
303
this.clearResourcesState();
304
}
305
306
private clearResourcesState(): void {
307
this.resources = [];
308
this.resourcesStateHash = undefined;
309
}
310
}
311
312