implement browser clipboard service#75293
implement browser clipboard service#75293bpasero merged 17 commits intomicrosoft:masterfrom sbatten:clipboardService
Conversation
bpasero
left a comment
There was a problem hiding this comment.
@sbatten can you please revisit where you changed methods to return a Promise and forgot to add an await, I found at least https://github.com/microsoft/vscode/pull/75293/files#diff-058cba421d6dc4a91f615f8a9e35adb8R553
Also, are we certain navigator.clipboard works as advertised? I think it can be undefined in certain cases so we might need to probe for that and install a workaround (see https://stackoverflow.com/questions/51805395/navigator-clipboard-is-undefined).
https://caniuse.com/#search=clipboard seems to indicate that the clipboard API is only available on user interactions (e.g. click), though not sure that still holds true.
Maybe @alexandrudima and @rebornix could help out here, given we seem to have some way of clipboard support (via https://github.com/Microsoft/vscode/blob/39947c72507deb8a44de785825243b9da30a7a19/src/vs/editor/contrib/clipboard/clipboard.ts#L22) in the form of document.execCommand).
| async provideTextContent(resource: URI): Promise<ITextModel> { | ||
| const model = this.modelService.createModel(await this.clipboardService.readText(), this.modeService.createByFilepathOrFirstLine(resource), resource); | ||
|
|
||
| return Promise.resolve(model); |
There was a problem hiding this comment.
| return Promise.resolve(model); | |
| return model; |
|
|
||
| export class ClipboardService implements IClipboardService { | ||
|
|
||
| _serviceBrand: any; |
There was a problem hiding this comment.
| _serviceBrand: any; | |
| _serviceBrand: ServiceIdentifier<IClipboardService>; |
| async resolve(variable: Variable): Promise<string | undefined> { | ||
| for (const delegate of this._delegates) { | ||
| let value = delegate.resolve(variable); | ||
| if (value !== undefined) { |
There was a problem hiding this comment.
@sbatten this is broken now, you have to await the value, no?
|
The last time when I modified Monaco’s clipboard service related code, we can access the clipboard only in events handler of cut, copy and paste but not navigator.clipboard . @sbatten we can take a look together. |
|
Re https://developer.mozilla.org/en-US/docs/Web/API/Clipboard
I'm wondering if we should use |
| this.clipboardService.writeText(result.body.result); | ||
| }, err => this.clipboardService.writeText(this.value.value)); | ||
| return this.clipboardService.writeText(result.body.result); | ||
| }, err => { return this.clipboardService.writeText(this.value.value); }); |
There was a problem hiding this comment.
The err change is redundant, does the same as the original.
| actions.push(new Action('workbench.debug.action.copyAll', nls.localize('copyAll', "Copy All"), undefined, true, () => { | ||
| this.clipboardService.writeText(this.getVisibleContent()); | ||
| return Promise.resolve(undefined); | ||
| return Promise.resolve(this.clipboardService.writeText(this.getVisibleContent())); |
There was a problem hiding this comment.
Seems like we should use async vs promises consistently in the same file at least, the change above this uses async
| enabled: true, | ||
| id: KEYBINDINGS_EDITOR_COMMAND_COPY, | ||
| run: () => this.copyKeybinding(keybindingItem) | ||
| run: () => { return this.copyKeybinding(keybindingItem); } |
There was a problem hiding this comment.
Same as above, () => foo returns foo
|
@bpasero updated with feedback. The link for caniuse is referring to the older synchronous clipboard API. The new API is async but not well supported across browsers. It does what we need which is a method to read and write arbitrary text to the clipboard. After discussing with @rebornix, this is what the service is responsible for while general copy, cut, paste are handled separately using the synchronous API. For me, this means our service does the best it can with the async API and throws when the browser does not support this. We cannot fallback to the synchronous API since there is no meaning to copy text that isn't selected when an extension asks to write text to the clipboard. |
bpasero
left a comment
There was a problem hiding this comment.
@sbatten I mentioned this before, you changed methods to return a Promise but you are not awaiting them properly. E.g. Variable.resolve():
resolveVariables(resolver: VariableResolver): this {
this.walk(candidate => {
if (candidate instanceof Variable) {
if (candidate.resolve(resolver)) {
this._placeholders = undefined;
}
}
return true;
});
return this;
}|
@bpasero yea i thought i got all these with 37e5c97 but as I continue, this is exploding more and more. The assumption of a synchronous clipboard is very pervasive. If I continue this way, I am going to have to write some tooling to do the conversion as it is becoming very difficult to keep track of what's changed and what needs to change. It does seem that the explosion roots from the clipboard based snippets however, so maybe there is a way to reduce the impact by modifying that a bit. |
|
I am not sure I can follow. There shouldn't be a ton of users of clipboard service, or? And I would assume that in many cases the usage of the clipboard service is one of the last things in the code (e.g. as a result of an action), so doing an |
|
@bpasero yes clipboard service usage is low but once that new location's function becomes async, this permeates, and where it is changed, the value is actually needed from readText. I'm looking into splitting some function calls to async and sync versions to limit the impact to the primary code path that is the problem (clipboard based snippets) |
|
@bpasero while that may also be an issue, it wasn't exactly to what I am referring. I'm referring to the following: In the above example, imagine |
|
@sbatten yes both things are valid:
|
agree and since this is becoming a dangerously large and unvalidate-able change, I want to minimize it or use some smarter tooling to detect issues. for context I have another incomplete local commit with another 10-20 files that have to change on top of what's here. |
|
Ok, just add it and I can review again. |
| id: 'acceptSelectedSuggestion', | ||
| precondition: SuggestContext.Visible, | ||
| handler: x => x.acceptSelectedSuggestion(true), | ||
| handler: async (x) => { await x.acceptSelectedSuggestion(true); }, |
There was a problem hiding this comment.
Functions like this don't need to use async/await, the fat arrow function is already returning the promise
There was a problem hiding this comment.
But it's also fine to write it this way
There was a problem hiding this comment.
@roblourens Yea you're right. Originally, I was planning to make them all with async/await for clarity reasons, but I'm not sure I was consistent in that approach.
|
@bpasero finished all updates... I hope. Ran unit tests to ensure no changes, caught one missed await in the test itself. |
jrieken
left a comment
There was a problem hiding this comment.
Sorry, this needs a different approach for snippets et al
| } | ||
|
|
||
| insert( | ||
| async insert( |
There was a problem hiding this comment.
🛑 this cannot be async, never. Subsequently all following async changes cannot happen.
Making snippets insert async, means that insert suggestion become async and that means you can accept a suggestion, type, and get the suggestion insert after that. This needs a different approach, e.g the snippet completion provider has the change to read the clipboard at the time of computing snippets (async is OK there)
There was a problem hiding this comment.
@sbatten a couple of feedback (outside of snippets/terminal):
- you do not need to ever use
Promise.resolve()if the thing is a promise you return, I noticed you are using it in a couple of places in your change startupProfiler: suggest to convert this to useasyncand not a mix ofasync+then
No description provided.