Skip to content

Add {will,did}{Create,Rename,Delete}Files - #687

Merged
Dirk Bäumer (dbaeumer) merged 35 commits into
microsoft:masterfrom
DanTup:file-operations
Dec 7, 2020
Merged

Dirk Bäumer (dbaeumer) merged 35 commits into
microsoft:masterfrom
DanTup:file-operations

Conversation

@DanTup

@DanTup Danny Tuppeny (DanTup) commented Nov 10, 2020 •

Copy link
Copy Markdown
Contributor

This is still very much WIP, but opening to get some feedback/guidance.

Implementation for microsoft/language-server-protocol#1074.

Comment thread client-node-tests/src/integration.test.ts Outdated
@DanTup

Danny Tuppeny (DanTup) commented Nov 16, 2020 •

Copy link
Copy Markdown
Contributor Author

Dirk Bäumer (@dbaeumer) I've implemented a test for OnWillCreateFiles - can you check that what's here seems sane before I implement the others (which are largely a copy/paste of this with Create swapped for Rename/Delete).

I made the test server register statically + dynamically (slightly different paths) so both can be tested together (I don't have middleware tests yet, but they should be easy).

Any preferences for how to test the Did{Create,Rename,Delete} notifications went through? (They don't have a response like the Will{...} requests.

Thanks!

Edit: It might not be very obvious from the test, but the server creates an edit for a dummy reference file which just includes all the paths it received. Seemed like the easiest way for the client to verify the server got the expected event (as well as verifying the edit it passed through to waitUntil). If there's a better way, let me know!

Comment thread client/src/common/client.ts Outdated

public initialize(capabilities: ServerCapabilities, _: DocumentSelector): void {
let syncOptions = capabilities.files;
if (syncOptions?.willCreate?.globPattern) {

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.

I started (for new code) to make falsify checks explicit since I trip over undefined and 0 quite often (espeically for number and string). So this should be syncOptions?.willCreate?.globPattern !== undefined. And do we need to check the syntax of the glob pattern?

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.

I see now the comment in register

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.

Regarding errors: during initialization we should check the pattern, and if not valid ignore the capability and add something to the output channel.

If an invalid pattern is received during dynamic registration we should throw. However we should always throw new Error and never a string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done both of these. We throw a custom error type in registration so it can be caught by static registration and ignored/logged instead. Let me know if this should done differently!

@dbaeumer

Copy link
Copy Markdown
Member

Regarding the test question: you can also throw an error on the server side which will propagate back to the client and will make the send call fail. Normally this will make the test fail.

@DanTup

Copy link
Copy Markdown
Contributor Author

Regarding the test question: you can also throw an error on the server side which will propagate back to the client and will make the send call fail. Normally this will make the test fail.

I think this would prevent checking that calling waitUntil is being handled correctly? Or at least, require separate tests (some that throw with the file list, for testing the correct files were sent, and another that returns a known edit that is passed to waitUntil to ensure that's being handled/called correctly). Any preference?

Comment thread client/src/common/client.ts Outdated
}
}

class InvalidGlobError extends Error {

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.

I would leave this out and simply throw new Error. The client will not catch it anyways and the server will not care :-)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done! Though this does mean if another error occurs (that isn't converting the glob to a regex) inside the registration method, it may warn with a misleading message (and would also ignore the error). There's not a lot of code there to go wrong, but thought it worth mentioning.

@DanTup

Danny Tuppeny (DanTup) commented Nov 25, 2020 •

Copy link
Copy Markdown
Contributor Author

Dirk Bäumer (@dbaeumer) any other feedback on this, or is it worth me implementing the other requests too? (they're mostly copy/pastes of this, though with the did ones being slightly simpler than the will ones as they're just notifications with no responses - though we will need a way to confirm in the test that the server received the notification?).

@dbaeumer

Copy link
Copy Markdown
Member

Danny Tuppeny (@DanTup) to be able to merge this we should have implementation for all notifications and requests. I will add my comments to the one implementation we have and the the rest should be copy paste.

@DanTup

Copy link
Copy Markdown
Contributor Author

Dirk Bäumer (@dbaeumer) sgtm! I always planed to implement them all before anything is merged - I just wanted to check you're happy with this one before creating the others. Thanks!

Comment thread protocol/src/common/protocol.ts Outdated
codeLens?: CodeLensWorkspaceClientCapabilities;
}

export interface FileClientCapabilities {

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.

Now that I think about the name we should may be call this FileManipulation to make it clear that these are not file events in the classical sense of file watching. We should also add a sentence to make this clear.

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.

You used FileOperations further down which is a good name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

Comment thread protocol/src/common/protocol.ts Outdated
*
* Since 3.16.0
*/
files?: FileClientCapabilities

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.

And then we can name this fileOperations as well. Same on the server side.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done!

(Also pushed changes to the LSP spec PR to reflect the same)

Comment thread server/src/common/server.ts Outdated
*
* @param handler The corresponding handler.
*/
onWillCreateFiles(handler: RequestHandler<CreateFilesParams, WorkspaceEdit | undefined | null, void>): void;

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.

I moved away from putting the handlers on the connection namespace since it makes it hard to understand this. Since we already have a window namespace and that namespace is use for UI specific requests and notification we should add these to the window namespace. This makes it also clear that this has nothing to do with normal file events since those would be on the workspace namespace. All window stuff is UI related.

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.

This also allows you to implement this on the server side as a feature and not directly in the server since the feature can be added to the RemoteWindow type here https://github.com/microsoft/vscode-languageserver-node/blob/master/server/src/common/server.ts#L470 and to the implementation here: https://github.com/microsoft/vscode-languageserver-node/blob/master/server/src/common/server.ts#L512

I can help with this if it is not clear how to do this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I started doing this, but I've gotten confused. The example I was copying (ShowDocumentFeature) is a server->client request. The ones I'm implementing come from the client. So I don't have a server implementation that's shared, the implementation needs to come from the specific server implementation.

I've pushed my changes so far, there are two places I'm stuck:

  1. What the (test) server implementation should look like now (this code doesn't compile):

https://github.com/microsoft/vscode-languageserver-node/pull/687/files#diff-18e38934adbfa54d730ee13bc65f20e81c71d6b5fcc526f1a6c543d9cb2995dbR243

  1. What should the Feature look like (when the server code has no default implementation unless it's being contributed by the implementing server):

https://github.com/microsoft/vscode-languageserver-node/pull/687/files#diff-36df91f0cf05683eb4b817da9fe0d3f54e6110d749be36546e3e47ac09f72280R15

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.

You can copy from an example that implements a language request. It would work the same for the window namespace. A corresponding example is here: https://github.com/microsoft/vscode-languageserver-node/blob/master/server/src/common/onTypeRename.ts#L6

The code for the test server is correct assuming that you have it working for the window namespace. Is your code checked in. Then I can tweak it and push to your repository if you give me write access.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Aha, I see! I've gotten further (and pushed my changes), though it seems like I'm missing a this.attachWorkDoneProgress from _RemoteWindowImpl (it's on _Languages already). What's the correct way to add this? (I can copy/paste from _Languages but I'm not sure if that's right - I'm also unsure if we should have progress here and if so whether I need some tweaks to the spec?).

I've given your access to my fork btw, so feel free to make changes if that's easier than describing. Thanks!

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.

OK. I did the changes and pushed them. Did some additional code cleanup One thing I didn't do to not surprise you is to rename protocol.window.fileOperations.ts to protocol.fileOperations. I usually don't put the namespace into the name. A also created a fileOperations.ts file in the client.

Regarding this.attachWorkDoneProgress since the request should be fast we shouldn't allow progress. So you use a normal RequestHandler.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great - thanks! I'll rename that file and fill in the rest of the notifications and tests.

@DanTup Danny Tuppeny (DanTup) changed the title [WIP] Add {will,did}{Create,Rename,Delete}Files Add {will,did}{Create,Rename,Delete}Files Nov 30, 2020
@dbaeumer

Copy link
Copy Markdown
Member

Danny Tuppeny (@DanTup) thanks for providing the PR.

Regarding pre-release: yes, either today or tomorrow. I still need to do some polish and I wanted to better understand the minimatch directory issue to see if we need to spec more.

@DanTup

Copy link
Copy Markdown
Contributor Author

Great!

Do shout if anything isn't clear or you want me to make changes. I went with the second option above (we suffix / on the folder URIs for matching, so both **/ and **/* will match all folders) so I think it should work as expected, though I'm not all that familiar with these glob patterns!

Thanks!

@dbaeumer

Copy link
Copy Markdown
Member

Danny Tuppeny (@DanTup) I played a litte with minimatch and I think we don't need to add the slash to the pattern since we have the pattern kind. I tested that for example:

let matcher = new Minimatch('a/**');
matcher.match('a/folder1/folder2/folder3');

evaluates to true.

So I would suggest that we do the following:

  • if kind (e.g. folder or file) is undefined we take the pattern as is and match
  • if kind is defined then we stat the URI and make sure that the type returned from the stat matches the requested kind.

This means we still need to do that stat but we don't need to tweak the paths. Is there something you can think of that doesn't make this work?

@dbaeumer

Copy link
Copy Markdown
Member

The filter method would then look like this:

	protected async filter(event: E, prop: (i: I) => code.Uri): Promise<E> {
		// (Asynchronously) map each file onto a boolean of whether it matches
		// any of the globs.
		const fileMatches = await Promise.all(event.files.map(async (item) => {
			const uri = prop(item);
			const path = uri.fsPath;
			for (const globs of this._globPatterns.values()) {
				for (const pattern of globs) {
					if (pattern.matcher.match(path)) {
						// The pattern matches. If kind is undefined then everything is ok
						if (pattern.kind === undefined) {
							return true;
						}
						const fileType = await FileOperationFeature.getFileType(uri);
						if (fileType === undefined || (fileType === code.FileType.File && pattern.kind === FileOperationPatternKind.file) || (fileType === code.FileType.Directory && pattern.kind === FileOperationPatternKind.folder)) {
							return true;
						}
					}
				}
			}
			return false;
		}));

		// Filter the files to those that matched.
		const files = event.files.filter((_, index) => fileMatches[index]);

		return { ...event, files };
	}

@dbaeumer

Copy link
Copy Markdown
Member

Tests are still green with that change. One thing we can debate is whether a failure of determining the file type should drop the event or sent it. Currently it is sent.

I have pushed the changes. Let me know if you see any problems with them.

@DanTup

Copy link
Copy Markdown
Contributor Author

I played a litte with minimatch and I think we don't need to add the slash to the pattern since we have the pattern kind

The case I was trying to cover was if the pattern ended with a /, eg:

// true
console.log(new Minimatch('a/**').match('a/folder1/folder2/folder3'));
// false
console.log(new Minimatch('a/**/').match('a/folder1/folder2/folder3'));
// true
console.log(new Minimatch('a/**/').match('a/folder1/folder2/folder3/'));

The middle one fails there, as the pattern has a trailing slash. I don't know whether that's reasonable or not, but some of my previous example patterns were like:

**{/,/*.dart}

If it's ok to say there shouldn't be trailing slashes on the patterns for directories (we can't enforce this easily ourselves if we're not doing the brace expansion), then I think the changes are fine. It would make sense to highlight this in the spec if so, to avoid catching people out.

One thing we can debate is whether a failure of determining the file type should drop the event or sent it. Currently it is sent.

I have no strong feeling on this, though since the server can easily drop it if it can't do anything with it (but can't handle what it doesn't get), maybe sending it makes most sense. Maybe worth being explicit about this in the spec whichever way you think is best?

@dbaeumer

Copy link
Copy Markdown
Member

OK. I do think we should cover console.log(new Minimatch('a/**/').match('a/folder1/folder2/folder3'));. I will look into fixing it.

@dbaeumer

Copy link
Copy Markdown
Member

Here is what I suggest we do: if the matching fails and kind is folder then we stat and if it is a directory we add a '/' to the end and rematch. The reasoning behind this is that the regular expression matching is a lot faster than stating the URI.

Does that makes sense?

@dbaeumer

Copy link
Copy Markdown
Member

See cd9b8e4

@DanTup

Copy link
Copy Markdown
Contributor Author

I can't think of any examples where that wouldn't work, so seems sensible to me.

The only question I have is whether minimatch cares that the trailing slash is forwards but the rest of the path might be backslashes now we're using .fsPath?

@dbaeumer

Copy link
Copy Markdown
Member

Minimatch actually doesn't care. But let me check again.

I used fsPath since we do use that in all other cases, even with document selector patterns. So this makes it at least consistent.

@DanTup

Copy link
Copy Markdown
Contributor Author

Minimatch actually doesn't care

Great, all sounds good to me then :-)

@dbaeumer

Copy link
Copy Markdown
Member

It does take care of it but it depends on the platform :-). Only under Windows it treats \ correctly and doesn't like '/' in a path. Need to see what we best do here to ensure everything works even with remote file systems.

@DanTup

Copy link
Copy Markdown
Contributor Author

Oh, I see. Can't we switch back to Uri.path and say that patterns always use /?

@dbaeumer

Copy link
Copy Markdown
Member

Using Uri.path is IMO confusing since VS Code always uses fsPath for patterns. This would result in strange behavior expecially under Windows with drive letters.

I did some more testing and it is actually OK. Under Windows:

let matcher = new Minimatch('a/**/')
> matcher.match('a\\folder\\folder\\')
true
> matcher.match('a\\folder\\folder/')
true
>

Which is OK. But what I will do after checking with what we do elsewhere is to replace \\ with / in the returned fsPath.

@dbaeumer

Copy link
Copy Markdown
Member

Done.

@DanTup

Copy link
Copy Markdown
Contributor Author

This would result in strange behavior expecially under Windows with drive letters.

Fair - I was thinking patterns would always be relative since they're implicitly constrained by the workspace, but that probably isn't a valid assumption.

Oh, one final thing - minimatch seems to be case-sensitive by default. I think we should document this, or state that they're case-insensitive and set nomatch?

// true
console.log(new Minimatch('a/**').match('a/folder1/folder2/folder3'));
// false
console.log(new Minimatch('A/**').match('a/folder1/folder2/folder3'));
// true
console.log(new Minimatch('A/**', { nocase: true }).match('a/folder1/folder2/folder3'));

Let me know if you want me to do any of that. Thanks!

@dbaeumer

Copy link
Copy Markdown
Member

Actually, that is a good point. We should add an options property to FileOperationPattern with a property ignoreCase: boolean.

Something like

interface FileOperationPatternOptions {
    ignoreCase: boolean;
}

Does that make sense to you ?

@DanTup

Copy link
Copy Markdown
Contributor Author

Yep! I can update the spec/client tomorrow if you'd like.

Should the options and/or ignoreCase be optional? And if so, what should it default to?

@dbaeumer

Dirk Bäumer (dbaeumer) commented Dec 8, 2020 •

Copy link
Copy Markdown
Member

I made it optional. Looks like this

export interface FileOperationPatternOptions {

	/**
	 * The pattern should be match ignoring casing.
	 */
	ignoreCase?: boolean;
}

Default should be false as with all optional values.

@dbaeumer

Copy link
Copy Markdown
Member

I update the spec as well.

@DanTup

Copy link
Copy Markdown
Contributor Author

I added a comment here about a typo + making the default clear from the description:

943b2db#r44910295

But otherwise, all looks good to me :-)

@dbaeumer

Copy link
Copy Markdown
Member

Published new next versions.

@DanTup

Copy link
Copy Markdown
Contributor Author

Thanks! I rebuilt everything from the latest version of the spec in the Dart server, and my renames are all working as expected! 😀

Looking at the stuff in the v3.16 milestone here and for LSP, I presume there aren't any significant changes planned before it's finalised? I'd like to start preparing PRs for the server changes (for this + semantic tokens) but have been holding off until the spec was less likely to change.

Thanks again for your help + patience with this!

@dbaeumer

Copy link
Copy Markdown
Member

Thanks a lot for all your help with this. I don't plan to add any new features for 3.16. As soon as VS Code 1.52 ships I will release a new LSP version.

@DanTup

Copy link
Copy Markdown
Contributor Author

Perfect, thanks!

Out of interest - is there anything specific in VS Code 1.52 that is required for v3.16? I'm wondering whether I should be increasing the minimum version of VS Code when I switch to this latest client or not - I haven't noticed any issues using it with v1.51, but my testing has been quite focused on the changes in this PR.

@dbaeumer

Copy link
Copy Markdown
Member

The new version of LSP will require 1.52 because of the linked range editing support.

@DanTup

Copy link
Copy Markdown
Contributor Author

Ah, I see - I'll bump my minimum version once VS code releases then too. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants