diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index f5c0b4114a4d8..1fb49920bb6bb 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -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); @@ -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 { + + async onDidCopy( + _document: vscode.TextDocument, + _selection: vscode.Selection, + _clipboard: { readonly text: string }, + ): Promise { + return { count: copyCount++ }; + } + + async onWillPaste( + document: vscode.TextDocument, + selection: vscode.Selection, + clipboard: { readonly text: string; readonly data?: ClipboardData; } + ): Promise { + 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( diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 17539558c91ca..16a06da0be834 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -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)); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 2b864b92dd1e5..9d0e738e24958 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -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; + + onWillPaste( + model: model.ITextModel, + selection: Selection, + clipboard: { + readonly text: string; + readonly data?: unknown; + }, + ): Promise; +} + /** * Represents a parameter of a callable-signature. A parameter can * have a label and a doc-comment. @@ -1732,6 +1752,11 @@ export const CodeLensProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const CopyPasteActionProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index 65c19a781f620..8c8d48fcdbb83 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -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'; @@ -65,6 +67,11 @@ const argsSchema: IJSONSchema = { } }; +let clipboardItem: undefined | { + readonly handle: string; + readonly results: CancelablePromise>; +}; + export class QuickFixController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.quickFixController'; @@ -83,6 +90,7 @@ export class QuickFixController extends Disposable implements IEditorContributio @IContextKeyService contextKeyService: IContextKeyService, @IEditorProgressService progressService: IEditorProgressService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IBulkEditService private readonly _bulkEditService: IBulkEditService, ) { super(); @@ -90,6 +98,84 @@ export class QuickFixController extends Disposable implements IEditorContributio 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(); + 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 | 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) => { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 97005e1f82ced..8855d52d0cc70 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5655,6 +5655,17 @@ declare namespace monaco.languages { readonly actions: ReadonlyArray; } + export interface CopyPasteActionProvider { + id: string; + onDidCopy?(model: editor.ITextModel, selection: Selection, clipboard: { + readonly text: string; + }, token: CancellationToken): Promise; + onWillPaste(model: editor.ITextModel, selection: Selection, clipboard: { + readonly text: string; + readonly data?: unknown; + }): Promise; + } + /** * Represents a parameter of a callable-signature. A parameter can * have a label and a doc-comment. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index ce78fd77ff9ba..21f5ef5246c4e 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -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 @@ -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 { + + /** + * 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; + + /** + * 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; + } + + /** + * + */ + interface CopyPasteActionProviderMetadata { + /** + * Identifies the type of code action + */ + readonly kind: CodeActionKind; + } + + namespace languages { + /** + * + */ + export function registerCopyPasteActionProvider( + selector: DocumentSelector, + provider: CopyPasteActionProvider, + metadata: CopyPasteActionProviderMetadata + ): Disposable; + } + + //#endregion + } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 635730ba75b15..86557a51b3902 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -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 => { + 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 { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 000b8a27af594..ce04f57e797c5 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -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); }, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 586c1549ba60f..9290904198521 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -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; @@ -1398,6 +1399,8 @@ export interface ExtHostLanguageFeaturesShape { $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise; $resolveCodeAction(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCodeActions(handle: number, cacheId: number): void; + $onDidCopy(handle: number, uri: UriComponents, selection: ISelection, clipboard: { readonly text: string; }): Promise; + $onWillPaste(handle: number, uri: UriComponents, selection: ISelection, clipboard: { text: string; handle?: string | undefined; }): Promise; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: modes.FormattingOptions, token: CancellationToken): Promise; $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: modes.FormattingOptions, token: CancellationToken): Promise; $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: modes.FormattingOptions, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index ef49bb42954c6..885714fd9600c 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -482,6 +482,49 @@ class CodeActionAdapter { } } +class CopyPasteActionProvider { + + private readonly _idPool = new IdGenerator(''); + + private storedValue?: { handle: string, data: unknown }; + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.CopyPasteActionProvider + ) { } + + async onDidCopy(resource: URI, selection: ISelection, clipboard: { readonly text: string; }): Promise { + if (!this._provider.onDidCopy) { + return undefined; + } + + const doc = this._documents.getDocument(resource); + const vscodeSelection = typeConvert.Selection.to(selection); + + const result = await this._provider.onDidCopy(doc, vscodeSelection, clipboard); + if (!result) { + return undefined; + } + + const handle = this._idPool.nextId(); + this.storedValue = { handle, data: result }; + return handle; + } + + async onWillPaste(resource: URI, selection: ISelection, clipboard: { text: string; handle: string | undefined; }): Promise { + const doc = this._documents.getDocument(resource); + const vscodeSelection = typeConvert.Selection.to(selection); + + const data = clipboard.handle && this.storedValue?.handle === clipboard.handle ? this.storedValue.data : undefined; + const result = await this._provider.onWillPaste(doc, vscodeSelection, { text: clipboard.text, data: data }); + if (!result) { + return; + } + + return typeConvert.WorkspaceEdit.from(result); + } +} + class DocumentFormattingAdapter { constructor( @@ -1320,7 +1363,7 @@ class CallHierarchyAdapter { } type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter - | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter + | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | CopyPasteActionProvider | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter @@ -1630,6 +1673,24 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, CodeActionAdapter, adapter => Promise.resolve(adapter.releaseCodeActions(cacheId)), undefined); } + // --- copy/paste actions + + registerCopyPasteActionProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CopyPasteActionProvider, metadata: vscode.CopyPasteActionProviderMetadata): vscode.Disposable { + const store = new DisposableStore(); + const handle = this._addNewAdapter(new CopyPasteActionProvider(this._documents, provider), extension); + this._proxy.$registerCopyPasteActionProvider(handle, this._transformDocumentSelector(selector), 'todo', !!provider.onDidCopy); + store.add(this._createDisposable(handle)); + return store; + } + + $onDidCopy(handle: number, resource: UriComponents, selection: ISelection, clipboard: { readonly text: string; }): Promise { + return this._withAdapter(handle, CopyPasteActionProvider, adapter => adapter.onDidCopy(URI.revive(resource), selection, clipboard), undefined); + } + + $onWillPaste(handle: number, resource: UriComponents, selection: ISelection, clipboard: { text: string; handle: string | undefined; }): Promise { + return this._withAdapter(handle, CopyPasteActionProvider, adapter => adapter.onWillPaste(URI.revive(resource), selection, clipboard), undefined); + } + // --- formatting registerDocumentFormattingEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentFormattingEditProvider): vscode.Disposable {