Repository: microsoft/vscode (TypeScript)
Date: 2022-11-15 15:10:52+00:00
Task type: Bug Fix

Issue:
API: `createFileSystemWatcher()` doesn't work when relative path ends with `/`
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- 🕮 Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions -->
<!-- 🔎 Search existing issues to avoid creating duplicates. -->
<!-- 🧪 Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ -->
<!-- 💡 Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. -->
<!-- 🔧 Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes

<!-- 🪓 If you answered No above, use 'Help: Start Extension Bisect' from Command Palette to try to identify the cause. -->
<!-- 📣 Issues caused by an extension need to be reported directly to the extension publisher. The 'Help > Report Issue' dialog can assist with this. -->

When watcher created with full filename without wildcards, no events fired when that file changed/created/deleted:
```
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file("C:\\"), "test.json"));
watcher.onDidChange(uri => console.log("change", uri));
watcher.onDidCreate(uri => console.log("create", uri));
watcher.onDidDelete(uri => console.log("delete", uri));
```
When filename (`test.json`) replaced with wildcards `*.json` it works fine.

```
Version: 1.71.2
Commit: 74b1f979648cc44d385a2286793c226e611f59e7
Date: 2022-09-14T21:03:37.738Z
Electron: 19.0.12
Chromium: 102.0.5005.167
Node.js: 16.14.2
V8: 10.2.154.15-electron.0
OS: Windows_NT x64 10.0.19044
Sandboxed: NoSteps to Reproduce:
```


Issue discussion:
It is interesting that this works:

```js
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file("C:\\Testing"), "test.json"));
```

Note that above `test.json` is in a folder `Testing` at the `C` root level.

This does not work - when the file `test.json` itself is at the root of `C`:

```js
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file("C:\\"), "test.json"));
```

So it looks like either `vscode.Uri.file("C:\\")`  or `vscode.RelativePattern()` don't work properly at the drive root level.

Do you need to watch a file at that level?  (Although it should work.)
In my initial tests the root path was irrelevant, as long as I used full complete filename - no events were firing.

With your example, however, I see where the issue is: it's the trailing slash.
In my tests the root directory path contained a trailing slash, once removed it works fine. Like in your example this doesn't work:
```javascript
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file("C:\\Testing\\"), "test.json"));
```
So this is working for you:

```
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file("C:"), "test.json"));
```

I can't get that to work.?
No, it doesn't. But it's because drive path [requires trailing slash](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#fully_qualified_vs._relative_paths), otherwise it's treated as relative path instead:

> Use a backslash as required as part of [volume names](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-volume), for example, the "C:\\" in "C:\path\file" or the "\\\server\share" in "\\\server\share\path\file" for Universal Naming Convention (UNC) names.
I see, and because the trailing slashes must be there for root drive paths, that will screw up `vscode.RelativePattern()` which isn't expecting trailing slashes.
What is the consensus, it only is an issue for root files? Or for any file when pointing to it via relative pattern (that would surprise me a bit because we use that on our own in our extensions iirc).
Basically, filewatcher doesn't fire when used `vscode.RelativePattern()` where `base` directory ended with a slash. Root directory is especially problematic, since it requires a slash at the end.
The logic for pattern matching the relative path is here:

https://github.com/microsoft/vscode/blob/ed1bc56bd8b6343296ba8310167928e1e955a74e/src/vs/workbench/api/common/extHostFileSystemEventService.ts#L64-L89

Opening up for investigation + possible fix (if any) to community. Running with `--verbose` will print a lot of detailed information about raw file events and how they are being processed.
Bad substring logic here: https://github.com/microsoft/vscode/blob/085c409898bbc89c83409f6a394e73130b932add/src/vs/base/common/glob.ts#L370

Solution patch:
diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts
--- a/src/vs/base/common/glob.ts
+++ b/src/vs/base/common/glob.ts
@@ -10,7 +10,7 @@ import { isEqualOrParent } from 'vs/base/common/extpath';
 import { LRUCache } from 'vs/base/common/map';
 import { basename, extname, posix, sep } from 'vs/base/common/path';
 import { isLinux } from 'vs/base/common/platform';
-import { escapeRegExpCharacters } from 'vs/base/common/strings';
+import { escapeRegExpCharacters, ltrim } from 'vs/base/common/strings';
 
 export interface IRelativePattern {
 
@@ -367,7 +367,12 @@ function wrapRelativePattern(parsedPattern: ParsedStringPattern, arg2: string |
 		// Given we have checked `base` being a parent of `path`,
 		// we can now remove the `base` portion of the `path`
 		// and only match on the remaining path components
-		return parsedPattern(path.substr(arg2.base.length + 1), basename);
+		// For that we try to extract the portion of the `path`
+		// that comes after the `base` portion. We have to account
+		// for the fact that `base` might end in a path separator
+		// (https://github.com/microsoft/vscode/issues/162498)
+
+		return parsedPattern(ltrim(path.substr(arg2.base.length), sep), basename);
 	};
 
 	// Make sure to preserve associated metadata
diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts
--- a/src/vscode-dts/vscode.d.ts
+++ b/src/vscode-dts/vscode.d.ts
@@ -2066,7 +2066,9 @@ declare module 'vscode' {
 	export class RelativePattern {
 
 		/**
-		 * A base file path to which this pattern will be matched against relatively.
+		 * A base file path to which this pattern will be matched against relatively. The
+		 * file path must be absolute, should not have any trailing path separators and
+		 * not include any relative segments (`.` or `..`).
 		 */
 		baseUri: Uri;
 


Test patch:
diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts
--- a/src/vs/base/test/common/glob.test.ts
+++ b/src/vs/base/test/common/glob.test.ts
@@ -1091,6 +1091,22 @@ suite('Glob', () => {
 		}
 	});
 
+	test('relative pattern - trailing slash / backslash (#162498)', function () {
+		if (isWindows) {
+			let p: glob.IRelativePattern = { base: 'C:\\', pattern: 'foo.cs' };
+			assertGlobMatch(p, 'C:\\foo.cs');
+
+			p = { base: 'C:\\bar\\', pattern: 'foo.cs' };
+			assertGlobMatch(p, 'C:\\bar\\foo.cs');
+		} else {
+			let p: glob.IRelativePattern = { base: '/', pattern: 'foo.cs' };
+			assertGlobMatch(p, '/foo.cs');
+
+			p = { base: '/bar/', pattern: 'foo.cs' };
+			assertGlobMatch(p, '/bar/foo.cs');
+		}
+	});
+
 	test('pattern with "base" does not explode - #36081', function () {
 		assert.ok(glob.match({ 'base': true }, 'base'));
 	});