Skip to content

Commit 7806751

Browse files
committed
Initial sketches for copy/paste action provider
For #30066
1 parent 5a091f6 commit 7806751

File tree

13 files changed

+370
-2
lines changed

13 files changed

+370
-2
lines changed

extensions/markdown-language-features/src/extension.ts

+33
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { githubSlugifier } from './slugify';
2020
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
2121

2222

23+
type ClipboardData = { count: number };
24+
2325
export function activate(context: vscode.ExtensionContext) {
2426
const telemetryReporter = loadDefaultTelemetryReporter();
2527
context.subscriptions.push(telemetryReporter);
@@ -43,6 +45,37 @@ export function activate(context: vscode.ExtensionContext) {
4345
logger.updateConfiguration();
4446
previewManager.updateConfiguration();
4547
}));
48+
49+
// Example copy paste provider that includes the number of times
50+
// you've copied something in the pasted text.
51+
52+
let copyCount = 0;
53+
54+
vscode.languages.registerCopyPasteActionProvider({ language: 'markdown', }, new class implements vscode.CopyPasteActionProvider<ClipboardData> {
55+
56+
async onDidCopy(
57+
_document: vscode.TextDocument,
58+
_selection: vscode.Selection,
59+
_clipboard: { readonly text: string },
60+
): Promise<ClipboardData | undefined> {
61+
return { count: copyCount++ };
62+
}
63+
64+
async onWillPaste(
65+
document: vscode.TextDocument,
66+
selection: vscode.Selection,
67+
clipboard: { readonly text: string; readonly data?: ClipboardData; }
68+
): Promise<vscode.WorkspaceEdit | undefined> {
69+
const edit = new vscode.WorkspaceEdit();
70+
71+
const newText = `(copy #${clipboard.data?.count}) ${clipboard.text}`;
72+
edit.replace(document.uri, selection, newText);
73+
74+
return edit;
75+
}
76+
}, {
77+
kind: vscode.CodeActionKind.Empty
78+
});
4679
}
4780

4881
function registerMarkdownLanguageFeatures(

src/vs/editor/browser/widget/codeEditorWidget.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1850,6 +1850,7 @@ export class EditorModeContext extends Disposable {
18501850
// update when registries change
18511851
this._register(modes.CompletionProviderRegistry.onDidChange(update));
18521852
this._register(modes.CodeActionProviderRegistry.onDidChange(update));
1853+
this._register(modes.CopyPasteActionProviderRegistry.onDidChange(update));
18531854
this._register(modes.CodeLensProviderRegistry.onDidChange(update));
18541855
this._register(modes.DefinitionProviderRegistry.onDidChange(update));
18551856
this._register(modes.DeclarationProviderRegistry.onDidChange(update));

src/vs/editor/common/modes.ts

+25
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,26 @@ export interface CodeActionProvider {
672672
_getAdditionalMenuItems?(context: CodeActionContext, actions: readonly CodeAction[]): Command[];
673673
}
674674

675+
export interface CopyPasteActionProvider {
676+
id: string;
677+
678+
onDidCopy?(
679+
model: model.ITextModel,
680+
selection: Selection,
681+
clipboard: { readonly text: string },
682+
token: CancellationToken,
683+
): Promise<unknown | undefined>;
684+
685+
onWillPaste(
686+
model: model.ITextModel,
687+
selection: Selection,
688+
clipboard: {
689+
readonly text: string;
690+
readonly data?: unknown;
691+
},
692+
): Promise<WorkspaceEdit | undefined>;
693+
}
694+
675695
/**
676696
* Represents a parameter of a callable-signature. A parameter can
677697
* have a label and a doc-comment.
@@ -1732,6 +1752,11 @@ export const CodeLensProviderRegistry = new LanguageFeatureRegistry<CodeLensProv
17321752
*/
17331753
export const CodeActionProviderRegistry = new LanguageFeatureRegistry<CodeActionProvider>();
17341754

1755+
/**
1756+
* @internal
1757+
*/
1758+
export const CopyPasteActionProviderRegistry = new LanguageFeatureRegistry<CopyPasteActionProvider>();
1759+
17351760
/**
17361761
* @internal
17371762
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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 { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
7+
import { CopyPasteActionController } from 'vs/editor/contrib/copyPasteAction/copyPasteActionController';
8+
9+
10+
registerEditorContribution(CopyPasteActionController.ID, CopyPasteActionController);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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 { addDisposableListener } from 'vs/base/browser/dom';
7+
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
8+
import { Disposable } from 'vs/base/common/lifecycle';
9+
import { generateUuid } from 'vs/base/common/uuid';
10+
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
11+
import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService';
12+
import { IEditorContribution } from 'vs/editor/common/editorCommon';
13+
import { CopyPasteActionProvider, CopyPasteActionProviderRegistry } from 'vs/editor/common/modes';
14+
15+
let clipboardItem: undefined | {
16+
readonly handle: string;
17+
readonly results: CancelablePromise<Map<CopyPasteActionProvider, unknown | undefined>>;
18+
};
19+
20+
const vscodeClipboardFormat = 'x-vscode/id';
21+
22+
export class CopyPasteActionController extends Disposable implements IEditorContribution {
23+
24+
public static readonly ID = 'editor.contrib.copyPasteActionController';
25+
26+
public static get(editor: ICodeEditor): CopyPasteActionController {
27+
return editor.getContribution<CopyPasteActionController>(CopyPasteActionController.ID);
28+
}
29+
30+
private readonly _editor: ICodeEditor;
31+
32+
constructor(
33+
editor: ICodeEditor,
34+
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
35+
) {
36+
super();
37+
38+
this._editor = editor;
39+
40+
this._register(addDisposableListener(document, 'copy', (e: ClipboardEvent) => {
41+
if (!e.clipboardData) {
42+
return;
43+
}
44+
45+
const model = editor.getModel();
46+
const selection = this._editor.getSelection();
47+
if (!model || !selection) {
48+
return;
49+
}
50+
51+
const providers = CopyPasteActionProviderRegistry.all(model).filter(x => !!x.onDidCopy);
52+
if (!providers.length) {
53+
return;
54+
}
55+
56+
// Call prevent default to prevent our new clipboard data from being overwritten (is this really required?)
57+
e.preventDefault();
58+
59+
// And then fill in raw text again since we prevented default
60+
const clipboardText = model.getValueInRange(selection);
61+
e.clipboardData.setData('text/plain', clipboardText);
62+
63+
// Save off a handle pointing to data that VS Code maintains.
64+
const handle = generateUuid();
65+
e.clipboardData.setData(vscodeClipboardFormat, handle);
66+
67+
const promise = createCancelablePromise(async token => {
68+
const results = await Promise.all(providers.map(async provider => {
69+
const result = await provider.onDidCopy!(model, selection, { text: clipboardText }, token);
70+
return { provider, result };
71+
}));
72+
73+
const map = new Map<CopyPasteActionProvider, unknown | undefined>();
74+
for (const { provider, result } of results) {
75+
map.set(provider, result);
76+
}
77+
78+
return map;
79+
});
80+
81+
clipboardItem = { handle: handle, results: promise };
82+
}));
83+
84+
this._register(addDisposableListener(document, 'paste', async (e: ClipboardEvent) => {
85+
const model = editor.getModel();
86+
const selection = this._editor.getSelection();
87+
if (!model || !selection) {
88+
return;
89+
}
90+
91+
const providers = CopyPasteActionProviderRegistry.all(model).filter(x => !!x.onDidCopy);
92+
if (!providers.length) {
93+
return;
94+
}
95+
96+
const handle = e.clipboardData?.getData(vscodeClipboardFormat);
97+
const clipboardText = e.clipboardData?.getData('text/plain') ?? '';
98+
99+
e.preventDefault();
100+
e.stopImmediatePropagation();
101+
102+
let results: Map<CopyPasteActionProvider, unknown | undefined> | undefined;
103+
if (handle && clipboardItem && clipboardItem?.handle === handle) {
104+
results = await clipboardItem.results;
105+
}
106+
107+
for (const provider of providers) {
108+
const data = results?.get(provider);
109+
const edit = await provider.onWillPaste(model, selection, { text: clipboardText, data });
110+
if (!edit) {
111+
continue;
112+
}
113+
114+
await this._bulkEditService.apply(ResourceEdit.convert(edit), { editor });
115+
}
116+
}, true));
117+
}
118+
}

src/vs/editor/editor.all.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'vs/editor/contrib/clipboard/clipboard';
1515
import 'vs/editor/contrib/codeAction/codeActionContributions';
1616
import 'vs/editor/contrib/codelens/codelensController';
1717
import 'vs/editor/contrib/colorPicker/colorDetector';
18+
import 'vs/editor/contrib/copyPasteAction/copyPasteActionContribution';
1819
import 'vs/editor/contrib/comment/comment';
1920
import 'vs/editor/contrib/contextmenu/contextmenu';
2021
import 'vs/editor/contrib/cursorUndo/cursorUndo';

src/vs/monaco.d.ts

+11
Original file line numberDiff line numberDiff line change
@@ -5655,6 +5655,17 @@ declare namespace monaco.languages {
56555655
readonly actions: ReadonlyArray<CodeAction>;
56565656
}
56575657

5658+
export interface CopyPasteActionProvider {
5659+
id: string;
5660+
onDidCopy?(model: editor.ITextModel, selection: Selection, clipboard: {
5661+
readonly text: string;
5662+
}, token: CancellationToken): Promise<unknown | undefined>;
5663+
onWillPaste(model: editor.ITextModel, selection: Selection, clipboard: {
5664+
readonly text: string;
5665+
readonly data?: unknown;
5666+
}): Promise<WorkspaceEdit | undefined>;
5667+
}
5668+
56585669
/**
56595670
* Represents a parameter of a callable-signature. A parameter can
56605671
* have a label and a doc-comment.

src/vs/vscode.proposed.d.ts

+82
Original file line numberDiff line numberDiff line change
@@ -2171,4 +2171,86 @@ declare module 'vscode' {
21712171
with(color: ThemeColor): ThemeIcon2;
21722172
}
21732173
//#endregion
2174+
2175+
//#region https://github.com/microsoft/vscode/issues/30066
2176+
2177+
/**
2178+
* TODOs:
2179+
* - Multiple providers?
2180+
* - Is the document already edited in onWillPaste?
2181+
* - Does `onWillPaste` need to re-implement basic paste
2182+
*
2183+
* - Figure out CopyPasteActionProviderMetadata
2184+
*/
2185+
2186+
/**
2187+
* Provider invoked when the user copies and pastes code.
2188+
*
2189+
* This gives extensions a chance to hook into pasting and change the text
2190+
* that is pasted.
2191+
*/
2192+
interface CopyPasteActionProvider<T = unknown> {
2193+
2194+
/**
2195+
* Optional method invoked after the user copies text in a file.
2196+
*
2197+
* During `onDidCopy`, an extension can compute metadata that is attached to
2198+
* the clipboard and is passed back to the provider in `onWillPaste`.
2199+
*
2200+
* @param document Document where the copy took place.
2201+
* @param selection Selection being copied in the `document`.
2202+
* @param clipboard Information about the clipboard state after the copy.
2203+
*
2204+
* @return Optional metadata passed to `onWillPaste`.
2205+
*/
2206+
onDidCopy?(
2207+
document: TextDocument,
2208+
selection: Selection,
2209+
clipboard: { readonly text: string },
2210+
): ProviderResult<T>;
2211+
2212+
/**
2213+
* Invoked before the user pastes into a document.
2214+
*
2215+
* In this method, extensions can return a workspace edit that replaces the standard pasting behavior.
2216+
*
2217+
* @param document Document being pasted into
2218+
* @param selection Current selection in the document.
2219+
* @param clipboard Information about the clipboard state. This may contain the metadata from `onDidCopy`.
2220+
*
2221+
* @return Optional workspace edit that applies the paste. Return undefined to use standard pasting
2222+
*/
2223+
onWillPaste(
2224+
document: TextDocument,
2225+
selection: Selection,
2226+
clipboard: {
2227+
readonly text: string;
2228+
readonly data?: T;
2229+
},
2230+
): ProviderResult<WorkspaceEdit>;
2231+
}
2232+
2233+
/**
2234+
*
2235+
*/
2236+
interface CopyPasteActionProviderMetadata {
2237+
/**
2238+
* Identifies the type of code action
2239+
*/
2240+
readonly kind: CodeActionKind;
2241+
}
2242+
2243+
namespace languages {
2244+
/**
2245+
*
2246+
*/
2247+
export function registerCopyPasteActionProvider(
2248+
selector: DocumentSelector,
2249+
provider: CopyPasteActionProvider,
2250+
metadata: CopyPasteActionProviderMetadata
2251+
): Disposable;
2252+
}
2253+
2254+
//#endregion
2255+
21742256
}

src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts

+20
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,26 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
324324
this._registrations.set(handle, modes.CodeActionProviderRegistry.register(selector, provider));
325325
}
326326

