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 events for copy/paste to allow language extensions to bring using/import statements along #30066

Open
DanTup opened this issue Jul 3, 2017 · 35 comments
Assignees
Labels
api api-proposal editor-code-actions Editor inplace actions (Ctrl + .) feature-request Request for new features or functionality
Milestone

Comments

@DanTup
Copy link
Contributor

DanTup commented Jul 3, 2017

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
Copy link
Contributor Author

DanTup commented Jul 6, 2017

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
Copy link
Collaborator

mjbvz commented Sep 18, 2020

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

  • Multiple different extensions would likely want to offer this functionality.
  • Implementing it requires state management. Store state on copy. Retrieve it on paste.
  • An extension can not precompute the text edits for this and cannot apply them synchronously.
  • On paste, an extension many want to make text edits outside of the paste text (such as adding imports at the top of a file).
  • On paste, an extension may want to make text edits to the pasted text. For example, consider copying a block of JS that uses alias imports and then pasting that into a file that imports the same file as a namespace. We'd want to convert the alias imports in the pasted text to use namespace imports.

Some goals:

  • Simple VS Code setting to control paste behavior
  • Avoid text flickers (where you paste and then the text you just pasted is rewritten a 200ms later)
  • Still let users do a basic paste (without triggering any actions)
  • Don't let misbehaving extensions block paste (have some sort of time box on paste)

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:

  • Extensions are driving the behavior (not VS Code)
  • No unified setting to control this behavior (each extension would likely bring along its own)

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
Copy link
Member

jrieken commented Sep 18, 2020

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
Copy link
Contributor Author

DanTup commented Sep 18, 2020

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
Copy link
Collaborator

mjbvz commented Sep 18, 2020

@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

@mjbvz mjbvz modified the milestones: Backlog, September 2020 Sep 18, 2020
@DanTup
Copy link
Contributor Author

DanTup commented Sep 19, 2020

@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
Copy link

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
Copy link
Member

jrieken commented Sep 21, 2020

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
Copy link
Collaborator

mjbvz commented Sep 22, 2020

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 :

  • Save arbitrary, asynchronously resolved data to the clipboard
  • Restore that data on paste
  • Prevent the default copy/paste if needed

@jrieken
Copy link
Member

jrieken commented Sep 22, 2020

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
Copy link
Collaborator

mjbvz commented Sep 22, 2020

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 added a commit to mjbvz/vscode that referenced this issue May 9, 2023
For microsoft#30066

This command shows a quick pick that lets you select how to paste content
mjbvz added a commit that referenced this issue May 10, 2023
For #30066

This command shows a quick pick that lets you select how to paste content
mjbvz added a commit that referenced this issue Aug 8, 2023
For #179430, #30066

Switching to use `yieldTo` instead of `priority` to let an extension de-rank itself in the list of edits. `priority` was an arbitrary number while `yieldTo` gives more control over how the ranking takes place
mjbvz added a commit that referenced this issue Aug 8, 2023
Move await from `priority` for drop/paste API proposals

For #179430, #30066

Switching to use `yieldTo` instead of `priority` to let an extension de-rank itself in the list of edits. `priority` was an arbitrary number while `yieldTo` gives more control over how the ranking takes place
mjbvz added a commit to mjbvz/vscode that referenced this issue Aug 8, 2023
For microsoft#179430, microsoft#30066

This lets us call just the provider we are interested in
mjbvz added a commit that referenced this issue Aug 8, 2023
For #179430, #30066

This lets us call just the provider we are interested in
@lukeapage
Copy link

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
Copy link

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

This comment has been minimized.

@fbricon
Copy link
Contributor

fbricon commented Jul 8, 2024

Any ETA on finalizing this API?

hitesh-1997 added a commit to sourcegraph/cody that referenced this issue Sep 24, 2024
# Context
The PR adds three major context sources useful for autocomplete and
next-edit-suggestion.

### Recent Copy retriever.

Developers often copy/cut and paste context and before pasting we show
autocomplete suggestions to the user. The PR leverages the copied
content on the clipboard by the user as a context source.
I wasn't able to find any vscode api event exposed which triggers when
user `copy` or `cut` text in the editor. This seems like an open issue:
microsoft/vscode#30066

Another alternative I think of is to use a keybinding of `Ctrl+x` and
`Ctrl+c`, but not fully sure about its implications, since this is one
of the most common shortcuts.

As a workaround, The way current PR accomplishes the same is:
1. Tracks the selection made by the user in the last `1 minutes` and
keeps tracks of upto `100` most recent selections.
3. At the time of retrieval, checks if the current clipboard content
matches the selection items in the list.

### Recent View Ports
This context source captures and utilizes the recently viewed portions
of code in the editor. It keeps track of the visible areas of code that
the developer has scrolled through or focused on within a specified time
frame.

### Diagnostics 
The Diagnostics context source leverages the diagnostic information
provided by VS Code and language servers. It collects and utilizes
information about errors, for a file as a context source.

## Test plan
1. Automated test 
- Added CI tests for each of the retrievers

2. Manual test
- Override the setting `"cody.autocomplete.experimental.graphContext":
"recent-copy"` in vscode settings.
- Observe the context events using `Autocomplete Trace View`
mjbvz added a commit to mjbvz/vscode that referenced this issue Nov 4, 2024
…default

Fixes microsoft#184871
For microsoft#30066

Adds new settings that let you configure the default way to paste/drop.

Also enables js/ts paste with imports by default for 5.7+. However will not apply by default. Instead it will be shown as an option after pasting. You can then use the `editor.pasteAs.preferences` setting to make it apply automatically or use the `javascript.updateImportsOnPaste.enabled` settings to disable the feature entirely
mjbvz added a commit that referenced this issue Nov 5, 2024
…default (#233031)

Fixes #184871
For #30066

Adds new settings that let you configure the default way to paste/drop.

Also enables js/ts paste with imports by default for 5.7+. However will not apply by default. Instead it will be shown as an option after pasting. You can then use the `editor.pasteAs.preferences` setting to make it apply automatically or use the `javascript.updateImportsOnPaste.enabled` settings to disable the feature entirely
@mjbvz mjbvz modified the milestones: On Deck, November 2024 Nov 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api api-proposal editor-code-actions Editor inplace actions (Ctrl + .) feature-request Request for new features or functionality
Projects
None yet
Development

No branches or pull requests