Skip to content

Commit

Permalink
Initial sketches for copy/paste action provider
Browse files Browse the repository at this point in the history
For #30066
  • Loading branch information
mjbvz committed Sep 23, 2020
1 parent 5a091f6 commit 9401beb
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 2 deletions.
33 changes: 33 additions & 0 deletions extensions/markdown-language-features/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { githubSlugifier } from './slugify';
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';


type ClipboardData = { count: number };

export function activate(context: vscode.ExtensionContext) {
const telemetryReporter = loadDefaultTelemetryReporter();
context.subscriptions.push(telemetryReporter);
Expand All @@ -43,6 +45,37 @@ export function activate(context: vscode.ExtensionContext) {
logger.updateConfiguration();
previewManager.updateConfiguration();
}));

// Example copy paste provider that includes the number of times
// you've copied something in the pasted text.

let copyCount = 0;

vscode.languages.registerCopyPasteActionProvider({ language: 'markdown', }, new class implements vscode.CopyPasteActionProvider<ClipboardData> {

async onDidCopy(
_document: vscode.TextDocument,
_selection: vscode.Selection,
_clipboard: { readonly text: string },
): Promise<ClipboardData | undefined> {
return { count: copyCount++ };
}

async onWillPaste(
document: vscode.TextDocument,
selection: vscode.Selection,
clipboard: { readonly text: string; readonly data?: ClipboardData; }
): Promise<vscode.WorkspaceEdit | undefined> {
const edit = new vscode.WorkspaceEdit();

const newText = `(copy #${clipboard.data?.count}) ${clipboard.text}`;
edit.replace(document.uri, selection, newText);

return edit;
}
}, {
kind: vscode.CodeActionKind.Empty
});
}

