Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add copy + paste edit provider #2072

Open
mjbvz opened this issue Dec 12, 2024 · 0 comments
Open

Add copy + paste edit provider #2072

mjbvz opened this issue Dec 12, 2024 · 0 comments
Labels
feature-request Request for new features or functionality new request
Milestone

Comments

@mjbvz
Copy link
Contributor

mjbvz commented Dec 12, 2024

The idea of letting the lsp be involved in copy/paste was first raised in #767. I'm opening a new issue to track a specific proposal of how this could be implement based on the VS Code document paste API

Overview

VS Code has a long standing proposed API that allows languages hook into copy and paste. This is already being used successfully for a number of flows:

  • Copying and pasting to bring along imports in JS/TS and in Markdown
  • Pasting files to insert relative or absolute paths
  • Pasting files to insert markdown links (also supports copying resources into the workspace)
  • Pasting text that looks like a url to insert a markdown link
  • Pasting files add url(...) in css files

This type of functionality is likely useful across many different editors. The VS Code paste API also has a few characteristics that I think it make well suited to the LSP:

  • Declarative: The api is similar to the code action API in that languages do not directly edit the document using it. On paste, a language returns edit objects. These edit objects include human readable descriptions of the edit that can be shown in the UI along with a workspace edit that can be applied by the editor

  • Abstract: The api uses simple data transfer objects to attach data on copy and read data on paste. This data transfer object was inspired by the dom DataTransfer API, but is abstract enough and simple enough that I think almost any editor could implement it

  • Driven by editors: We use a provider pattern instead of events, so the editor is ultimately responsible for deciding when the call out on copy or paste. Again this is very similar to how code actions are implemented

Relevant links

Proposal

This proposal is based on the current VS Code API document paste proposal

Basic flow

  • On copy, we invoke prepareDocumentPaste. This returns a modified data transfer object. This is an asynchronous operation and should not block copying in the UI

  • On paste, we make sure any relevant prepareDocumentPaste calls have completed. We then create a new data transfer by merging the data transfer provided by the editor with the data transfers from prepareDocumentPaste. We then invoke providePasteEdits which returns a set of zero or more edits that can apply the paste

  • The editor gets back a list of possible edits from all providers. It can then decided what to do with them. For VS Code, we always apply the first edit and then let users switch to any other edits provided. However an editor could instead always show a selector on paste

  • Once a paste edit needs to be applied, we call resolvePasteEdit. This extra step allows languages to defer the potentially expensive work of computing the actual edit to apply

Capability

  • property path (optional): documentPasteEditProvider
  • property type: DocumentPasteOptions
export interface DocumentPasteOptions extends WorkDoneProgressOptions {

    // List of mime types that may be added on copy
    // If not provided, will not be invoked on copy
    copyMimeTypes?: string[];

    // List of edit kinds that paste may return
    // If not provided, this provided will not be invoked on paste
    providedPasteEditKinds?: string[]
       
    // List of mime types that this provided will be invoked for on paste
    pasteMimeTypes?: string[]

    // If set, the provided can support an additional resolve step to fill in edit details
    supportsResolve?: boolean
}

Prepare paste

This is invoked on copy. You can use it to attach data that will be picked up on paste

Request:

  • method: documentPaste/preparePaste
  • params: PreparePasteParams
interface PreparePasteParams {
    // The document where the copy took place in
    textDocument: TextDocumentIdentifier;

    // The ranges being copied in the document
    ranges: Range[];

    // The data transfer associated with the copy
    dataTransfer: DataTransfer;

}
interface DataTransfer {
    // A map of mime types to their corresponding data
    items: { [mimeType: string]: string };
}

Response:

  • result: DataTransfer The additions to the data transfer. (TODO: should this allow deleting something from the incoming data transfer?)

Provide paste edits

This is invoked before the user pastes into a text editor. Returned edits can replace the standard pasting behavior.

Request:

  • method: documentPaste/providePasteEdits
  • params: ProvidePasteEditsParams
interface ProvidePasteEditsParams {
    // The document being pasted into
    textDocument: TextDocumentIdentifier;

    // The ranges in the document to paste into
    ranges: Range[];

    // The data transfer associated with the paste
    dataTransfer: DataTransfer;

    // Additional context for the paste
    context: DocumentPasteEditContext;
}

interface DocumentPasteEditContext {
    // Requested kind of paste edits to return
    only?: string[];

    // The reason why paste edits were requested
    triggerKind: DocumentPasteTriggerKind;
}

enum DocumentPasteTriggerKind {
    Automatic = 0,
    PasteAs = 1,
}

Response:

  • result: DocumentPasteEdit[]
interface DocumentPasteEdit {
    // Human readable label that describes the edit
    title: string;

    // Kind of the edit
    kind: string;

    // The text or snippet to insert at the pasted locations
    insertText: string | SnippetString;

    // An optional additional edit to apply on paste
    additionalEdit?: WorkspaceEdit;

    // Controls ordering when multiple paste edits can potentially be applied
    yieldTo?: string[];
}

ResolvePaste edit

This is an optional method which fills in DocumentPasteEdit .additionalEdit before the edit is applied. This is useful

  • Request:
    method: 'documentPaste/resolvePasteEdit'
    params: ResolvePasteEditParams
interface ResolvePasteEditParams {
    // The paste edit to resolve
    pasteEdit: DocumentPasteEdit;
}

Response:

  • result: DocumentPasteEdit The filled in paste edit. Only changes to additionalEdits should be respected for now

Other notes

Drag and drop

Copy and pasting is very similar to draging and dropping content into an editor. In VS Code we implement these features using separate APIs however the APIs are almost identical

I have somewhat mixed feelings about this. Having separate APIs lets languages customize the behavior for each flow. It also lets the two apis evolve independently. However having two apis means duplicated implementation code and a lot of duplicated API/protocol

Files

In VS Code, the DataTransfer object also supports files/binary data. This is an important use case but also adds complexity

The main concern is that we don't want to transfer around the file data util it is needed. A single video file may be 10-100s of MBs which we really don't want to send across the wire, especially if all the paste provider does is say that the file should be renamed or moved

That means that reading the file content needs to be an asynchronous operation. For reference, here's what VS Code's data transfer object looks like:

export interface DataTransferFile {
	/**
	 * The name of the file.
	 */
	readonly name: string;

	/**
	 * The full file path of the file.
	 *
	 * May be `undefined` on web.
	 */
	readonly uri?: Uri;

	/**
	 * Get the full file contents of the file.
	 */
	data(): Thenable<Uint8Array>;
}

/**
 * Encapsulates data transferred during drag and drop operations.
 */
export class DataTransferItem {
	/**
	 * Get a string representation of this item.
	 *
	 * If {@linkcode DataTransferItem.value} is an object, this returns the result of json stringifying {@linkcode DataTransferItem.value} value.
	 */
	asString(): Thenable<string>;

	/**
	 * Try getting the {@link DataTransferFile file} associated with this data transfer item.
	 *
	 * Note that the file object is only valid for the scope of the drag and drop operation.
	 *
	 * @returns The file for the data transfer or `undefined` if the item is either not a file or the
	 * file data cannot be accessed.
	 */
	asFile(): DataTransferFile | undefined;

	/**
	 * Custom data stored on this item.
	 *
	 * You can use `value` to share data across operations. The original object can be retrieved so long as the extension that
	 * created the `DataTransferItem` runs in the same extension host.
	 */
	readonly value: any;

	/**
	 * @param value Custom data stored on this item. Can be retrieved using {@linkcode DataTransferItem.value}.
	 */
	constructor(value: any);
}

/**
 * A map containing a mapping of the mime type of the corresponding transferred data.
 *
 * Drag and drop controllers that implement {@link TreeDragAndDropController.handleDrag `handleDrag`} can add additional mime types to the
 * data transfer. These additional mime types will only be included in the `handleDrop` when the the drag was initiated from
 * an element in the same drag and drop controller.
 */
export class DataTransfer implements Iterable<[mimeType: string, item: DataTransferItem]> {
	/**
	 * Retrieves the data transfer item for a given mime type.
	 *
	 * @param mimeType The mime type to get the data transfer item for, such as `text/plain` or `image/png`.
	 * Mimes type look ups are case-insensitive.
	 *
	 * Special mime types:
	 * - `text/uri-list` — A string with `toString()`ed Uris separated by `\r\n`. To specify a cursor position in the file,
	 * set the Uri's fragment to `L3,5`, where 3 is the line number and 5 is the column number.
	 */
	get(mimeType: string): DataTransferItem | undefined;

	/**
	 * Sets a mime type to data transfer item mapping.
	 *
	 * @param mimeType The mime type to set the data for. Mimes types stored in lower case, with case-insensitive looks up.
	 * @param value The data transfer item for the given mime type.
	 */
	set(mimeType: string, value: DataTransferItem): void;
}

You can we see also made asString() async, which also allows deferring transferring any normal clipboard data until it is needed. Another important note is that extensions in VS Code cannot create file data transfer objects

My current proposal doesn't have the data transfer be asynchronous but I think we should strong consider doing this. A very basic client could get away with always transferring the full clipboard contents, with proper lazy loading being implemented as a future optimization

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-request Request for new features or functionality new request
Projects
None yet
Development

No branches or pull requests

2 participants