-
Notifications
You must be signed in to change notification settings - Fork 888
Description
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
-
More complete example showing this API in used to update code on paste for markdown (extension side and server side)
-
Video showing the whole update imports on paste flow in action for JS/TS in VS Code
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 fromprepareDocumentPaste
. We then invokeprovidePasteEdits
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 toadditionalEdits
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