Add PasteEditProvider (#107283)

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
This commit is contained in:
Matt Bierner 2022-05-25 03:27:58 -07:00 committed by GitHub
parent a6724dcc10
commit e4f7f6a9da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 512 additions and 51 deletions

View file

@ -16,7 +16,8 @@
"Programming Languages"
],
"enabledApiProposals": [
"textEditorDrop"
"textEditorDrop",
"documentPaste"
],
"activationEvents": [
"onLanguage:markdown",
@ -414,6 +415,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",

View file

@ -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#`.",

View file

@ -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),
);
}

View file

@ -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<vscode.SnippetTextEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('experimental.editor.pasteLinks.enabled', false);
if (!enabled) {
return;
}
return tryInsertUriList(document, range, dataTransfer, token);
}
});
}

View file

@ -32,45 +32,45 @@ export function registerDropIntoEditor(selector: vscode.DocumentSelector) {
}
const replacementRange = new vscode.Range(position, position);
return this.tryInsertUriList(document, replacementRange, dataTransfer, token);
}
private async tryInsertUriList(document: vscode.TextDocument, replacementRange: vscode.Range, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.SnippetTextEdit | undefined> {
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
}
}
if (!uris.length) {
return;
}
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);
const ext = URI.Utils.extname(uri).toLowerCase();
snippet.appendText(imageFileExtensions.has(ext) ? '![' : '[');
snippet.appendTabstop();
snippet.appendText(`](${mdPath})`);
if (i <= uris.length - 1 && uris.length > 1) {
snippet.appendText(' ');
}
});
return new vscode.SnippetTextEdit(replacementRange, snippet);
return tryInsertUriList(document, replacementRange, dataTransfer, token);
}
});
}
export async function tryInsertUriList(document: vscode.TextDocument, replacementRange: vscode.Range, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.SnippetTextEdit | undefined> {
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
}
}
if (!uris.length) {
return;
}
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);
const ext = URI.Utils.extname(uri).toLowerCase();
snippet.appendText(imageFileExtensions.has(ext) ? '![' : '[');
snippet.appendTabstop();
snippet.appendText(`](${mdPath})`);
if (i <= uris.length - 1 && uris.length > 1) {
snippet.appendText(' ');
}
});
return new vscode.SnippetTextEdit(replacementRange, snippet);
}

View file

@ -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"
]
}

View file

@ -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<string>) {
this.set(mimeType, {
asString: async () => stringOrPromise,

View file

@ -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<string>(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;
}

View file

@ -721,6 +721,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<undefined | VSDataTransfer>;
provideDocumentPasteEdits(model: model.ITextModel, selection: Selection, dataTransfer: VSDataTransfer, token: CancellationToken): Promise<WorkspaceEdit | SnippetTextEdit | undefined>;
}
/**
* Represents a parameter of a callable-signature. A parameter can
* have a label and a doc-comment.

View file

@ -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>('ILanguageFeaturesService');
@ -25,6 +25,8 @@ export interface ILanguageFeaturesService {
readonly codeActionProvider: LanguageFeatureRegistry<CodeActionProvider>;
readonly documentPasteEditProvider: LanguageFeatureRegistry<DocumentPasteEditProvider>;
readonly renameProvider: LanguageFeatureRegistry<RenameProvider>;
readonly documentFormattingEditProvider: LanguageFeatureRegistry<DocumentFormattingEditProvider>;

View file

@ -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<DocumentRangeSemanticTokensProvider>(this._score.bind(this));
readonly documentSemanticTokensProvider = new LanguageFeatureRegistry<DocumentSemanticTokensProvider>(this._score.bind(this));
readonly documentOnDropEditProvider = new LanguageFeatureRegistry<DocumentOnDropEditProvider>(this._score.bind(this));
readonly documentPasteEditProvider = new LanguageFeatureRegistry<DocumentPasteEditProvider>(this._score.bind(this));
private _notebookTypeResolver?: NotebookInfoResolver;

View file

@ -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<IConfigurationRegistry>(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,
},
}
});

View file

@ -0,0 +1,195 @@
/*---------------------------------------------------------------------------------------------
* 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 { 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';
const vscodeClipboardMime = 'application/vnd.code.copyId';
class DefaultPasteEditProvider implements DocumentPasteEditProvider {
async provideDocumentPasteEdits(model: ITextModel, selection: Selection, dataTransfer: VSDataTransfer, _token: CancellationToken): Promise<WorkspaceEdit | undefined> {
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>(CopyPasteController.ID)!;
}
private readonly _editor: ICodeEditor;
private _currentClipboardItem: undefined | {
readonly handle: string;
readonly dataTransferPromise: CancelablePromise<VSDataTransfer>;
};
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 selection = this._editor.getSelection();
if (!e.clipboardData || !selection || !editor.hasModel()) {
return;
}
const model = editor.getModel();
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 tokenSource = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection);
try {
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, 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;
}
}
} finally {
tokenSource.dispose();
}
}, true));
}
public arePasteActionsEnabled(model: ITextModel): boolean {
return this._configurationService.getValue('editor.experimental.pasteActions.enabled', {
resource: model.uri
});
}
}

View file

@ -186,3 +186,4 @@ class DefaultOnDropProvider implements DocumentOnDropEditProvider {
registerEditorContribution(DropIntoEditorController.ID, DropIntoEditorController);

View file

@ -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';

View file

@ -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<VSDataTransfer | undefined> => {
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 {

View file

@ -457,6 +457,10 @@ 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 {
checkProposedApiEnabled(extension, 'documentPaste');
return extHostLanguageFeatures.registerDocumentPasteEditProvider(extension, checkSelector(selector), provider);
},
registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
},

View file

@ -372,6 +372,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;
@ -1730,6 +1731,8 @@ export interface ExtHostLanguageFeaturesShape {
$provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: languages.CodeActionContext, token: CancellationToken): Promise<ICodeActionListDto | undefined>;
$resolveCodeAction(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<IWorkspaceEditDto | undefined>;
$releaseCodeActions(handle: number, cacheId: number): void;
$prepareDocumentPaste(handle: number, uri: UriComponents, range: IRange, dataTransfer: DataTransferDTO, token: CancellationToken): Promise<DataTransferDTO | undefined>;
$providePasteEdits(handle: number, uri: UriComponents, range: IRange, dataTransfer: DataTransferDTO, token: CancellationToken): Promise<IWorkspaceEditDto | Dto<languages.SnippetTextEdit> | undefined>;
$provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: languages.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;
$provideDocumentRangeFormattingEdits(handle: number, resource: UriComponents, range: IRange, options: languages.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;
$provideOnTypeFormattingEdits(handle: number, resource: UriComponents, position: IPosition, ch: string, options: languages.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined>;

View file

@ -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<extHostProtocol.DataTransferDTO | undefined> {
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<undefined | extHostProtocol.IWorkspaceEditDto | Dto<languages.SnippetTextEdit>> {
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<extHostProtocol.DataTransferDTO | undefined> {
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<extHostProtocol.IWorkspaceEditDto | Dto<languages.SnippetTextEdit> | 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 {

View file

@ -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',

View file

@ -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<void>;
/**
* 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<WorkspaceEdit | SnippetTextEdit>;
}
namespace languages {
export function registerDocumentPasteEditProvider(selector: DocumentSelector, provider: DocumentPasteEditProvider): Disposable;
}
}