327+
// --- copy paste action provider
328+
329+
$registerCopyPasteActionProvider(handle: number, selector: IDocumentFilterDto[], id: string, supportsCopy: boolean): void {
330+
const provider: modes.CopyPasteActionProvider = {
331+
id,
332+
onDidCopy: supportsCopy
333+
? (model: ITextModel, selection: Selection, clipboard: { readonly text: string }): Promise<string | undefined> => {
334+
return this._proxy.$onDidCopy(handle, model.uri, selection, clipboard);
335+
}
336+
: undefined,
337+
338+
onWillPaste: async (model: ITextModel, selection: Selection, clipboard: { text: string, data?: string }) => {
339+
const result = await this._proxy.$onWillPaste(handle, model.uri, selection, { text: clipboard.text, handle: clipboard.data });
340+
return result && reviveWorkspaceEditDto(result);
341+
}
342+
};
343+
344+
this._registrations.set(handle, modes.CopyPasteActionProviderRegistry.register(selector, provider));
345+
}
346+
327347
// --- formatting
328348

329349
$registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void {

src/vs/workbench/api/common/extHost.api.impl.ts

+3
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
361361
registerCodeActionsProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable {
362362
return extHostLanguageFeatures.registerCodeActionProvider(extension, checkSelector(selector), provider, metadata);
363363
},
364+
registerCopyPasteActionProvider(selector: vscode.DocumentSelector, provider: vscode.CopyPasteActionProvider, metadata: vscode.CopyPasteActionProviderMetadata): vscode.Disposable {
365+
return extHostLanguageFeatures.registerCopyPasteActionProvider(extension, checkSelector(selector), provider, metadata);
366+
},
364367
registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
365368
return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
366369
},

src/vs/workbench/api/common/extHost.protocol.ts

+3
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable {
381381
$registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], stopPattern: IRegExpDto | undefined): void;
382382
$registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void;
383383
$registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void;
384+
$registerCopyPasteActionProvider(handle: number, selector: IDocumentFilterDto[], id: string, supportsCopy: boolean): void;
384385
$registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void;
385386
$registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void;
386387
$registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void;
@@ -1398,6 +1399,8 @@ export interface ExtHostLanguageFeaturesShape {
13981399
$provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise<ICodeActionListDto | undefined>;
13991400
$resolveCodeAction(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<IWorkspaceEditDto | undefined>;
14001401
$releaseCodeActions(handle: number, cacheId: number): void;
1402+
$onDidCopy(handle: number, uri: UriComponents, selection: ISelection, clipboard: { readonly text: string; }): Promise<string | undefined>;
1403+
$onWillPaste(handle: number, uri: UriComponents, selection: ISelection, clipboard: { text: string; handle?: string | undefined; }): Promise<IWorkspaceEditDto | undefined>;
14011404
$provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;
14021405
$provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;
14031406
$provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;

0 commit comments

Comments
 (0)