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
Comments
|
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! |
|
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 ActionsTry to reuse source actions for implement an "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 apiLet 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 ProviderDefine 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 |
|
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... |
|
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). |
|
@jrieken I originally thought this issue would be as simple as adding an We can probably implement these concepts on desktop but I'm not sure about web. I think we should be able to use the @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 |
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. |
|
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 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? |
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 |
|
Definitely some limitations with // @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 :
|
|
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. |
|
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; |
|
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 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. |
|
Strictly from the editor's point of view, making Making things async would mean we would always have to go through 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 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 |
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).
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).
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. |
|
@DanTup In the first two paragraphs I attempt to reason why copy itself cannot be made async. Maybe we could add an
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, 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%.
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 :-)
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). |
|
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: microsoft/vscode-languageserver-node#736.
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. |
|
@dbaeumer The current api proposal flows looks something like:
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 @alexdima To workaround the async issue, I currently:
|
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 |
|
@mjbvz Regarding
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. |

Formed in 2009, the Archive Team (not to be confused with the archive.org Archive-It Team) is a rogue archivist collective dedicated to saving copies of rapidly dying or deleted websites for the sake of history and digital heritage. The group is 100% composed of volunteers and interested parties, and has expanded into a large amount of related projects for saving online and digital history.

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.
The text was updated successfully, but these errors were encountered: