Skip to content

implement browser clipboard service#75293

Merged
bpasero merged 17 commits intomicrosoft:masterfrom
sbatten:clipboardService
Jul 11, 2019
Merged

implement browser clipboard service#75293
bpasero merged 17 commits intomicrosoft:masterfrom
sbatten:clipboardService

Conversation

@sbatten
Copy link
Copy Markdown
Member

@sbatten sbatten commented Jun 11, 2019

No description provided.

@sbatten sbatten requested a review from bpasero June 11, 2019 18:59
@sbatten sbatten self-assigned this Jun 11, 2019
Copy link
Copy Markdown
Member

@bpasero bpasero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Promise.resolve(model);
return model;


export class ClipboardService implements IClipboardService {

_serviceBrand: any;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbatten this is broken now, you have to await the value, no?

@rebornix
Copy link
Copy Markdown
Member

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.

@rebornix
Copy link
Copy Markdown
Member

Re https://developer.mozilla.org/en-US/docs/Web/API/Clipboard

Calls to the methods of the Clipboard object will not succeed if the user hasn't granted the needed permissions using the Permissions API and the "clipboard-read" or "clipboard-write" permission as appropriate.

I'm wondering if we should use document.execCommand by default if we can do that (say there is a copy event handler registered for the focused element, or it's an input/textarea/etc), and then fall back to navigator.clipboard. We don't want to bother with permission access when it's not necessary.

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); });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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); }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, () => foo returns foo

@sbatten
Copy link
Copy Markdown
Member Author

sbatten commented Jun 14, 2019

@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.

Copy link
Copy Markdown
Member

@bpasero bpasero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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;
}

@sbatten
Copy link
Copy Markdown
Member Author

sbatten commented Jun 18, 2019

@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.

@bpasero
Copy link
Copy Markdown
Member

bpasero commented Jun 18, 2019

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 await... should be no problem?

@bpasero bpasero added the web Issues related to running VSCode in the web label Jun 18, 2019
@sbatten
Copy link
Copy Markdown
Member Author

sbatten commented Jun 18, 2019

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

bpasero commented Jun 18, 2019

@sbatten oh now I see what you mean: if someone calls writeText and someone readText without waiting, you might end up reading stale values?

I think the only solution here is to add a Barrier that synchronizes read/write access. We have utility classes for this in async.ts

@sbatten
Copy link
Copy Markdown
Member Author

sbatten commented Jun 18, 2019

@bpasero while that may also be an issue, it wasn't exactly to what I am referring.

I'm referring to the following:

function A() {
 const val = B();
 return val !== undefined;
}

function B() {
 return true;
}

In the above example, imagine B becomes async. The same would need to happen to A because it needs to await the value of B. B may only have one reference in the whole code base. But A has N, C_1 ... C_N. If we iterate on this process, it explodes quickly as C_x can also have many references. Further, even with a linting rule applied, this case is not detected because we store the promise of B so we don't know it is floating. So, finding all references and updating has been the approach so far but like I said, one changes brings 10 others. Eventually this will end, but its a dangerous change. The result is me looking into a smaller more local refactor of snippets or tooling to make sure the larger change is safe.

@bpasero
Copy link
Copy Markdown
Member

bpasero commented Jun 18, 2019

@sbatten yes both things are valid:

  • we need to sync on reading and writing
  • we need to change all methods that read/write clipboard to become async at least if the caller relies on the result of the call. I think we can assume that this is only the case for reading but not writing

@sbatten
Copy link
Copy Markdown
Member Author

sbatten commented Jun 19, 2019

@bpasero

we need to change all methods that read/write clipboard to become async at least if the caller relies on the result of the call. I think we can assume that this is only the case for reading but not writing

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.

@bpasero
Copy link
Copy Markdown
Member

bpasero commented Jun 19, 2019

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); },
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functions like this don't need to use async/await, the fat arrow function is already returning the promise

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it's also fine to write it this way

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@sbatten
Copy link
Copy Markdown
Member Author

sbatten commented Jun 20, 2019

@bpasero finished all updates... I hope. Ran unit tests to ensure no changes, caught one missed await in the test itself.

@bpasero bpasero requested review from bpasero and jrieken June 24, 2019 06:38
@bpasero
Copy link
Copy Markdown
Member

bpasero commented Jun 24, 2019

Adding @jrieken for changes in snippets land and @Tyriar for terminal.

@bpasero bpasero requested review from Tyriar and roblourens June 24, 2019 10:32
Copy link
Copy Markdown
Member

@jrieken jrieken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this needs a different approach for snippets et al

}

insert(
async insert(
Copy link
Copy Markdown
Member

@jrieken jrieken Jun 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 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)

Copy link
Copy Markdown
Member

@bpasero bpasero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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 use async and not a mix of async+then

Copy link
Copy Markdown
Contributor

@Tyriar Tyriar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Terminal 👌

@sbatten
Copy link
Copy Markdown
Member Author

sbatten commented Jul 10, 2019

@jrieken updated with your changes to remove nearly all changes to snippets code. please verify

@bpasero updated to address your feedback

@sbatten sbatten requested review from bpasero and jrieken July 10, 2019 22:38
@bpasero bpasero merged commit 305d5be into microsoft:master Jul 11, 2019
@bpasero bpasero added this to the July 2019 milestone Jul 11, 2019
@github-actions github-actions bot locked and limited conversation to collaborators Mar 28, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

web Issues related to running VSCode in the web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants