microsoft / vscode

Visual Studio Code
https://code.visualstudio.com
MIT License
164.95k stars 29.52k forks source link

Add events for copy/paste to allow language extensions to bring using/import statements along #30066

Open DanTup opened 7 years ago

DanTup commented 7 years ago

Apologies if this already exists, I can't seem to find anything about it (I expected it'd be listed in complex commands if anywhere).

The language service I use for Dart is getting a new feature that will allow me to send a command to get data about imports/using statements that apply to a subset of code. It's designed to be run when a user copies a chunk of code to the clipboard. When pasting, this data can be exchanged for a bunch of edits that would add the equivalent imports in the new file to avoid the user ending up having to fire a bunch of "import xxx" code actions after pasting.

In order to do this I need to know when the user has copied something into their clipboard (including the file and offset/range) and also again after they have pasted (just the file) so I can add some additional edits.

DanTup commented 7 years ago

It would actually work out better if we could also attach additional data into the clipboard during copy (to avoid there being a mismatch between the import data we stashed and the clipboard, which might have been replaced externally since) and get it back during paste; but I don't know enough about how clipboards work to know how feasible that is!

mjbvz commented 4 years ago

Having copy/paste bring along imports is a good motivating use case for designing this api. Here is some of what makes it interesting:

Some goals:

Sketches

(note that these are not API proposals, just quick ideas on how this could work)

Source Actions

Try to reuse source actions for implement an editor.codeActionsOnPaste setting. For example, the setting:

"editor.codeActionsOnPaste": [
    "source.organizeImports"
]

Would run organize imports source action whenever the user pastes.

I don't see this as being enough on its own, except for super simple use cases such as organize imports. We need some way for extensions to store state from a copy and use that to produce a set of text edits on paste. Source actions don't provide enough infrastructure on their own

Extend clipboard api

Let extensions be notified of clipboard copy and paste events. Let extensions store metadata on the clipboard.

vscode.env.clipboard.onCopy(async entry => {
     const metadata = await getMetadataForSelection();

     entry.addMetadata('myExtension', metadata);
});

vscode.env.clipboard.onPaste(async entry => {
     const metadata = entry.getMetadata('myExtension';

     if (metadata) {
            // convert metadata into text edits and apply these
     }
});

This leaves the implementation largely up to individual extensions. There are a few big open questions here too:

Paste Action Provider

Define a contract that extensions can implement to hook into copy/paste. This basically just adds a contract on top of the clipboard api sketch above:

class AddImportsOnPasteProvider implements vscode.PasteActionProvider {

    async providePasteActions(document: vsocode.TextDocument, copiedRange: vscode.Range): Promise<vscode.PasteAction[]> {

         // Get metadata about copied text
         const metadata = await getMetadataForSelection(document, copiedRange);

         // Return command that VS Code applies on paste
         return [{
             id: 'typescript.moveImports',
             command: <vscode.Command>{
                 title: 'Move Imports',
                 command: 'typescript.doMoveImports',
                 args: [metadata]
             }
         }];
    }
}

Instead of using commands, we could have have an applyPaste method on vscode.PasteActionProvider that would be invoked a paste.

jrieken commented 4 years ago

So, the extended proposed is somewhat analogue to format on save which happens automatically and the onWillSave-event for less uniform cases, right?

A critical piece in this would be those clipboard events. E.g. when copying from a website and pasting into the editor, then I don't believe that we have a chance to know that copy has happened. Not even sure if we know that inside the editor, e.g when copy/pasting between two files...

DanTup commented 4 years ago

If I understand correctly, in the "Paste Action Provider" example it looks like the metadata is collected during the paste operation:

// Get metadata about copied text
const metadata = await getMetadataForSelection(document, copiedRange);

I think that would fall down if a user copies text, the modifies it/deletes it/hits save (which formats). I think the metadata would have to be collected during the copy to be reliable (so the provider would probably need to have hooks for both copy and paste?).

Is this something that could also be supported for LSP? (if so, it may be worth considering how the API would translate to that).

mjbvz commented 4 years ago

@jrieken I originally thought this issue would be as simple as adding an editor.codeActionsOnPaste setting, but it turns out that that would not handle most of the interesting API use cases. For a use case such as moving imports around with copy/paste, the key part is that the operation is stateful. We can either leave managing this state up to extensions or go with a more provider based approach that allows VS Code to manage this state.

We can probably implement these concepts on desktop but I'm not sure about web. I think we should be able to use the copy and paste events, but these may trigger permissions dialogs.

@DanTup The extension would need to save off the metadata in such a way that, on paste, the extension can handle any changes that happened since the copy

A provider based approach is a better fit for the LSP. The goal is to implement it in VS Code first, then add it to the LSP once it has been stabilized

DanTup commented 4 years ago

@DanTup The extension would need to save off the metadata in such a way that, on paste, the extension can handle any changes that happened since the copy

Yep, that's what I have in mind - but it needs the Copy event to do this too (the provider example above doesn't seem sufficient). My expectation is that when you Copy, the extension would collect a list of all imports required for that code, then when you paste, it would generate the correct code to import any that are not already imported in the new file (and account for relative paths). That shouldn't be affected by any changes happening in between.

testforstephen commented 4 years ago

Another use case for copy/paste is auto-escape string on paste. For example, pasting some text to a Java string literal, auto escape the characters such as \," etc.

The current PasteActionProvider seems to only consider copy/paste between editors. if copying a piece of content from outside (for example, copy a file path from File Explorer), is there a way to make the extension participate in the paste actions?

jrieken commented 4 years ago

We can probably implement these concepts on desktop but I'm not sure about web. I think we should be able to use the copy and paste events, but these may trigger permissions dialogs.

Didn't know about those events. Seems to be working for desktop and web. Also, us moving to a node-free-renderer means that it should really work with web. It seems tho that they don't work with navigator.clipboard (which we use to implement the clipboard API) but only with copy/paste that was initiated from a user gesture. Needs maybe a little investigation but looks promising

mjbvz commented 4 years ago

Definitely some limitations with copy and paste events but here's what I've come up with for the implementation:

// @ts-check

/**
 * Use this to save off a async resolved clipboard entry.
 *
 * In the example we resolve this eagerly but you could also resolve it lazily instead
 * 
 * @type {{ handle: string, value?: string } | undefined}
 */
let clipboardItem;

document.addEventListener('copy', e => {
    const handle = '' + Date.now();

    // Save off a handle pointing to data that VS Code maintains.
    e.clipboardData.setData('x-vscode/id', handle);
    clipboardItem = { handle: handle }

    // Simulate the extension resolving the clipboard data asynchronously  
    setTimeout(() => {
        // Make sure we are still on the same copy
        if (clipboardItem?.handle === handle) {
            clipboardItem.value = 'my custom value'
        }
    }, 500);

    // Call prevent default to prevent out new clipboard data from being overwritten (is this really required?)
    e.preventDefault();

    // And then fill in raw text again since we prevented default
    e.clipboardData.setData('text/plain', document.getSelection()?.toString() ?? '');
});

document.addEventListener('paste', e => {
    // Check to see if the copy for this paste came from VS Code
    const id = e.clipboardData.getData('x-vscode/id');

    // If it did, make sure our clipboard data still belongs to the copy that generated it.
    if (id === clipboardItem?.handle) {
        const value = clipboardItem.value;

        // Handle the case where the clipboard has not been resolved yet
        if (typeof value === 'undefined') {
            // Reset
            clipboardItem = undefined;

            // Note that we could wait on a Promise or do something else here...
        } else {

            // Our clipboard item has resolved and is still revevant!
            e.preventDefault();

            // Modify the document based on it
            /** @type {HTMLTextAreaElement | undefined} */
            const element = e.target;

            const selectionStart = element.selectionStart || 0;
            const selectionEnd = element.selectionEnd || 0;

            element.value = `${element.value.substring(0, selectionStart)}${value}${element.value.substring(selectionEnd, element.value.length)}`;
            element.selectionStart = selectionStart + value.length;
            element.selectionEnd = element.selectionStart;
        }
    }
})

This shows that we should be able to :

jrieken commented 4 years ago

What concerns me is that this flow only works if you copy from within the editor. No treatment when copying from external sources like stackoverflow - which is likely in more need of post-paste-cleanup.

mjbvz commented 4 years ago

Yes this was just an experiment to see if I could implement what this api will require.

For the next step, I'm going to try implementing a VS Code API that is closer to this:

interface CopyPasteActionProvider<T = unknown> {

    /**
     * Optional method invoked after the user copies some text in a file.
     * 
     * @param document Document where the copy took place.
     * @param selection Selection being copied in the `document`
     * @param clipboard Information about the clipboard state after the copy.
     * 
     * @return Optional metadata that is passed to `onWillPaste`.
     */
    onDidCopy?(
        document: vscode.TextDocument,
        selection: vscode.Selection,
        clipboard: { readonly text: string },
    ): Promise<{ readonly data: T } | undefined>;

    /**
     * Invoked before the user pastes into a document.
     * 
     * @param document Document being pasted into
     * @param selection Current selection in the document.
     * @param clipboard Information about the clipboard state. This may contain the metadata from `onDidCopy`.
     * 
     * @return Optional workspace edit that applies the paste (TODO: right now always requires implementer to also implement basic paste)
     */
    onWillPaste(
        document: vscode.TextDocument,
        selection: vscode.Selection,
        clipboard: {
            readonly text: string;
            readonly data?: T;
        },
    ): Promise<WorkspaceEdit | undefined>;
}

interface CopyPasteActionProviderMetadata {
    /**
     * Identifies the type of paste action being returned, such as `moveImports`. (maybe this should just be a simple string)
     */
    readonly kind: CodeActionKind; 
}

function registerCopyPasteActionProvider(
    selector: vscode.DocumentSelector,
    provider: CopyPasteActionProvider,
    metadata: CopyPasteActionProviderMetadata
): Disposable;
mjbvz commented 4 years ago

107283 Adds a draft API and implementation. It is not ready to be checked in for this iteration as it is still very experimental.

Here's a few more questions that have come up during discussions so far:

We don't want to try boiling the ocean with this issue, but also need to consider these other use cases/possible future extensions to the VS Code api.


@jrieken Next iteration, let's also discuss how pasting should behave. Some open questions:


@alexdima We will also need to talk about how to properly implement of this API. At the moment, I believe we may need to add new copy/paste hooks to the Monaco editor. I also know you have experience implementing VS Code's default copy and paste behavior, and want to see if you have suggestions/feedback on the API proposal

jrieken commented 4 years ago

Wrt the API proposal I was wondering if it makes more sense to expose those hooks as simple events. Now it is a provider which doesn't use strict provider language (like XYZProvider -> provideXYZ). So, we could have the same things exposed as vscode.window.onWillPaste and onDidCopy (maybe with a EditorText suffix to set expectations)

mjbvz commented 4 years ago

@jrieken I think that would work for an onDidCopy event that lets extensions update the clipboard.

However I do think we want VS Code to drive what happens on paste. A few reasons:

jrieken commented 4 years ago

Hm, this is very similar to onWillSaveTextDocument which allows extension participation and we have tackled those questions. E.g extensions (event handler) have a fixed time budget, can only error a few times, must use the waitUntil-api etc.

I don't think the implementation is any different between those two approaches but I think having them exposed as events is the better fit given how the current proposal looks. The CopyPasteActionProvider is not a provider'ish

dbaeumer commented 3 years ago

This came up lately in LSP as well and from my experience implementing this for languages in the past I think there are more things to consider

So I think we need to define whether copy / paste should try to be semantic preserving to the pasted code or whether we want to make the code work in the pasted location. I personally tend to implement the second approach.

Implementation wise having a onWillPaste puts quite some implementation complexity onto the server. To really answer the questions of fixing imports and making the code work the server needs to create a working copy of the file, paste in the code, build an AST and run a type binding phase. All of this shouldn't have any side effect onto other code (e.g. no diagnostics should be created in another file).

I see the point of flickering when we first paste and then do the fixing but it might not be so bad since:

So for me the captured imports on copy would only act as a hint in case the pasted code result in errors in that location.

alexdima commented 3 years ago

Strictly from the editor's point of view, making copy or paste async (in order to allow for async participants) might not be feasible. In the web, we get copy/paste browser events and we have access to the clipboard data transfer object only in the event handlers themselves.

Making things async would mean we would always have to go through navigator.clipboard which prompts to the user (in the editor this is only used when right click > Paste is used, which is in my experience pretty rare compared to ctrl+v). When using ctrl+c/ctrl+x/ctrl+v, the editor does not prompt for permissions. Also, navigator.clipboard is not really supported in all browsers entirely, for example it doesn't work to read from the clipboard in Firefox (that's why we don't have right click > Paste in FF).

That being said, we already store some metadata when copying to the clipboard. For example, we remember if the copy happened with multiple cursors or not in order to behave differently when pasting. We use a special clipboard mimetype vscode-editor-data (i.e. different than plaintext and text/html). When pasting, we can then read this metadata again. As part of this metadata, we could potentially store the URI and the range where the copy or cut event occurred in case this might be useful when pasting.

Also, making pasting async might become problematic w.r.t. timing and user expectation. What should happen when a new key press occurs after a paste and onWillPaste still hasn't returned. Should there be a time limit enforced? What if the time limit is reached? Is it possible to manually execute the "post paste massage". e.g. when the Save participant fails today, it is usually possible to manually invoke Format Document or Quick Fix to get the save participant to execute. What about this "paste massager", how should this be exposed directly?

DanTup commented 3 years ago

This came up lately in LSP as well and from my experience implementing this for languages in the past I think there are more things to consider

capturing the imports on copy is not enough. Besides the fact that you might copy from another source the imports might not make any sense in the new paste location.

This sounds fairly server-implementation-specific and not something the editor should be concerned with. My intention wasn't just to copy the imports, but to delegate to the language server (which has a specific feature for producing metadata during a copy, and then producing edits given back that metadata during a paste).

When pasting, we can then read this metadata again. As part of this metadata, we could potentially store the URI and the range where the copy or cut event occurred in case this might be useful when pasting.

Would this be in addition to a copy event? To me, having access to this data at paste time seems a bit useless - the user could have copied the text and then deleted the file. Metadata about the imports needs to be gathered at copy time (and is something the language server needs to be involved in).

Also, making pasting async might become problematic w.r.t. timing and user expectation. What should happen when a new key press occurs after a paste and onWillPaste still hasn't returned.

If the worst case is that in this scenario the additional edits are completely lost and can't be re-applied, I don't think that's a deal breaker. You can always hit undo and then re-paste, without typing in between. I think users would much rather have this feature with limitations like that than not be able to have it at all.

alexdima commented 3 years ago

@DanTup In the first two paragraphs I attempt to reason why copy itself cannot be made async. Maybe we could add an onDidCopy that would execute immediately after the copy event is dispatched by the browser and handled by the editor. The language server would have a chance to compute and return some piece of data. We would store that piece of data until the next paste comes in, and if we know it was a paste from that specific copy event, we can match things up again. But unless we get some help from browsers, the data itself cannot be stored in the OS clipboard, only some identifier which can be used to correlate things.

If the worst case is that in this scenario the additional edits are completely lost and can't be re-applied, I don't think that's a deal breaker. You can always hit undo and then re-paste, without typing in between. I think users would much rather have this feature with limitations like that than not be able to have it at all.

I think a better strategy would be to give up on waiting for text edits from the language server and simply paste the text without the imports.

DanTup commented 3 years ago

In the first two paragraphs I attempt to reason why copy itself cannot be made async. Maybe we could add an onDidCopy that would execute immediately after the copy event is dispatched by the browser and handled by the editor. The language server would have a chance to compute and return some piece of data.

Yep, understood. I just wanted to highlight the importance of having a copy event of some sort (and that trying to make up for it only with paste would not work reliably). I think if there are caveats (again, like if the user modifies the document quickly, the results are invalid/discarded) that's entirely acceptable. Being able to bring imports >95% of the time is a significant important over 0%.

But unless we get some help from browsers, the data itself cannot be stored in the OS clipboard, only some identifier which can be used to correlate things.

Oh, I see - I thought the metadata was being suggested for use by the extension, but if you mean so VS Code can use it to locate which "extension data" corresponds to that (because it wasn't available at the time the clipboard was populated), that makes sense. I was reading entirely as an extension author and not considering the internal implementation :-)

I think a better strategy would be to give up on waiting for text edits from the language server and simply paste the text without the imports.

Yep, that's kinda what I meant - although I'd assumed the original text would be immediately pasted, and then the servers edits to insert imports (and potentially prefixes for type names in the pasted code, if required to avoid conflicts) applied afterwards. I think I'd personally prefer to see the text appear immediately and then an update fixing up the imports/prefixes than paste feel slow (a delay in pasting might lead to people hitting Ctrl+C again).

333fred commented 3 years ago

I see a number of comments in this issue specifically focusing on imports as a reason for extra text edits to be applied after a paste event, and I just wanted to make sure that we're not considering that as the only reason why a user might want to have an on-paste handler. It might actually format the code in question, which causes other lines in the code to need to be reformatted. Or you could have a handler that, if you paste content into a string, does some form of character escaping for nested quote marks as another example. I admit I have trouble reasoning about an API that takes a paste edit and returns a possible series of edits. Does this need to include the original edit, or does it need to include an edit that matches the exact span of the original edit, with some other additional edits that can touch adjacent locations? Where does the cursor end up after? IMO the API would be significantly easier to implement (as a language server providing the handler) as a "here's the document that was just pasted into and here's the range of the text that was pasted in", and that's the approach I took with my LSP spec proposal: https://github.com/microsoft/vscode-languageserver-node/pull/736.

Maybe we could add an onDidCopy that would execute immediately after the copy event is dispatched by the browser and handled by the editor. The language server would have a chance to compute and return some piece of data. We would store that piece of data until the next paste comes in, and if we know it was a paste from that specific copy event, we can match things up again.

I don't think this is a workable solution. Languages have many different ways of formatting import statements, even within a single language. You could copy/paste from one file without an alias to another file with an alias, and a smart formatter would want to be able to use those aliases in the pasted code. It's too dependent on the pasted-into context to be resolvable up front.

mjbvz commented 3 years ago

@dbaeumer The current api proposal flows looks something like:

  1. User copies text

  2. The extension is notified that a copy has happened. The extension generates an opaque blob of data that contains all the information to generate the imports on paste.

    With TypeScript for example, this blob would include the symbols and the files the came from. The important part is that the extension does not have to generate text to be pasted during this phase.

  3. The user pastes. Here the extension gets the opaque blob back and generate a workspace edit describe the text edit.

The extension would be responsible for handling cases such as: copy/paste between different projects, file changes between copy/paste, and so on.

I can talk with the TS team about onWillPaste vs onDidPaste to see if one would be easier to implement


@alexdima To workaround the async issue, I currently:

  1. When a copy happens, store a reference id in the clipboard. Let the copy event finish synchronously.

  2. Asynchronously go out to the extension to resolve the actual data to store. Map this to the reference id once we have it

  3. When a paste happens, look up the data to use based on the reference id in the clipboard. Using this, let the extension handle the rest of the paste

333fred commented 3 years ago

I can talk with the TS team about onWillPaste vs onDidPaste to see if one would be easier to implement

For reference of an existing implementation, I want this to be able to expose the same Roslyn functionality that already exists in VS (http://sourceroslyn.io/#Microsoft.CodeAnalysis.EditorFeatures/Implementation/Formatting/IEditorFormattingService.cs,35), which is structured as a onDidPaste.

dbaeumer commented 3 years ago

@mjbvz Regarding

With TypeScript for example, this blob would include the symbols and the files the came from. The important part is that the extension does not have to generate text to be pasted during this phase.

I don't think that this is the expensive part. The expensive part IMO is to find out whether these imports still make sense at the pasted location. IMO we can't insert them blindly.

pouyarezvani commented 3 years ago

@mjbvz @DanTup, I know you guys are hard at work! but could we possibly get any updates on this feature please? Thanks!

badaszszsz commented 2 years ago

~How I could test this out against https://github.com/Dart-Code/Dart-Code/issues/351 ? Should I compile VSCode myself, and then "add" Dart-Code somehow do my VSCode? Thanks in advance for any help.~ DanTup explained me on https://github.com/Dart-Code/Dart-Code/issues/351 and here - forget my comment ☺️

DanTup commented 2 years ago

@badaszszsz I added some notes on https://github.com/Dart-Code/Dart-Code/issues/351. You wouldn't need to recompile VS Code, you can use the Insiders version to test out proposed APIs. However Dart uses LSP, so it would be better to wait for this to become part of LSP before implementing it in the Dart server.

akaroml commented 2 years ago

This PR redhat-developer/vscode-java#2703 makes use of the new API, to automatically format pasted content when pasting inside a string literal. The API is working well in the insider's build. The final release of this feature depends on the stable release of the API. We also plan to further adopt this API to automatically import missing types while pasting. Please help make the API final ASAP.

nedgar commented 2 years ago

Hi ~Eclipse~ VS Code friends! Long time no speak! In my current job, I'm using some VS Code development processes and git commit / issue / PR hygiene as good examples of best practices, to share with our dev team. I really like this issue for several reasons:

Kai, @kieferrm I was wondering if you'd be able to give a brief summary of how this has been carried from milestone to milestone, and whether it's possible to reconstruct what was done for each. I see some linked commits, but no linked PRs. Was this broken down into sub-issues?

Kaiyusa commented 1 year ago

O don't understand this... Can someone translate it for me please ,💰

fisforfaheem commented 1 year ago

Much needed as of now

lukeapage commented 1 year ago

I saw this appear in the typescript 5.3 iteration plan..

Since no-one has mentioned it, I use a extension that does this already: "Copy With Imports" - id: stringham.copy-with-imports

Its a awesome time saver. I particularly like that copy pasting a exported symbol will add a import to that export.

tooltitude-support commented 10 months ago

There was a mention of markdown paste link in the latest release of vscode, which implemented through this API. Are there any plans to stabilize it soon?

fisforfaheem commented 8 months ago

any updates

fbricon commented 4 months ago

Any ETA on finalizing this API?