From da6ea9d497baa91103d9fae478d88d4cbedf49fc Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Wed, 23 Sep 2020 12:16:35 -0700 Subject: [PATCH 1/2] Add PasteEditProvider For #30066 This adds a new `documentPaste` api proposal that lets extensions hook into copy and paste. This can be used to do things such as: - Create link when pasting an image - Bring along imports when copy and pasting code --- .../markdown-language-features/package.json | 6 + .../package.nls.json | 3 +- .../src/extension.ts | 4 +- .../src/languageFeatures/copyPaste.ts | 26 +++ .../src/languageFeatures/dropIntoEditor.ts | 64 +++--- .../markdown-language-features/tsconfig.json | 3 +- src/vs/base/common/dataTransfer.ts | 4 + src/vs/editor/browser/dnd.ts | 28 +++ .../editor/browser/widget/codeEditorWidget.ts | 1 + src/vs/editor/common/languages.ts | 9 + .../common/services/languageFeatures.ts | 4 +- .../services/languageFeaturesService.ts | 3 +- .../browser/copyPasteContribution.ts | 25 +++ .../copyPaste/browser/copyPasteController.ts | 188 ++++++++++++++++++ .../browser/dropIntoEditorContribution.ts | 1 + src/vs/editor/editor.all.ts | 1 + .../api/browser/mainThreadLanguageFeatures.ts | 47 ++++- .../workbench/api/common/extHost.api.impl.ts | 3 + .../workbench/api/common/extHost.protocol.ts | 3 + .../api/common/extHostLanguageFeatures.ts | 69 ++++++- .../common/extensionsApiProposals.ts | 1 + .../vscode.proposed.documentPaste.d.ts | 46 +++++ 22 files changed, 496 insertions(+), 43 deletions(-) create mode 100644 extensions/markdown-language-features/src/languageFeatures/copyPaste.ts create mode 100644 src/vs/editor/browser/dnd.ts create mode 100644 src/vs/editor/contrib/copyPaste/browser/copyPasteContribution.ts create mode 100644 src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts create mode 100644 src/vscode-dts/vscode.proposed.documentPaste.d.ts diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 4ef07a0dd65cd..88314664f5179 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -414,6 +414,12 @@ "markdownDescription": "%configuration.markdown.editor.drop.enabled%", "scope": "resource" }, + "markdown.experimental.editor.pasteLinks.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.markdown.editor.pasteLinks.enabled%", + "scope": "resource" + }, "markdown.experimental.validate.enabled": { "type": "boolean", "scope": "resource", diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 2c8977cf2e015..1f3598e3fd857 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -28,7 +28,8 @@ "configuration.markdown.links.openLocation.currentGroup": "Open links in the active editor group.", "configuration.markdown.links.openLocation.beside": "Open links beside the active editor.", "configuration.markdown.suggest.paths.enabled.description": "Enable/disable path suggestions for markdown links", - "configuration.markdown.editor.drop.enabled": "Enable/disable dropping into the markdown editor to insert shift. Requires enabling `#workbench.experimental.editor.dropIntoEditor.enabled#`.", + "configuration.markdown.editor.drop.enabled": "Enable/disable dropping into the markdown editor to insert shift. Requires enabling `#workbenck.experimental.editor.dropIntoEditor.enabled#`.", + "configuration.markdown.editor.pasteLinks.enabled": "Enable/disable pasting files into a Markdown editor inserts Markdown links.", "configuration.markdown.experimental.validate.enabled.description": "Enable/disable all error reporting in Markdown files.", "configuration.markdown.experimental.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, e.g. `[link][ref]`. Requires enabling `#markdown.experimental.validate.enabled#`.", "configuration.markdown.experimental.validate.headerLinks.enabled.description": "Validate links to headers in Markdown files, e.g. `[link](#header)`. Requires enabling `#markdown.experimental.validate.enabled#`.", diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index f088f91391a72..0f2399692e987 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -6,8 +6,9 @@ import * as vscode from 'vscode'; import { CommandManager } from './commandManager'; import * as commands from './commands/index'; -import { register as registerDiagnostics } from './languageFeatures/diagnostics'; +import { registerPasteProvider } from './languageFeatures/copyPaste'; import { MdDefinitionProvider } from './languageFeatures/definitionProvider'; +import { register as registerDiagnostics } from './languageFeatures/diagnostics'; import { MdLinkProvider } from './languageFeatures/documentLinkProvider'; import { MdDocumentSymbolProvider } from './languageFeatures/documentSymbolProvider'; import { registerDropIntoEditor } from './languageFeatures/dropIntoEditor'; @@ -78,6 +79,7 @@ function registerMarkdownLanguageFeatures( MdPathCompletionProvider.register(selector, engine, linkProvider), registerDiagnostics(selector, engine, workspaceContents, linkProvider, commandManager), registerDropIntoEditor(selector), + registerPasteProvider(selector), registerFindFileReferences(commandManager, referencesProvider), ); } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts b/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts new file mode 100644 index 0000000000000..d9e939b463cdc --- /dev/null +++ b/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { tryInsertUriList } from './dropIntoEditor'; + +export function registerPasteProvider(selector: vscode.DocumentSelector) { + return vscode.languages.registerDocumentPasteEditProvider(selector, new class implements vscode.DocumentPasteEditProvider { + + async provideDocumentPasteEdits( + document: vscode.TextDocument, + range: vscode.Range, + dataTransfer: vscode.DataTransfer, + token: vscode.CancellationToken, + ): Promise { + const enabled = vscode.workspace.getConfiguration('markdown', document).get('experimental.editor.pasteLinks.enabled', false); + if (!enabled) { + return; + } + + return tryInsertUriList(document, range, dataTransfer, token); + } + }); +} diff --git a/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts b/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts index d5d7c4d9d2916..2ad71ec0516f0 100644 --- a/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts +++ b/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts @@ -32,45 +32,45 @@ export function registerDropIntoEditor(selector: vscode.DocumentSelector) { } const replacementRange = new vscode.Range(position, position); - return this.tryInsertUriList(document, replacementRange, dataTransfer, token); + return tryInsertUriList(document, replacementRange, dataTransfer, token); } + }); +} - private async tryInsertUriList(document: vscode.TextDocument, replacementRange: vscode.Range, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { - const urlList = await dataTransfer.get('text/uri-list')?.asString(); - if (!urlList || token.isCancellationRequested) { - return undefined; - } - - const uris: vscode.Uri[] = []; - for (const resource of urlList.split('\n')) { - try { - uris.push(vscode.Uri.parse(resource)); - } catch { - // noop - } - } +export async function tryInsertUriList(document: vscode.TextDocument, replacementRange: vscode.Range, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { + const urlList = await dataTransfer.get('text/uri-list')?.asString(); + if (!urlList || token.isCancellationRequested) { + return undefined; + } - if (!uris.length) { - return; - } + const uris: vscode.Uri[] = []; + for (const resource of urlList.split('\n')) { + try { + uris.push(vscode.Uri.parse(resource)); + } catch { + // noop + } + } - const snippet = new vscode.SnippetString(); - uris.forEach((uri, i) => { - const mdPath = document.uri.scheme === uri.scheme - ? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath).replace(/\\/g, '/')) - : uri.toString(false); + if (!uris.length) { + return; + } - const ext = URI.Utils.extname(uri).toLowerCase(); - snippet.appendText(imageFileExtensions.has(ext) ? '![' : '['); - snippet.appendTabstop(); - snippet.appendText(`](${mdPath})`); + const snippet = new vscode.SnippetString(); + uris.forEach((uri, i) => { + const mdPath = document.uri.scheme === uri.scheme + ? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath).replace(/\\/g, '/')) + : uri.toString(false); - if (i <= uris.length - 1 && uris.length > 1) { - snippet.appendText(' '); - } - }); + const ext = URI.Utils.extname(uri).toLowerCase(); + snippet.appendText(imageFileExtensions.has(ext) ? '![' : '['); + snippet.appendTabstop(); + snippet.appendText(`](${mdPath})`); - return new vscode.SnippetTextEdit(replacementRange, snippet); + if (i <= uris.length - 1 && uris.length > 1) { + snippet.appendText(' '); } }); + + return new vscode.SnippetTextEdit(replacementRange, snippet); } diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index 775eaa7c0a8d2..7c1d4a7fca897 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -7,6 +7,7 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.textEditorDrop.d.ts", - "../../src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts" + "../../src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts", + "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts" ] } diff --git a/src/vs/base/common/dataTransfer.ts b/src/vs/base/common/dataTransfer.ts index 1f56e79f75190..d5d99bd283b32 100644 --- a/src/vs/base/common/dataTransfer.ts +++ b/src/vs/base/common/dataTransfer.ts @@ -37,6 +37,10 @@ export class VSDataTransfer { this._data.set(mimeType, value); } + public delete(mimeType: string) { + this._data.delete(mimeType); + } + public setString(mimeType: string, stringOrPromise: string | Promise) { this.set(mimeType, { asString: async () => stringOrPromise, diff --git a/src/vs/editor/browser/dnd.ts b/src/vs/editor/browser/dnd.ts new file mode 100644 index 0000000000000..08a3f55878640 --- /dev/null +++ b/src/vs/editor/browser/dnd.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; +import { URI } from 'vs/base/common/uri'; + + +export function toVSDataTransfer(dataTransfer: DataTransfer) { + const vsDataTransfer = new VSDataTransfer(); + for (const item of dataTransfer.items) { + const type = item.type; + if (item.kind === 'string') { + const asStringValue = new Promise(resolve => item.getAsString(resolve)); + vsDataTransfer.setString(type, asStringValue); + } else if (item.kind === 'file') { + const file = item.getAsFile() as null | (File & { path?: string }); + if (file) { + const uri = file.path ? URI.parse(file.path) : undefined; + vsDataTransfer.setFile(type, file.name, uri, async () => { + return new Uint8Array(await file.arrayBuffer()); + }); + } + } + } + return vsDataTransfer; +} diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 991afab9ef830..4e297424d97a1 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -2033,6 +2033,7 @@ export class EditorModeContext extends Disposable { this._register(_languageFeaturesService.completionProvider.onDidChange(update)); this._register(_languageFeaturesService.codeActionProvider.onDidChange(update)); this._register(_languageFeaturesService.codeLensProvider.onDidChange(update)); + this._register(_languageFeaturesService.documentPasteEditProvider.onDidChange(update)); this._register(_languageFeaturesService.definitionProvider.onDidChange(update)); this._register(_languageFeaturesService.declarationProvider.onDidChange(update)); this._register(_languageFeaturesService.implementationProvider.onDidChange(update)); diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 1c366d79a5abe..5fd23bffad9fd 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -916,6 +916,15 @@ export interface CodeActionProvider { _getAdditionalMenuItems?(context: CodeActionContext, actions: readonly CodeAction[]): Command[]; } +/** + * @internal + */ +export interface DocumentPasteEditProvider { + prepareDocumentPaste?(model: model.ITextModel, selection: Selection, dataTransfer: VSDataTransfer, token: CancellationToken): Promise; + + provideDocumentPasteEdits(model: model.ITextModel, selection: Selection, dataTransfer: VSDataTransfer, token: CancellationToken): Promise; +} + /** * Represents a parameter of a callable-signature. A parameter can * have a label and a doc-comment. diff --git a/src/vs/editor/common/services/languageFeatures.ts b/src/vs/editor/common/services/languageFeatures.ts index e0ba241658228..2c80887df1a54 100644 --- a/src/vs/editor/common/services/languageFeatures.ts +++ b/src/vs/editor/common/services/languageFeatures.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { LanguageFeatureRegistry, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentPasteEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const ILanguageFeaturesService = createDecorator('ILanguageFeaturesService'); @@ -25,6 +25,8 @@ export interface ILanguageFeaturesService { readonly codeActionProvider: LanguageFeatureRegistry; + readonly documentPasteEditProvider: LanguageFeatureRegistry; + readonly renameProvider: LanguageFeatureRegistry; readonly documentFormattingEditProvider: LanguageFeatureRegistry; diff --git a/src/vs/editor/common/services/languageFeaturesService.ts b/src/vs/editor/common/services/languageFeaturesService.ts index 2ed7cebcf62e2..4059a90b37098 100644 --- a/src/vs/editor/common/services/languageFeaturesService.ts +++ b/src/vs/editor/common/services/languageFeaturesService.ts @@ -5,7 +5,7 @@ import { URI } from 'vs/base/common/uri'; import { LanguageFeatureRegistry, NotebookInfo, NotebookInfoResolver } from 'vs/editor/common/languageFeatureRegistry'; -import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; +import { CodeActionProvider, CodeLensProvider, CompletionItemProvider, DocumentPasteEditProvider, DeclarationProvider, DefinitionProvider, DocumentColorProvider, DocumentFormattingEditProvider, DocumentHighlightProvider, DocumentOnDropEditProvider, DocumentRangeFormattingEditProvider, DocumentRangeSemanticTokensProvider, DocumentSemanticTokensProvider, DocumentSymbolProvider, EvaluatableExpressionProvider, FoldingRangeProvider, HoverProvider, ImplementationProvider, InlayHintsProvider, InlineCompletionsProvider, InlineValuesProvider, LinkedEditingRangeProvider, LinkProvider, OnTypeFormattingEditProvider, ReferenceProvider, RenameProvider, SelectionRangeProvider, SignatureHelpProvider, TypeDefinitionProvider } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -41,6 +41,7 @@ export class LanguageFeaturesService implements ILanguageFeaturesService { readonly documentRangeSemanticTokensProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentSemanticTokensProvider = new LanguageFeatureRegistry(this._score.bind(this)); readonly documentOnDropEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); + readonly documentPasteEditProvider = new LanguageFeatureRegistry(this._score.bind(this)); private _notebookTypeResolver?: NotebookInfoResolver; diff --git a/src/vs/editor/contrib/copyPaste/browser/copyPasteContribution.ts b/src/vs/editor/contrib/copyPaste/browser/copyPasteContribution.ts new file mode 100644 index 0000000000000..55c120d060f65 --- /dev/null +++ b/src/vs/editor/contrib/copyPaste/browser/copyPasteContribution.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; +import { CopyPasteController } from 'vs/editor/contrib/copyPaste/browser/copyPasteController'; +import * as nls from 'vs/nls'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; + +registerEditorContribution(CopyPasteController.ID, CopyPasteController); + +Registry.as(Extensions.Configuration).registerConfiguration({ + ...editorConfigurationBaseNode, + properties: { + 'editor.experimental.pasteActions.enabled': { + type: 'boolean', + scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, + description: nls.localize('pasteActions', "Enable/disable running edits from extensions on paste."), + default: false, + }, + } +}); diff --git a/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts b/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts new file mode 100644 index 0000000000000..868252ae833ae --- /dev/null +++ b/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { addDisposableListener } from 'vs/base/browser/dom'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Mimes } from 'vs/base/common/mime'; +import { generateUuid } from 'vs/base/common/uuid'; +import { toVSDataTransfer } from 'vs/editor/browser/dnd'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; +import { Selection } from 'vs/editor/common/core/selection'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { DocumentPasteEditProvider, SnippetTextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +const vscodeClipboardMime = 'application/vnd.code.copyId'; + +class DefaultPasteEditProvider implements DocumentPasteEditProvider { + + async provideDocumentPasteEdits(model: ITextModel, selection: Selection, dataTransfer: VSDataTransfer, _token: CancellationToken): Promise { + const textDataTransfer = dataTransfer.get(Mimes.text) ?? dataTransfer.get('text'); + if (textDataTransfer) { + const text = await textDataTransfer.asString(); + return { + edits: [new ResourceTextEdit(model.uri, { range: selection, text })] + }; + } + + return undefined; + } +} + +export class CopyPasteController extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.copyPasteActionController'; + + public static get(editor: ICodeEditor): CopyPasteController { + return editor.getContribution(CopyPasteController.ID)!; + } + + private readonly _editor: ICodeEditor; + + private _currentClipboardItem: undefined | { + readonly handle: string; + readonly dataTransferPromise: CancelablePromise; + }; + + constructor( + editor: ICodeEditor, + @IBulkEditService private readonly _bulkEditService: IBulkEditService, + @IClipboardService private readonly _clipboardService: IClipboardService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + ) { + super(); + + this._editor = editor; + + this._languageFeaturesService.documentPasteEditProvider.register('*', new DefaultPasteEditProvider()); + + const container = editor.getContainerDomNode(); + + this._register(addDisposableListener(container, 'copy', (e: ClipboardEvent) => { + if (!e.clipboardData) { + return; + } + + const model = editor.getModel(); + const selection = this._editor.getSelection(); + if (!model || !selection) { + return; + } + + if (!this.arePasteActionsEnabled(model)) { + return; + } + + const providers = this._languageFeaturesService.documentPasteEditProvider.ordered(model).filter(x => !!x.prepareDocumentPaste); + if (!providers.length) { + return; + } + + const dataTransfer = toVSDataTransfer(e.clipboardData); + + // Save off a handle pointing to data that VS Code maintains. + const handle = generateUuid(); + e.clipboardData.setData(vscodeClipboardMime, handle); + + const promise = createCancelablePromise(async token => { + const results = await Promise.all(providers.map(provider => { + return provider.prepareDocumentPaste!(model, selection, dataTransfer, token); + })); + + for (const result of results) { + result?.forEach((value, key) => { + dataTransfer.set(key, value); + }); + } + + return dataTransfer; + }); + + this._currentClipboardItem?.dataTransferPromise.cancel(); + this._currentClipboardItem = { handle: handle, dataTransferPromise: promise }; + })); + + this._register(addDisposableListener(container, 'paste', async (e: ClipboardEvent) => { + const model = editor.getModel(); + const selection = this._editor.getSelection(); + if (!model || !selection || !e.clipboardData) { + return; + } + + if (!this.arePasteActionsEnabled(model)) { + return; + } + + const originalDocVersion = model.getVersionId(); + + const providers = this._languageFeaturesService.documentPasteEditProvider.ordered(model); + if (!providers.length) { + return; + } + + const handle = e.clipboardData?.getData(vscodeClipboardMime); + if (typeof handle !== 'string') { + return; + } + + e.preventDefault(); + e.stopImmediatePropagation(); + + const dataTransfer = toVSDataTransfer(e.clipboardData); + + if (handle && this._currentClipboardItem?.handle === handle) { + const toMergeDataTransfer = await this._currentClipboardItem.dataTransferPromise; + toMergeDataTransfer.forEach((value, key) => { + dataTransfer.set(key, value); + }); + } + + if (!dataTransfer.has(Mimes.uriList)) { + const resources = await this._clipboardService.readResources(); + if (resources.length) { + const value = resources.join('\n'); + dataTransfer.set(Mimes.uriList, { + value, + asString: async () => value, + asFile: () => undefined, + }); + } + } + + dataTransfer.delete(vscodeClipboardMime); + + for (const provider of providers) { + const edit = await provider.provideDocumentPasteEdits(model, selection, dataTransfer, CancellationToken.None); + if (originalDocVersion !== model.getVersionId()) { + return; + } + + if (edit) { + if ((edit as WorkspaceEdit).edits) { + await this._bulkEditService.apply(ResourceEdit.convert(edit as WorkspaceEdit), { editor }); + } else { + performSnippetEdit(editor, edit as SnippetTextEdit); + } + return; + } + } + }, true)); + } + + public arePasteActionsEnabled(model: ITextModel): boolean { + return this._configurationService.getValue('editor.experimental.pasteActions.enabled', { + resource: model.uri + }); + } +} diff --git a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts index edbc29db56ee5..30fc995ae70f4 100644 --- a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts +++ b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts @@ -188,3 +188,4 @@ class DefaultOnDropProvider implements DocumentOnDropEditProvider { registerEditorContribution(DropIntoEditorController.ID, DropIntoEditorController); + diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index aba1e89aa1db6..1168ef7bb16da 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -15,6 +15,7 @@ import 'vs/editor/contrib/clipboard/browser/clipboard'; import 'vs/editor/contrib/codeAction/browser/codeActionContributions'; import 'vs/editor/contrib/codelens/browser/codelensController'; import 'vs/editor/contrib/colorPicker/browser/colorContributions'; +import 'vs/editor/contrib/copyPaste/browser/copyPasteContribution'; import 'vs/editor/contrib/comment/browser/comment'; import 'vs/editor/contrib/contextmenu/browser/contextmenu'; import 'vs/editor/contrib/cursorUndo/browser/cursorUndo'; diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index ff3bd15f82a46..f89e0698b9d95 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -5,6 +5,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; import { CancellationError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -23,14 +24,13 @@ import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; import { DataTransferCache } from 'vs/workbench/api/common/shared/dataTransferCache'; import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import * as search from 'vs/workbench/contrib/search/common/search'; import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape, reviveWorkspaceEditDto } from '../common/extHost.protocol'; -import { VSDataTransfer } from 'vs/base/common/dataTransfer'; -import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceEditDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape, reviveWorkspaceEditDto } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -366,6 +366,47 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread this._registrations.set(handle, this._languageFeaturesService.codeActionProvider.register(selector, provider)); } + // --- copy paste action provider + + $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], supportsCopy: boolean): void { + const provider: languages.DocumentPasteEditProvider = { + prepareDocumentPaste: supportsCopy + ? async (model: ITextModel, selection: Selection, dataTransfer: VSDataTransfer, token: CancellationToken): Promise => { + const dataTransferDto = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); + if (token.isCancellationRequested) { + return undefined; + } + + const result = await this._proxy.$prepareDocumentPaste(handle, model.uri, selection, dataTransferDto, token); + if (!result) { + return undefined; + } + + + const dataTransferOut = new VSDataTransfer(); + result.items.forEach(([type, item]) => { + dataTransferOut.setString(type, item.asString); + }); + return dataTransferOut; + } + : undefined, + + provideDocumentPasteEdits: async (model: ITextModel, selection: Selection, dataTransfer: VSDataTransfer, token: CancellationToken) => { + const d = await typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); + const result = await this._proxy.$providePasteEdits(handle, model.uri, selection, d, token); + if (!result) { + return; + } else if ((result as IWorkspaceEditDto).edits) { + return reviveWorkspaceEditDto(result as IWorkspaceEditDto); + } else { + return result as languages.SnippetTextEdit; + } + } + }; + + this._registrations.set(handle, this._languageFeaturesService.documentPasteEditProvider.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 1c1d4cbcaea64..28929cd3c71b4 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -459,6 +459,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); }, + registerDocumentPasteEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider): vscode.Disposable { + return extHostLanguageFeatures.registerDocumentPasteEditProvider(extension, checkSelector(selector), provider); + }, 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 9ae91a955be90..f41190bff52b7 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -371,6 +371,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerLinkedEditingRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void; + $registerPasteEditProvider(handle: number, selector: IDocumentFilterDto[], 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; @@ -1734,6 +1735,8 @@ export interface ExtHostLanguageFeaturesShape { $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: languages.CodeActionContext, token: CancellationToken): Promise; $resolveCodeAction(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCodeActions(handle: number, cacheId: number): void; + $prepareDocumentPaste(handle: number, uri: UriComponents, range: IRange, dataTransfer: DataTransferDTO, token: CancellationToken): Promise; + $providePasteEdits(handle: number, uri: UriComponents, range: IRange, dataTransfer: DataTransferDTO, token: CancellationToken): Promise | undefined>; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: languages.FormattingOptions, token: CancellationToken): Promise; $provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: languages.FormattingOptions, token: CancellationToken): Promise; $provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: languages.FormattingOptions, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 53f5ad0808164..4f06e60d82c4d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { mixin } from 'vs/base/common/objects'; import type * as vscode from 'vscode'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit, Location, InlineCompletionTriggerKindNew, InlineCompletionTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit, Location, InlineCompletionTriggerKindNew, InlineCompletionTriggerKind, WorkspaceEdit } from 'vs/workbench/api/common/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import * as languages from 'vs/editor/common/languages'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -31,7 +31,7 @@ import { IdGenerator } from 'vs/base/common/idGenerator'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { Cache } from './cache'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { isCancellationError } from 'vs/base/common/errors'; +import { isCancellationError, NotImplementedError } from 'vs/base/common/errors'; import { raceCancellationError } from 'vs/base/common/async'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @@ -485,6 +485,52 @@ class CodeActionAdapter { } } +class DocumentPasteEditProvider { + + constructor( + private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape, + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentPasteEditProvider, + private readonly _handle: number, + ) { } + + async prepareDocumentPaste(resource: URI, range: IRange, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + if (!this._provider.prepareDocumentPaste) { + return undefined; + } + + const doc = this._documents.getDocument(resource); + const vscodeRange = typeConvert.Range.to(range); + + const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, () => { + throw new NotImplementedError(); + }); + await this._provider.prepareDocumentPaste(doc, vscodeRange, dataTransfer, token); + + return typeConvert.DataTransfer.toDataTransferDTO(dataTransfer); + } + + async providePasteEdits(requestId: number, resource: URI, range: IRange, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise> { + const doc = this._documents.getDocument(resource); + const vscodeRange = typeConvert.Range.to(range); + + const dataTransfer = typeConvert.DataTransfer.toDataTransfer(dataTransferDto, async (index) => { + return (await this._proxy.$resolveDocumentOnDropFileData(this._handle, requestId, index)).buffer; + }); + + const edit = await this._provider.provideDocumentPasteEdits(doc, vscodeRange, dataTransfer, token); + if (!edit) { + return; + } + + if (edit instanceof WorkspaceEdit) { + return typeConvert.WorkspaceEdit.from(edit); + } else { + return typeConvert.SnippetTextEdit.from(edit as vscode.SnippetTextEdit); + } + } +} + class DocumentFormattingAdapter { constructor( @@ -1769,7 +1815,7 @@ class DocumentOnDropEditAdapter { } type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter - | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter + | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentPasteEditProvider | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter | CompletionsAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter @@ -2413,6 +2459,23 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF Promise.resolve(adapter.provideDocumentOnDropEdits(requestId, URI.revive(resource), position, dataTransferDto, token)), undefined, undefined); } + // --- copy/paste actions + + registerDocumentPasteEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider): vscode.Disposable { + const handle = this._nextHandle(); + this._adapter.set(handle, new AdapterData(new DocumentPasteEditProvider(this._proxy, this._documents, provider, handle), extension)); + this._proxy.$registerPasteEditProvider(handle, this._transformDocumentSelector(selector), !!provider.prepareDocumentPaste); + return this._createDisposable(handle); + } + + $prepareDocumentPaste(handle: number, resource: UriComponents, range: IRange, dataTransfer: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise { + return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.prepareDocumentPaste(URI.revive(resource), range, dataTransfer, token), undefined, token); + } + + $providePasteEdits(handle: number, resource: UriComponents, range: IRange, dataTransferDto: extHostProtocol.DataTransferDTO, token: CancellationToken): Promise | undefined> { + return this._withAdapter(handle, DocumentPasteEditProvider, adapter => adapter.providePasteEdits(0, URI.revive(resource), range, dataTransferDto, token), undefined, token); + } + // --- configuration private static _serializeRegExp(regExp: RegExp): extHostProtocol.IRegExpDto { diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index c562da5671fc5..61f2b2d1a848d 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -18,6 +18,7 @@ export const allApiProposals = Object.freeze({ dataTransferFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts', diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', + documentPaste: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', diff --git a/src/vscode-dts/vscode.proposed.documentPaste.d.ts b/src/vscode-dts/vscode.proposed.documentPaste.d.ts new file mode 100644 index 0000000000000..1777c8d4145c0 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.documentPaste.d.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/30066/ + + /** + * Provider invoked when the user copies and pastes code. + */ + interface DocumentPasteEditProvider { + + /** + * Optional method invoked after the user copies text in a file. + * + * During {@link prepareDocumentPaste}, an extension can compute metadata that is attached to + * a {@link DataTransfer} and is passed back to the provider in {@link provideDocumentPasteEdits}. + * + * @param document Document where the copy took place. + * @param range Range being copied in the `document`. + * @param dataTransfer The data transfer associated with the copy. You can store additional values on this for later use in {@link provideDocumentPasteEdits}. + * @param token A cancellation token. + */ + prepareDocumentPaste?(document: TextDocument, range: Range, dataTransfer: DataTransfer, token: CancellationToken): void | Thenable; + + /** + * 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 range Currently selected range in the document. + * @param dataTransfer The data transfer associated with the paste. + * @param token A cancellation token. + * + * @return Optional workspace edit that applies the paste. Return undefined to use standard pasting. + */ + provideDocumentPasteEdits(document: TextDocument, range: Range, dataTransfer: DataTransfer, token: CancellationToken): ProviderResult; + } + + namespace languages { + export function registerDocumentPasteEditProvider(selector: DocumentSelector, provider: DocumentPasteEditProvider): Disposable; + } +} From 04af11afa8f32a49e969057d36b0b1b0741968bd Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Tue, 24 May 2022 11:10:53 -0700 Subject: [PATCH 2/2] Addressing comments --- .../markdown-language-features/package.json | 3 +- .../editor/browser/widget/codeEditorWidget.ts | 1 - .../copyPaste/browser/copyPasteController.ts | 67 ++++++++++--------- .../workbench/api/common/extHost.api.impl.ts | 1 + 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 88314664f5179..f2ad883243f3b 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -16,7 +16,8 @@ "Programming Languages" ], "enabledApiProposals": [ - "textEditorDrop" + "textEditorDrop", + "documentPaste" ], "activationEvents": [ "onLanguage:markdown", diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 4e297424d97a1..991afab9ef830 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -2033,7 +2033,6 @@ export class EditorModeContext extends Disposable { this._register(_languageFeaturesService.completionProvider.onDidChange(update)); this._register(_languageFeaturesService.codeActionProvider.onDidChange(update)); this._register(_languageFeaturesService.codeLensProvider.onDidChange(update)); - this._register(_languageFeaturesService.documentPasteEditProvider.onDidChange(update)); this._register(_languageFeaturesService.definitionProvider.onDidChange(update)); this._register(_languageFeaturesService.declarationProvider.onDidChange(update)); this._register(_languageFeaturesService.implementationProvider.onDidChange(update)); diff --git a/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts b/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts index 868252ae833ae..6c203af56cd52 100644 --- a/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/copyPaste/browser/copyPasteController.ts @@ -18,6 +18,7 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { DocumentPasteEditProvider, SnippetTextEdit, WorkspaceEdit } from 'vs/editor/common/languages'; import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState'; import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -114,12 +115,12 @@ export class CopyPasteController extends Disposable implements IEditorContributi })); this._register(addDisposableListener(container, 'paste', async (e: ClipboardEvent) => { - const model = editor.getModel(); const selection = this._editor.getSelection(); - if (!model || !selection || !e.clipboardData) { + if (!e.clipboardData || !selection || !editor.hasModel()) { return; } + const model = editor.getModel(); if (!this.arePasteActionsEnabled(model)) { return; } @@ -139,43 +140,49 @@ export class CopyPasteController extends Disposable implements IEditorContributi e.preventDefault(); e.stopImmediatePropagation(); - const dataTransfer = toVSDataTransfer(e.clipboardData); + const tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection); - if (handle && this._currentClipboardItem?.handle === handle) { - const toMergeDataTransfer = await this._currentClipboardItem.dataTransferPromise; - toMergeDataTransfer.forEach((value, key) => { - dataTransfer.set(key, value); - }); - } + try { + const dataTransfer = toVSDataTransfer(e.clipboardData); - if (!dataTransfer.has(Mimes.uriList)) { - const resources = await this._clipboardService.readResources(); - if (resources.length) { - const value = resources.join('\n'); - dataTransfer.set(Mimes.uriList, { - value, - asString: async () => value, - asFile: () => undefined, + if (handle && this._currentClipboardItem?.handle === handle) { + const toMergeDataTransfer = await this._currentClipboardItem.dataTransferPromise; + toMergeDataTransfer.forEach((value, key) => { + dataTransfer.set(key, value); }); } - } - dataTransfer.delete(vscodeClipboardMime); - - for (const provider of providers) { - const edit = await provider.provideDocumentPasteEdits(model, selection, dataTransfer, CancellationToken.None); - if (originalDocVersion !== model.getVersionId()) { - return; + if (!dataTransfer.has(Mimes.uriList)) { + const resources = await this._clipboardService.readResources(); + if (resources.length) { + const value = resources.join('\n'); + dataTransfer.set(Mimes.uriList, { + value, + asString: async () => value, + asFile: () => undefined, + }); + } } - if (edit) { - if ((edit as WorkspaceEdit).edits) { - await this._bulkEditService.apply(ResourceEdit.convert(edit as WorkspaceEdit), { editor }); - } else { - performSnippetEdit(editor, edit as SnippetTextEdit); + dataTransfer.delete(vscodeClipboardMime); + + for (const provider of providers) { + const edit = await provider.provideDocumentPasteEdits(model, selection, dataTransfer, tokenSource.token); + if (originalDocVersion !== model.getVersionId()) { + return; + } + + if (edit) { + if ((edit as WorkspaceEdit).edits) { + await this._bulkEditService.apply(ResourceEdit.convert(edit as WorkspaceEdit), { editor }); + } else { + performSnippetEdit(editor, edit as SnippetTextEdit); + } + return; } - return; } + } finally { + tokenSource.dispose(); } }, true)); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 28929cd3c71b4..0480807d069f7 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -460,6 +460,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguageFeatures.registerCodeActionProvider(extension, checkSelector(selector), provider, metadata); }, registerDocumentPasteEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentPasteEditProvider): vscode.Disposable { + checkProposedApiEnabled(extension, 'documentPaste'); return extHostLanguageFeatures.registerDocumentPasteEditProvider(extension, checkSelector(selector), provider); }, registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {