mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-20 02:08:47 +00:00
Fix range when copying empty selection (#182227)
This fixes the range extensions get when copying an empty selection. As part of this, I've also: - Added tests for this change - Made the paste parts of the api optional. This is useful when a test provider only wants to add data on copy
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
"customEditorMove",
|
||||
"diffCommand",
|
||||
"documentFiltersExclusive",
|
||||
"documentPaste",
|
||||
"editorInsets",
|
||||
"extensionRuntime",
|
||||
"extensionsAny",
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
import { assertNoRpc, createRandomFile, usingDisposables } from '../utils';
|
||||
|
||||
const textPlain = 'text/plain';
|
||||
|
||||
(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('vscode API - Copy Paste', () => {
|
||||
|
||||
teardown(assertNoRpc);
|
||||
|
||||
test('Copy should be able to overwrite text/plain', usingDisposables(async (disposables) => {
|
||||
const file = await createRandomFile('$abcde@');
|
||||
const doc = await vscode.workspace.openTextDocument(file);
|
||||
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
editor.selections = [new vscode.Selection(0, 1, 0, 6)];
|
||||
|
||||
disposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider {
|
||||
async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise<void> {
|
||||
const existing = dataTransfer.get(textPlain);
|
||||
if (existing) {
|
||||
const str = await existing.asString();
|
||||
const reversed = reverseString(str);
|
||||
dataTransfer.set(textPlain, new vscode.DataTransferItem(reversed));
|
||||
}
|
||||
}
|
||||
}, { copyMimeTypes: [textPlain] }));
|
||||
|
||||
await vscode.commands.executeCommand('editor.action.clipboardCopyAction');
|
||||
const newDocContent = getNextDocumentText(disposables, doc);
|
||||
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
|
||||
assert.strictEqual(await newDocContent, '$edcba@');
|
||||
}));
|
||||
|
||||
test('Copy with empty selection should copy entire line', usingDisposables(async (disposables) => {
|
||||
const file = await createRandomFile('abc\ndef');
|
||||
const doc = await vscode.workspace.openTextDocument(file);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
|
||||
disposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider {
|
||||
async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise<void> {
|
||||
const existing = dataTransfer.get(textPlain);
|
||||
if (existing) {
|
||||
const str = await existing.asString();
|
||||
// text/plain includes the trailing new line in this case
|
||||
// On windows, this will always be `\r\n` even if the document uses `\n`
|
||||
const eol = str.match(/\r?\n$/);
|
||||
const reversed = reverseString(str.slice(0, -eol![0].length));
|
||||
dataTransfer.set(textPlain, new vscode.DataTransferItem(reversed + '\n'));
|
||||
}
|
||||
}
|
||||
}, { copyMimeTypes: [textPlain] }));
|
||||
|
||||
await vscode.commands.executeCommand('editor.action.clipboardCopyAction');
|
||||
const newDocContent = getNextDocumentText(disposables, doc);
|
||||
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
|
||||
assert.strictEqual(await newDocContent, `cba\nabc\ndef`);
|
||||
}));
|
||||
|
||||
test('Copy with multiple selections should get all selections', usingDisposables(async (disposables) => {
|
||||
const file = await createRandomFile('111\n222\n333');
|
||||
const doc = await vscode.workspace.openTextDocument(file);
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
|
||||
editor.selections = [
|
||||
new vscode.Selection(0, 0, 0, 3),
|
||||
new vscode.Selection(2, 0, 2, 3),
|
||||
];
|
||||
|
||||
disposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider {
|
||||
async prepareDocumentPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise<void> {
|
||||
const existing = dataTransfer.get(textPlain);
|
||||
if (existing) {
|
||||
const selections = ranges.map(range => document.getText(range));
|
||||
dataTransfer.set(textPlain, new vscode.DataTransferItem(`(${ranges.length})${selections.join(' ')}`));
|
||||
}
|
||||
}
|
||||
}, { copyMimeTypes: [textPlain] }));
|
||||
|
||||
await vscode.commands.executeCommand('editor.action.clipboardCopyAction');
|
||||
editor.selections = [new vscode.Selection(0, 0, 0, 0)];
|
||||
const newDocContent = getNextDocumentText(disposables, doc);
|
||||
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
|
||||
|
||||
assert.strictEqual(await newDocContent, `(2)111 333111\n222\n333`);
|
||||
}));
|
||||
|
||||
test('Earlier invoked copy providers should win when writing values', usingDisposables(async (disposables) => {
|
||||
const file = await createRandomFile('abc\ndef');
|
||||
const doc = await vscode.workspace.openTextDocument(file);
|
||||
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
editor.selections = [new vscode.Selection(0, 0, 0, 3)];
|
||||
|
||||
const callOrder: string[] = [];
|
||||
const a_id = 'a';
|
||||
const b_id = 'b';
|
||||
|
||||
let providerAResolve: () => void;
|
||||
const providerAFinished = new Promise<void>(resolve => providerAResolve = resolve);
|
||||
|
||||
disposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider {
|
||||
async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise<void> {
|
||||
callOrder.push(a_id);
|
||||
dataTransfer.set(textPlain, new vscode.DataTransferItem('a'));
|
||||
providerAResolve();
|
||||
}
|
||||
}, { copyMimeTypes: [textPlain] }));
|
||||
|
||||
// Later registered providers will be called first
|
||||
disposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider {
|
||||
async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise<void> {
|
||||
callOrder.push(b_id);
|
||||
|
||||
// Wait for the first provider to finish even though we were called first.
|
||||
// This tests that resulting order does not depend on the order the providers
|
||||
// return in.
|
||||
await providerAFinished;
|
||||
|
||||
dataTransfer.set(textPlain, new vscode.DataTransferItem('b'));
|
||||
}
|
||||
}, { copyMimeTypes: [textPlain] }));
|
||||
|
||||
await vscode.commands.executeCommand('editor.action.clipboardCopyAction');
|
||||
const newDocContent = getNextDocumentText(disposables, doc);
|
||||
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
|
||||
assert.strictEqual(await newDocContent, 'b\ndef');
|
||||
|
||||
// Confirm provider call order is what we expected
|
||||
assert.deepStrictEqual(callOrder, [b_id, a_id]);
|
||||
}));
|
||||
|
||||
test('Copy providers should not be able to effect the data transfer of another', usingDisposables(async (disposables) => {
|
||||
const file = await createRandomFile('abc\ndef');
|
||||
const doc = await vscode.workspace.openTextDocument(file);
|
||||
|
||||
const editor = await vscode.window.showTextDocument(doc);
|
||||
editor.selections = [new vscode.Selection(0, 0, 0, 3)];
|
||||
|
||||
|
||||
let providerAResolve: () => void;
|
||||
const providerAFinished = new Promise<void>(resolve => providerAResolve = resolve);
|
||||
|
||||
disposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider {
|
||||
async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise<void> {
|
||||
dataTransfer.set(textPlain, new vscode.DataTransferItem('xyz'));
|
||||
providerAResolve();
|
||||
}
|
||||
}, { copyMimeTypes: [textPlain] }));
|
||||
|
||||
disposables.push(vscode.languages.registerDocumentPasteEditProvider({ language: 'plaintext' }, new class implements vscode.DocumentPasteEditProvider {
|
||||
async prepareDocumentPaste(_document: vscode.TextDocument, _ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise<void> {
|
||||
|
||||
// Wait for the first provider to finish
|
||||
await providerAFinished;
|
||||
|
||||
// We we access the data transfer here, we should not see changes made by the first provider
|
||||
const entry = dataTransfer.get(textPlain);
|
||||
const str = await entry!.asString();
|
||||
dataTransfer.set(textPlain, new vscode.DataTransferItem(reverseString(str)));
|
||||
}
|
||||
}, { copyMimeTypes: [textPlain] }));
|
||||
|
||||
await vscode.commands.executeCommand('editor.action.clipboardCopyAction');
|
||||
const newDocContent = getNextDocumentText(disposables, doc);
|
||||
await vscode.commands.executeCommand('editor.action.clipboardPasteAction');
|
||||
assert.strictEqual(await newDocContent, 'cba\ndef');
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
function reverseString(str: string) {
|
||||
return str.split("").reverse().join("");
|
||||
}
|
||||
|
||||
function getNextDocumentText(disposables: vscode.Disposable[], doc: vscode.TextDocument): Promise<string> {
|
||||
return new Promise<string>(resolve => {
|
||||
disposables.push(vscode.workspace.onDidChangeTextDocument(e => {
|
||||
if (e.document === doc) {
|
||||
resolve(doc.getText());
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,6 +62,17 @@ export function disposeAll(disposables: vscode.Disposable[]) {
|
||||
vscode.Disposable.from(...disposables).dispose();
|
||||
}
|
||||
|
||||
export function usingDisposables<R>(fn: (this: Mocha.Context, store: vscode.Disposable[]) => Promise<R>) {
|
||||
return async function (this: Mocha.Context): Promise<R> {
|
||||
const disposables: vscode.Disposable[] = [];
|
||||
try {
|
||||
return await fn.call(this, disposables);
|
||||
} finally {
|
||||
disposeAll(disposables);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function delay(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -799,11 +799,11 @@ export interface DocumentPasteEditProvider {
|
||||
readonly id?: string;
|
||||
|
||||
readonly copyMimeTypes?: readonly string[];
|
||||
readonly pasteMimeTypes: readonly string[];
|
||||
readonly pasteMimeTypes?: readonly string[];
|
||||
|
||||
prepareDocumentPaste?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<undefined | IReadonlyVSDataTransfer>;
|
||||
|
||||
provideDocumentPasteEdits(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;
|
||||
provideDocumentPasteEdits?(model: model.ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise<DocumentPasteEdit | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -133,7 +133,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
return;
|
||||
}
|
||||
|
||||
ranges = ranges.map(range => new Range(range.startLineNumber, 0, range.startLineNumber, model.getLineLength(range.startLineNumber)));
|
||||
ranges = [new Range(ranges[0].startLineNumber, 1, ranges[0].startLineNumber, 1 + model.getLineLength(ranges[0].startLineNumber))];
|
||||
}
|
||||
|
||||
const toCopy = this._editor._getViewModel()?.getPlainTextToCopy(selections, enableEmptySelectionClipboard, platform.isWindows);
|
||||
@@ -219,7 +219,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
|
||||
const allProviders = this._languageFeaturesService.documentPasteEditProvider
|
||||
.ordered(model)
|
||||
.filter(provider => provider.pasteMimeTypes.some(type => matchesMimeType(type, allPotentialMimeTypes)));
|
||||
.filter(provider => provider.pasteMimeTypes?.some(type => matchesMimeType(type, allPotentialMimeTypes)));
|
||||
if (!allProviders.length) {
|
||||
return;
|
||||
}
|
||||
@@ -253,7 +253,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
}
|
||||
|
||||
// Filter out any providers the don't match the full data transfer we will send them.
|
||||
const supportedProviders = allProviders.filter(provider => isSupportedProvider(provider, dataTransfer));
|
||||
const supportedProviders = allProviders.filter(provider => isSupportedPasteProvider(provider, dataTransfer));
|
||||
if (!supportedProviders.length
|
||||
|| (supportedProviders.length === 1 && supportedProviders[0].id === 'text') // Only our default text provider is active
|
||||
) {
|
||||
@@ -300,7 +300,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
}
|
||||
|
||||
// Filter out any providers the don't match the full data transfer we will send them.
|
||||
const supportedProviders = allProviders.filter(provider => isSupportedProvider(provider, dataTransfer));
|
||||
const supportedProviders = allProviders.filter(provider => isSupportedPasteProvider(provider, dataTransfer));
|
||||
|
||||
const providerEdits = await this.getPasteEdits(supportedProviders, dataTransfer, model, selections, tokenSource.token);
|
||||
if (tokenSource.token.isCancellationRequested) {
|
||||
@@ -392,7 +392,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
private async getPasteEdits(providers: readonly DocumentPasteEditProvider[], dataTransfer: VSDataTransfer, model: ITextModel, selections: readonly Selection[], token: CancellationToken): Promise<DocumentPasteEdit[]> {
|
||||
const result = await raceCancellation(
|
||||
Promise.all(
|
||||
providers.map(provider => provider.provideDocumentPasteEdits(model, selections, dataTransfer, token))
|
||||
providers.map(provider => provider.provideDocumentPasteEdits?.(model, selections, dataTransfer, token))
|
||||
).then(coalesce),
|
||||
token);
|
||||
result?.sort((a, b) => b.priority - a.priority);
|
||||
@@ -420,6 +420,6 @@ export class CopyPasteController extends Disposable implements IEditorContributi
|
||||
}
|
||||
}
|
||||
|
||||
function isSupportedProvider(provider: DocumentPasteEditProvider, dataTransfer: VSDataTransfer): boolean {
|
||||
return provider.pasteMimeTypes.some(type => dataTransfer.matches(type));
|
||||
function isSupportedPasteProvider(provider: DocumentPasteEditProvider, dataTransfer: VSDataTransfer): boolean {
|
||||
return Boolean(provider.pasteMimeTypes?.some(type => dataTransfer.matches(type)));
|
||||
}
|
||||
|
||||
@@ -930,9 +930,10 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider
|
||||
private readonly dataTransfers = new DataTransferFileCache();
|
||||
|
||||
public readonly copyMimeTypes?: readonly string[];
|
||||
public readonly pasteMimeTypes: readonly string[];
|
||||
public readonly pasteMimeTypes?: readonly string[];
|
||||
|
||||
readonly prepareDocumentPaste?: languages.DocumentPasteEditProvider['prepareDocumentPaste'];
|
||||
readonly provideDocumentPasteEdits?: languages.DocumentPasteEditProvider['provideDocumentPasteEdits'];
|
||||
|
||||
constructor(
|
||||
private readonly handle: number,
|
||||
@@ -962,27 +963,29 @@ class MainThreadPasteEditProvider implements languages.DocumentPasteEditProvider
|
||||
return dataTransferOut;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async provideDocumentPasteEdits(model: ITextModel, selections: Selection[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken) {
|
||||
const request = this.dataTransfers.add(dataTransfer);
|
||||
try {
|
||||
const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer);
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
if (metadata.supportsPaste) {
|
||||
this.provideDocumentPasteEdits = async (model: ITextModel, selections: Selection[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken) => {
|
||||
const request = this.dataTransfers.add(dataTransfer);
|
||||
try {
|
||||
const dataTransferDto = await typeConvert.DataTransfer.from(dataTransfer);
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this._proxy.$providePasteEdits(this.handle, request.id, model.uri, selections, dataTransferDto, token);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const result = await this._proxy.$providePasteEdits(this.handle, request.id, model.uri, selections, dataTransferDto, token);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
additionalEdit: result.additionalEdit ? reviveWorkspaceEditDto(result.additionalEdit, this._uriIdentService, dataId => this.resolveFileData(request.id, dataId)) : undefined,
|
||||
return {
|
||||
...result,
|
||||
additionalEdit: result.additionalEdit ? reviveWorkspaceEditDto(result.additionalEdit, this._uriIdentService, dataId => this.resolveFileData(request.id, dataId)) : undefined,
|
||||
};
|
||||
} finally {
|
||||
request.dispose();
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
request.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1830,8 +1830,9 @@ export type ITypeHierarchyItemDto = Dto<TypeHierarchyItem>;
|
||||
|
||||
export interface IPasteEditProviderMetadataDto {
|
||||
readonly supportsCopy: boolean;
|
||||
readonly supportsPaste: boolean;
|
||||
readonly copyMimeTypes?: readonly string[];
|
||||
readonly pasteMimeTypes: readonly string[];
|
||||
readonly pasteMimeTypes?: readonly string[];
|
||||
}
|
||||
|
||||
export interface IPasteEditDto {
|
||||
|
||||
@@ -530,6 +530,10 @@ class DocumentPasteEditProvider {
|
||||
}
|
||||
|
||||
async providePasteEdits(requestId: number, resource: URI, ranges: IRange[], dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise<undefined | extHostProtocol.IPasteEditDto> {
|
||||
if (!this._provider.provideDocumentPasteEdits) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = this._documents.getDocument(resource);
|
||||
const vscodeRanges = ranges.map(range => typeConvert.Range.to(range));
|
||||
|
||||
@@ -2420,6 +2424,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
|
||||
this._adapter.set(handle, new AdapterData(new DocumentPasteEditProvider(this._proxy, this._documents, provider, handle, extension), extension));
|
||||
this._proxy.$registerPasteEditProvider(handle, this._transformDocumentSelector(selector, extension), {
|
||||
supportsCopy: !!provider.prepareDocumentPaste,
|
||||
supportsPaste: !!provider.provideDocumentPasteEdits,
|
||||
copyMimeTypes: metadata.copyMimeTypes,
|
||||
pasteMimeTypes: metadata.pasteMimeTypes,
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ declare module 'vscode' {
|
||||
*
|
||||
* @return Optional workspace edit that applies the paste. Return undefined to use standard pasting.
|
||||
*/
|
||||
provideDocumentPasteEdits(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, token: CancellationToken): ProviderResult<DocumentPasteEdit>;
|
||||
provideDocumentPasteEdits?(document: TextDocument, ranges: readonly Range[], dataTransfer: DataTransfer, token: CancellationToken): ProviderResult<DocumentPasteEdit>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +98,7 @@ declare module 'vscode' {
|
||||
* Note that {@link DataTransferFile} entries are only created when dropping content from outside the editor, such as
|
||||
* from the operating system.
|
||||
*/
|
||||
readonly pasteMimeTypes: readonly string[];
|
||||
readonly pasteMimeTypes?: readonly string[];
|
||||
}
|
||||
|
||||
namespace languages {
|
||||
|
||||
Reference in New Issue
Block a user