Skip to content

Commit 945077a

Browse files
authored
Fix paste edit providers incorrectly overwriting data transfer values (microsoft#182169)
At present, copy providers on the ext host side return the full data transfer object passed to them (which includes any additions they make). We then use this to construct a new data transfer that is passed on to paste providers There are a few bugs with this approach: - If there are multiple copy providers, they can incorrectly end up overwriting each other. For example if the initial data transfer contains `text/plain` and there are two providers, the first of which tries writing a new `text/plain`, the second provider will overwrite this with the initial `text/plain` value - If the original data transfer contains multiple entries for a mime, these always end up being overwritten with a single value - We shouldn't waste type transferring value back to the main thread if the main thread already has them This PR tries to fix this by skipping ext host to main thread transfer of non-modified data transfer items. As part of this work, I reworked a few internal structures and introduced a new `IReadonlyVSDataTransfer` interface to make it more clear when a internal data transfer is or is not expected to be modified
1 parent ad91a63 commit 945077a

File tree

14 files changed

+163
-136
lines changed

14 files changed

+163
-136
lines changed

src/vs/base/common/dataTransfer.ts

Lines changed: 36 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,80 +8,90 @@ import { Iterable } from 'vs/base/common/iterator';
88
import { URI } from 'vs/base/common/uri';
99
import { generateUuid } from 'vs/base/common/uuid';
1010

11-
interface IDataTransferFile {
11+
export interface IDataTransferFile {
12+
readonly id: string;
1213
readonly name: string;
1314
readonly uri?: URI;
1415
data(): Promise<Uint8Array>;
1516
}
1617

1718
export interface IDataTransferItem {
18-
readonly id: string;
1919
asString(): Thenable<string>;
2020
asFile(): IDataTransferFile | undefined;
2121
value: any;
2222
}
2323

2424
export function createStringDataTransferItem(stringOrPromise: string | Promise<string>): IDataTransferItem {
2525
return {
26-
id: generateUuid(),
2726
asString: async () => stringOrPromise,
2827
asFile: () => undefined,
2928
value: typeof stringOrPromise === 'string' ? stringOrPromise : undefined,
3029
};
3130
}
3231

3332
export function createFileDataTransferItem(fileName: string, uri: URI | undefined, data: () => Promise<Uint8Array>): IDataTransferItem {
33+
const file = { id: generateUuid(), name: fileName, uri, data };
3434
return {
35-
id: generateUuid(),
3635
asString: async () => '',
37-
asFile: () => ({ name: fileName, uri, data }),
36+
asFile: () => file,
3837
value: undefined,
3938
};
4039
}
4140

42-
export class VSDataTransfer {
43-
44-
private readonly _entries = new Map<string, IDataTransferItem[]>();
45-
41+
export interface IReadonlyVSDataTransfer extends Iterable<readonly [string, IDataTransferItem]> {
4642
/**
4743
* Get the total number of entries in this data transfer.
4844
*/
49-
public get size(): number {
50-
let size = 0;
51-
this.forEach(() => size++);
52-
return size;
53-
}
45+
get size(): number;
5446

5547
/**
5648
* Check if this data transfer contains data for `mimeType`.
5749
*
5850
* This uses exact matching and does not support wildcards.
5951
*/
60-
public has(mimeType: string): boolean {
61-
return this._entries.has(this.toKey(mimeType));
62-
}
63-
52+
has(mimeType: string): boolean;
6453
/**
6554
* Check if this data transfer contains data matching `pattern`.
6655
*
6756
* This allows matching for wildcards, such as `image/*`.
6857
*
6958
* Use the special `files` mime type to match any file in the data transfer.
7059
*/
60+
matches(pattern: string): boolean;
61+
62+
/**
63+
* Retrieve the first entry for `mimeType`.
64+
*
65+
* Note that if you want to find all entries for a given mime type, use {@link IReadonlyVSDataTransfer.entries} instead.
66+
*/
67+
get(mimeType: string): IDataTransferItem | undefined;
68+
}
69+
70+
export class VSDataTransfer implements IReadonlyVSDataTransfer {
71+
72+
private readonly _entries = new Map<string, IDataTransferItem[]>();
73+
74+
public get size(): number {
75+
let size = 0;
76+
for (const _ of this._entries) {
77+
size++;
78+
}
79+
return size;
80+
}
81+
82+
public has(mimeType: string): boolean {
83+
return this._entries.has(this.toKey(mimeType));
84+
}
85+
7186
public matches(pattern: string): boolean {
7287
const mimes = [...this._entries.keys()];
73-
if (Iterable.some(this.values(), item => item.asFile())) {
88+
if (Iterable.some(this, ([_, item]) => item.asFile())) {
7489
mimes.push('files');
7590
}
7691

7792
return matchesMimeType_normalized(normalizeMimeType(pattern), mimes);
7893
}
7994

80-
/**
81-
* Retrieve the first entry for `mimeType`.
82-
*
83-
* Note that if want to find all entries for a given mime type, use {@link VSDataTransfer.entries} instead.
84-
*/
8595
public get(mimeType: string): IDataTransferItem | undefined {
8696
return this._entries.get(this.toKey(mimeType))?.[0];
8797
}
@@ -121,34 +131,14 @@ export class VSDataTransfer {
121131
*
122132
* There may be multiple entries for each mime type.
123133
*/
124-
public *entries(): Iterable<[string, IDataTransferItem]> {
125-
for (const [mine, items] of this._entries.entries()) {
134+
public *[Symbol.iterator](): IterableIterator<readonly [string, IDataTransferItem]> {
135+
for (const [mine, items] of this._entries) {
126136
for (const item of items) {
127137
yield [mine, item];
128138
}
129139
}
130140
}
131141

132-
/**
133-
* Iterate over all items in this data transfer.
134-
*
135-
* There may be multiple entries for each mime type.
136-
*/
137-
public values(): Iterable<IDataTransferItem> {
138-
return Array.from(this._entries.values()).flat();
139-
}
140-
141-
/**
142-
* Call `f` for each item and mime in the data transfer.
143-
*
144-
* There may be multiple entries for each mime type.
145-
*/
146-
public forEach(f: (value: IDataTransferItem, mime: string) => void) {
147-
for (const [mime, item] of this.entries()) {
148-
f(item, mime);
149-
}
150-
}
151-
152142
private toKey(mimeType: string): string {
153143
return normalizeMimeType(mimeType);
154144
}

src/vs/editor/common/languages.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
77
import { CancellationToken } from 'vs/base/common/cancellation';
88
import { Codicon } from 'vs/base/common/codicons';
99
import { Color } from 'vs/base/common/color';
10-
import { VSDataTransfer } from 'vs/base/common/dataTransfer';
10+
import { IReadonlyVSDataTransfer } from 'vs/base/common/dataTransfer';
1111
import { Event } from 'vs/base/common/event';
1212
import { IMarkdownString } from 'vs/base/common/htmlContent';
1313
import { IDisposable } from 'vs/base/common/lifecycle';
@@ -801,9 +801,9 @@ export interface DocumentPasteEditProvider {
801801
readonly copyMimeTypes?: readonly string[];
802802
readonly pasteMimeTypes: readonly string[];
803803

804-
prepareDocumentPaste?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<undefined | VSDataTransfer>;
804+
prepareDocumentPaste?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<undefined | IReadonlyVSDataTransfer>;
805805

806-
provideDocumentPasteEdits(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;
806+
provideDocumentPasteEdits(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;
807807
}
808808

809809
/**
@@ -1960,5 +1960,5 @@ export interface DocumentOnDropEdit {
19601960
export interface DocumentOnDropEditProvider {
19611961
readonly dropMimeTypes?: readonly string[];
19621962

1963-
provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): ProviderResult<DocumentOnDropEdit>;
1963+
provideDocumentOnDropEdits(model: model.ITextModel, position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): ProviderResult<DocumentOnDropEdit>;
19641964
}

src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,18 @@ export class CopyPasteController extends Disposable implements IEditorContributi
165165
});
166166

167167
const promise = createCancelablePromise(async token => {
168-
const results = await Promise.all(providers.map(provider => {
168+
const results = coalesce(await Promise.all(providers.map(provider => {
169169
return provider.prepareDocumentPaste!(model, ranges, dataTransfer, token);
170-
}));
170+
})));
171+
172+
// Values from higher priority providers should overwrite values from lower priority ones.
173+
// Reverse the array to so that the calls to `replace` below will do this
174+
results.reverse();
171175

172176
for (const result of results) {
173-
result?.forEach((value, key) => {
174-
dataTransfer.replace(key, value);
175-
});
177+
for (const [mime, value] of result) {
178+
dataTransfer.replace(mime, value);
179+
}
176180
}
177181

178182
return dataTransfer;
@@ -368,9 +372,9 @@ export class CopyPasteController extends Disposable implements IEditorContributi
368372
return;
369373
}
370374

371-
toMergeDataTransfer.forEach((value, key) => {
375+
for (const [key, value] of toMergeDataTransfer) {
372376
dataTransfer.replace(key, value);
373-
});
377+
}
374378
}
375379

376380
if (!dataTransfer.has(Mimes.uriList)) {

src/vs/editor/contrib/dropOrPasteInto/browser/defaultProviders.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { coalesce } from 'vs/base/common/arrays';
77
import { CancellationToken } from 'vs/base/common/cancellation';
8-
import { UriList, VSDataTransfer } from 'vs/base/common/dataTransfer';
8+
import { IReadonlyVSDataTransfer, UriList } from 'vs/base/common/dataTransfer';
99
import { Disposable } from 'vs/base/common/lifecycle';
1010
import { Mimes } from 'vs/base/common/mime';
1111
import { Schemas } from 'vs/base/common/network';
@@ -27,17 +27,17 @@ abstract class SimplePasteAndDropProvider implements DocumentOnDropEditProvider,
2727
abstract readonly dropMimeTypes: readonly string[] | undefined;
2828
abstract readonly pasteMimeTypes: readonly string[];
2929

30-
async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
30+
async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined> {
3131
const edit = await this.getEdit(dataTransfer, token);
3232
return edit ? { id: this.id, insertText: edit.insertText, label: edit.label, detail: edit.detail, priority: edit.priority } : undefined;
3333
}
3434

35-
async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentOnDropEdit | undefined> {
35+
async provideDocumentOnDropEdits(_model: ITextModel, _position: IPosition, dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentOnDropEdit | undefined> {
3636
const edit = await this.getEdit(dataTransfer, token);
3737
return edit ? { id: this.id, insertText: edit.insertText, label: edit.label, priority: edit.priority } : undefined;
3838
}
3939

40-
protected abstract getEdit(dataTransfer: VSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;
40+
protected abstract getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;
4141
}
4242

4343
class DefaultTextProvider extends SimplePasteAndDropProvider {
@@ -46,7 +46,7 @@ class DefaultTextProvider extends SimplePasteAndDropProvider {
4646
readonly dropMimeTypes = [Mimes.text];
4747
readonly pasteMimeTypes = [Mimes.text];
4848

49-
protected async getEdit(dataTransfer: VSDataTransfer, _token: CancellationToken) {
49+
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, _token: CancellationToken) {
5050
const textEntry = dataTransfer.get(Mimes.text);
5151
if (!textEntry) {
5252
return;
@@ -75,7 +75,7 @@ class PathProvider extends SimplePasteAndDropProvider {
7575
readonly dropMimeTypes = [Mimes.uriList];
7676
readonly pasteMimeTypes = [Mimes.uriList];
7777

78-
protected async getEdit(dataTransfer: VSDataTransfer, token: CancellationToken) {
78+
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken) {
7979
const entries = await extractUriList(dataTransfer);
8080
if (!entries.length || token.isCancellationRequested) {
8181
return;
@@ -128,7 +128,7 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
128128
super();
129129
}
130130

131-
protected async getEdit(dataTransfer: VSDataTransfer, token: CancellationToken) {
131+
protected async getEdit(dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken) {
132132
const entries = await extractUriList(dataTransfer);
133133
if (!entries.length || token.isCancellationRequested) {
134134
return;
@@ -155,7 +155,7 @@ class RelativePathProvider extends SimplePasteAndDropProvider {
155155
}
156156
}
157157

158-
async function extractUriList(dataTransfer: VSDataTransfer): Promise<{ readonly uri: URI; readonly originalText: string }[]> {
158+
async function extractUriList(dataTransfer: IReadonlyVSDataTransfer): Promise<{ readonly uri: URI; readonly originalText: string }[]> {
159159
const urlListEntry = dataTransfer.get(Mimes.uriList);
160160
if (!urlListEntry) {
161161
return [];

src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr
145145
for (const id of data) {
146146
const treeDataTransfer = await this._treeViewsDragAndDropService.removeDragOperationTransfer(id.identifier);
147147
if (treeDataTransfer) {
148-
for (const [type, value] of treeDataTransfer.entries()) {
148+
for (const [type, value] of treeDataTransfer) {
149149
dataTransfer.replace(type, value);
150150
}
151151
}

0 commit comments

Comments
 (0)