function registerMarkdownLanguageFeatures(
Expand Down
1 change: 1 addition & 0 deletions src/vs/editor/browser/widget/codeEditorWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1850,6 +1850,7 @@ export class EditorModeContext extends Disposable {
// update when registries change
this._register(modes.CompletionProviderRegistry.onDidChange(update));
this._register(modes.CodeActionProviderRegistry.onDidChange(update));
this._register(modes.CopyPasteActionProviderRegistry.onDidChange(update));
this._register(modes.CodeLensProviderRegistry.onDidChange(update));
this._register(modes.DefinitionProviderRegistry.onDidChange(update));
this._register(modes.DeclarationProviderRegistry.onDidChange(update));
Expand Down
25 changes: 25 additions & 0 deletions src/vs/editor/common/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,26 @@ export interface CodeActionProvider {
_getAdditionalMenuItems?(context: CodeActionContext, actions: readonly CodeAction[]): Command[];
}

export interface CopyPasteActionProvider {
id: string;

onDidCopy?(
model: model.ITextModel,
selection: Selection,
clipboard: { readonly text: string },
token: CancellationToken,
): Promise<unknown | undefined>;

onWillPaste(
model: model.ITextModel,
selection: Selection,
clipboard: {
readonly text: string;
readonly data?: unknown;
},
): Promise<WorkspaceEdit | undefined>;
}

/**
* Represents a parameter of a callable-signature. A parameter can
* have a label and a doc-comment.
Expand Down Expand Up @@ -1732,6 +1752,11 @@ export const CodeLensProviderRegistry = new LanguageFeatureRegistry<CodeLensProv
*/
export const CodeActionProviderRegistry = new LanguageFeatureRegistry<CodeActionProvider>();

/**
* @internal
*/
export const CopyPasteActionProviderRegistry = new LanguageFeatureRegistry<CopyPasteActionProvider>();

/**
* @internal
*/
Expand Down
88 changes: 87 additions & 1 deletion src/vs/editor/contrib/codeAction/codeActionCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@
*--------------------------------------------------------------------------------------------*/

import { IAnchor } from 'vs/base/browser/ui/contextview/contextview';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Lazy } from 'vs/base/common/lazy';
import { Disposable } from 'vs/base/common/lifecycle';
import { escapeRegExpCharacters } from 'vs/base/common/strings';
import { generateUuid } from 'vs/base/common/uuid';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService';
import { IPosition } from 'vs/editor/common/core/position';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { CodeActionTriggerType } from 'vs/editor/common/modes';
import { CodeActionTriggerType, CopyPasteActionProvider, CopyPasteActionProviderRegistry } from 'vs/editor/common/modes';
import { codeActionCommandId, CodeActionItem, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/codeAction';
import { CodeActionUi } from 'vs/editor/contrib/codeAction/codeActionUi';
import { MessageController } from 'vs/editor/contrib/message/messageController';
Expand Down Expand Up @@ -65,6 +67,11 @@ const argsSchema: IJSONSchema = {
}
};

let clipboardItem: undefined | {
readonly handle: string;
readonly results: CancelablePromise<Map<CopyPasteActionProvider, unknown | undefined>>;
};

export class QuickFixController extends Disposable implements IEditorContribution {

public static readonly ID = 'editor.contrib.quickFixController';
Expand All @@ -83,13 +90,92 @@ export class QuickFixController extends Disposable implements IEditorContributio
@IContextKeyService contextKeyService: IContextKeyService,
@IEditorProgressService progressService: IEditorProgressService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
) {
super();

this._editor = editor;
this._model = this._register(new CodeActionModel(this._editor, markerService, contextKeyService, progressService));
this._register(this._model.onDidChangeState(newState => this.update(newState)));

document.addEventListener('copy', e => {
if (!e.clipboardData) {
return;
}

const model = editor.getModel();
const selection = this._editor.getSelection();
if (!model || !selection) {
return;
}

const providers = CopyPasteActionProviderRegistry.all(model).filter(x => !!x.onDidCopy);
if (!providers.length) {
return;
}

// Call prevent default to prevent our new clipboard data from being overwritten (is this really required?)
e.preventDefault();

// And then fill in raw text again since we prevented default
const clipboardText = model.getValueInRange(selection);
e.clipboardData.setData('text/plain', clipboardText);

// Save off a handle pointing to data that VS Code maintains.
const handle = generateUuid();
e.clipboardData.setData('x-vscode/id', handle);

const promise = createCancelablePromise(async token => {
const results = await Promise.all(providers.map(async provider => {
const result = await provider.onDidCopy!(model, selection, { text: clipboardText }, token);
return { provider, result };
}));

const map = new Map<CopyPasteActionProvider, unknown | undefined>();
for (const { provider, result } of results) {
map.set(provider, result);
}

return map;
});

clipboardItem = { handle: handle, results: promise };
});

document.addEventListener('paste', async e => {
const model = editor.getModel();
const selection = this._editor.getSelection();
if (!model || !selection) {
return;
}

const providers = CopyPasteActionProviderRegistry.all(model).filter(x => !!x.onDidCopy);
if (!providers.length) {
return;
}

const handle = e.clipboardData?.getData('x-vscode/id');
const clipboardText = e.clipboardData?.getData('text/plain') ?? '';

e.preventDefault();
e.stopImmediatePropagation();

let results: Map<CopyPasteActionProvider, unknown | undefined> | undefined;
if (handle && clipboardItem?.handle === handle) {
results = await clipboardItem.results;
}

for (const provider of providers) {
const data = results?.get(provider);
const edit = await provider.onWillPaste(model, selection, { text: clipboardText, data });
if (!edit) {
continue;
}

await this._bulkEditService.apply(ResourceEdit.convert(edit), { editor });
}
}, true);

this._ui = new Lazy(() =>
this._register(new CodeActionUi(editor, QuickFixAction.Id, AutoFixAction.Id, {
applyCodeAction: async (action, retrigger) => {
Expand Down
11 changes: 11 additions & 0 deletions src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5655,6 +5655,17 @@ declare namespace monaco.languages {
readonly actions: ReadonlyArray<CodeAction>;
}

export interface CopyPasteActionProvider {
id: string;
onDidCopy?(model: editor.ITextModel, selection: Selection, clipboard: {
readonly text: string;
}, token: CancellationToken): Promise<unknown | undefined>;
onWillPaste(model: editor.ITextModel, selection: Selection, clipboard: {
readonly text: string;
readonly data?: unknown;
}): Promise<WorkspaceEdit | undefined>;
}

/**
* Represents a parameter of a callable-signature. A parameter can
* have a label and a doc-comment.
Expand Down
84 changes: 84 additions & 0 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ProviderResult } from 'vscode';

/**
* This is the place for API experiments and proposals.
* These API are NOT stable and subject to change. They are only available in the Insiders
Expand Down Expand Up @@ -2171,4 +2173,86 @@ declare module 'vscode' {
with(color: ThemeColor): ThemeIcon2;
}
//#endregion

//#region https://github.com/microsoft/vscode/issues/30066

/**
* TODOs:
* - Multiple providers?
* - Is the document already edited in onWillPaste?
* - Does `onWillPaste` need to re-implement basic paste
*
* - Figure out CopyPasteActionProviderMetadata
*/

/**
* Provider invoked when the user copies and pastes code.
*
* This gives extensions a chance to hook into pasting and change the text
* that is pasted.
*/
interface CopyPasteActionProvider<T = unknown> {

/**
* Optional method invoked after the user copies text in a file.
*
* During `onDidCopy`, an extension can compute metadata that is attached to
* the clipboard and is passed back to the provider in `onWillPaste`.
*
* @param document Document where the copy took place.
* @param selection Selection being copied in the `document`.
* @param clipboard Information about the clipboard state after the copy.
*
* @return Optional metadata passed to `onWillPaste`.
*/
onDidCopy?(
document: TextDocument,
selection: Selection,
clipboard: { readonly text: string },
): ProviderResult<T>;

/**
* Invoked before the user pastes into a document.
*
* In this method, extensions can return a workspace edit that replaces the standard pasting behavior.
*
* @param document Document being pasted into
* @param selection Current selection in the document.
* @param clipboard Information about the clipboard state. This may contain the metadata from `onDidCopy`.
*
* @return Optional workspace edit that applies the paste. Return undefined to use standard pasting
*/
onWillPaste(
document: TextDocument,
selection: Selection,
clipboard: {
readonly text: string;
readonly data?: T;
},
): ProviderResult<WorkspaceEdit>;
}

/**
*
*/
interface CopyPasteActionProviderMetadata {
/**
* Identifies the type of code action
*/
readonly kind: CodeActionKind;
}

namespace languages {
/**
*
*/
export function registerCopyPasteActionProvider(
selector: DocumentSelector,
provider: CopyPasteActionProvider,
metadata: CopyPasteActionProviderMetadata
): Disposable;
}

//#endregion

}
20 changes: 20 additions & 0 deletions src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,26 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
this._registrations.set(handle, modes.CodeActionProviderRegistry.register(selector, provider));
}

// --- copy paste action provider

$registerCopyPasteActionProvider(handle: number, selector: IDocumentFilterDto[], id: string, supportsCopy: boolean): void {
const provider: modes.CopyPasteActionProvider = {
id,
onDidCopy: supportsCopy
? (model: ITextModel, selection: Selection, clipboard: { readonly text: string }): Promise<string | undefined> => {
return this._proxy.$onDidCopy(handle, model.uri, selection, clipboard);
}
: undefined,

onWillPaste: async (model: ITextModel, selection: Selection, clipboard: { text: string, data?: string }) => {
const result = await this._proxy.$onWillPaste(handle, model.uri, selection, { text: clipboard.text, handle: clipboard.data });
return result && reviveWorkspaceEditDto(result);
}
};

this._registrations.set(handle, modes.CopyPasteActionProviderRegistry.register(selector, provider));
}

// --- formatting

$registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void {
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
registerCodeActionsProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable {
return extHostLanguageFeatures.registerCodeActionProvider(extension, checkSelector(selector), provider, metadata);
},
registerCopyPasteActionProvider(selector: vscode.DocumentSelector, provider: vscode.CopyPasteActionProvider, metadata: vscode.CopyPasteActionProviderMetadata): vscode.Disposable {
return extHostLanguageFeatures.registerCopyPasteActionProvider(extension, checkSelector(selector), provider, metadata);
},
registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
},
Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable {
$registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], stopPattern: IRegExpDto | undefined): void;
$registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void;
$registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void;
$registerCopyPasteActionProvider(handle: number, selector: IDocumentFilterDto[], id: string, supportsCopy: boolean): void;
$registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void;
$registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void;
$registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void;
Expand Down Expand Up @@ -1398,6 +1399,8 @@ export interface ExtHostLanguageFeaturesShape {
$provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise<ICodeActionListDto | undefined>;
$resolveCodeAction(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<IWorkspaceEditDto | undefined>;
$releaseCodeActions(handle: number, cacheId: number): void;
$onDidCopy(handle: number, uri: UriComponents, selection: ISelection, clipboard: { readonly text: string; }): Promise<string | undefined>;
$onWillPaste(handle: number, uri: UriComponents, selection: ISelection, clipboard: { text: string; handle?: string | undefined; }): Promise<IWorkspaceEditDto | undefined>;
$provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;
$provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;
$provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;
Expand Down
Loading

0 comments on commit 9401beb

Please sign in to comment.