Repository: microsoft/vscode (TypeScript)
Date: 2021-01-15 13:46:13+00:00
Task type: Bug Fix

Issue:
LabelService.getUriLabel misreports relative path when workspace folder is at root of filesystem
Issue Type: <b>Bug</b>

This showed up when trying to use the new `RELATIVE_FILEPATH` snippet variable (see #114208) with a resource served by a FileSystemProvider in which it is common to have a workspace path of `/`

I plan to submit a PR. Initially it will only contain a new test to demonstrate the bug. Then I will push a fix.

VS Code version: Code - Insiders 1.53.0-insider (1a6eef3170be2a11751ee50d450328d458f7bfac, 2021-01-15T05:15:40.826Z)
OS version: Windows_NT x64 10.0.19042


<!-- generated by issue reporter -->
LabelService.getUriLabel misreports relative path when workspace folder is at root of filesystem
Issue Type: <b>Bug</b>

This showed up when trying to use the new `RELATIVE_FILEPATH` snippet variable (see #114208) with a resource served by a FileSystemProvider in which it is common to have a workspace path of `/`

I plan to submit a PR. Initially it will only contain a new test to demonstrate the bug. Then I will push a fix.

VS Code version: Code - Insiders 1.53.0-insider (1a6eef3170be2a11751ee50d450328d458f7bfac, 2021-01-15T05:15:40.826Z)
OS version: Windows_NT x64 10.0.19042


<!-- generated by issue reporter -->


Issue discussion:

Solution patch:
diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts
--- a/src/vs/workbench/services/label/common/labelService.ts
+++ b/src/vs/workbench/services/label/common/labelService.ts
@@ -152,6 +152,8 @@ export class LabelService extends Disposable implements ILabelService {
 			while (relativeLabel[overlap] && relativeLabel[overlap] === baseResourceLabel[overlap]) { overlap++; }
 			if (!relativeLabel[overlap] || relativeLabel[overlap] === formatting.separator) {
 				relativeLabel = relativeLabel.substring(1 + overlap);
+			} else if (overlap === baseResourceLabel.length && baseResource.uri.path === '/') {
+				relativeLabel = relativeLabel.substring(overlap);
 			}
 
 			const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1;


Test patch:
diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts
--- a/src/vs/workbench/services/label/test/browser/label.test.ts
+++ b/src/vs/workbench/services/label/test/browser/label.test.ts
@@ -160,7 +160,7 @@ suite('URI Label', () => {
 });
 
 
-suite('multi-root worksapce', () => {
+suite('multi-root workspace', () => {
 	let labelService: LabelService;
 
 	setup(() => {
@@ -171,7 +171,7 @@ suite('multi-root worksapce', () => {
 		labelService = new LabelService(
 			TestEnvironmentService,
 			new TestContextService(
-				new Workspace('test-workspaace', [
+				new Workspace('test-workspace', [
 					new WorkspaceFolder({ uri: sources, index: 0, name: 'Sources' }, { uri: sources.toString() }),
 					new WorkspaceFolder({ uri: tests, index: 1, name: 'Tests' }, { uri: tests.toString() }),
 					new WorkspaceFolder({ uri: other, index: 2, name: resources.basename(other) }, { uri: other.toString() }),
@@ -179,7 +179,7 @@ suite('multi-root worksapce', () => {
 			new TestPathService());
 	});
 
-	test('labels of files in multiroot workspaces are the foldername folloed by offset from the folder', () => {
+	test('labels of files in multiroot workspaces are the foldername followed by offset from the folder', () => {
 		labelService.registerFormatter({
 			scheme: 'file',
 			formatting: {
@@ -250,3 +250,57 @@ suite('multi-root worksapce', () => {
 		});
 	});
 });
+
+suite('workspace at FSP root', () => {
+	let labelService: LabelService;
+
+	setup(() => {
+		const rootFolder = URI.parse('myscheme://myauthority/');
+
+		labelService = new LabelService(
+			TestEnvironmentService,
+			new TestContextService(
+				new Workspace('test-workspace', [
+					new WorkspaceFolder({ uri: rootFolder, index: 0, name: 'FSProotFolder' }, { uri: rootFolder.toString() }),
+				])),
+			new TestPathService());
+		labelService.registerFormatter({
+			scheme: 'myscheme',
+			formatting: {
+				label: '${scheme}://${authority}${path}',
+				separator: '/',
+				tildify: false,
+				normalizeDriveLetter: false,
+				workspaceSuffix: '',
+				authorityPrefix: '',
+				stripPathStartingSeparator: false
+			}
+		});
+	});
+
+	test('non-relative label', () => {
+
+		const tests = {
+			'myscheme://myauthority/myFile1.txt': 'myscheme://myauthority/myFile1.txt',
+			'myscheme://myauthority/folder/myFile2.txt': 'myscheme://myauthority/folder/myFile2.txt',
+		};
+
+		Object.entries(tests).forEach(([uriString, label]) => {
+			const generated = labelService.getUriLabel(URI.parse(uriString), { relative: false });
+			assert.equal(generated, label);
+		});
+	});
+
+	test('relative label', () => {
+
+		const tests = {
+			'myscheme://myauthority/myFile1.txt': 'myFile1.txt',
+			'myscheme://myauthority/folder/myFile2.txt': 'folder/myFile2.txt',
+		};
+
+		Object.entries(tests).forEach(([uriString, label]) => {
+			const generated = labelService.getUriLabel(URI.parse(uriString), { relative: true });
+			assert.equal(generated, label);
+		});
+	});
+});