@theia/workspace 1.77.0-next.6 → 1.77.0-next.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/lib/browser/metadata-storage/workspace-metadata-storage-service.spec.js +3 -2
  2. package/lib/browser/metadata-storage/workspace-metadata-storage-service.spec.js.map +1 -1
  3. package/lib/browser/quick-open-workspace.js +1 -1
  4. package/lib/browser/quick-open-workspace.js.map +1 -1
  5. package/lib/browser/test/with-workspace-service-defaults.d.ts +8 -0
  6. package/lib/browser/test/with-workspace-service-defaults.d.ts.map +1 -0
  7. package/lib/browser/test/with-workspace-service-defaults.js +28 -0
  8. package/lib/browser/test/with-workspace-service-defaults.js.map +1 -0
  9. package/lib/browser/workspace-commands.spec.js +2 -1
  10. package/lib/browser/workspace-commands.spec.js.map +1 -1
  11. package/lib/browser/workspace-delete-handler.js +1 -1
  12. package/lib/browser/workspace-delete-handler.js.map +1 -1
  13. package/lib/browser/workspace-frontend-contribution.d.ts.map +1 -1
  14. package/lib/browser/workspace-frontend-contribution.js +4 -2
  15. package/lib/browser/workspace-frontend-contribution.js.map +1 -1
  16. package/lib/browser/workspace-schema-updater.js +1 -1
  17. package/lib/browser/workspace-schema-updater.js.map +1 -1
  18. package/lib/browser/workspace-search-filter-service.spec.js +2 -1
  19. package/lib/browser/workspace-search-filter-service.spec.js.map +1 -1
  20. package/lib/browser/workspace-service.d.ts +85 -0
  21. package/lib/browser/workspace-service.d.ts.map +1 -1
  22. package/lib/browser/workspace-service.js +203 -5
  23. package/lib/browser/workspace-service.js.map +1 -1
  24. package/lib/browser/workspace-service.spec.d.ts +2 -0
  25. package/lib/browser/workspace-service.spec.d.ts.map +1 -0
  26. package/lib/browser/workspace-service.spec.js +232 -0
  27. package/lib/browser/workspace-service.spec.js.map +1 -0
  28. package/lib/browser/workspace-trust-service.js +1 -1
  29. package/lib/browser/workspace-trust-service.js.map +1 -1
  30. package/lib/browser/workspace-trust-service.spec.js +3 -2
  31. package/lib/browser/workspace-trust-service.spec.js.map +1 -1
  32. package/lib/browser/workspace-uri-contribution.d.ts +14 -0
  33. package/lib/browser/workspace-uri-contribution.d.ts.map +1 -1
  34. package/lib/browser/workspace-uri-contribution.js +44 -2
  35. package/lib/browser/workspace-uri-contribution.js.map +1 -1
  36. package/lib/browser/workspace-uri-contribution.spec.js +43 -5
  37. package/lib/browser/workspace-uri-contribution.spec.js.map +1 -1
  38. package/lib/node/default-workspace-server.js +4 -4
  39. package/lib/node/default-workspace-server.js.map +1 -1
  40. package/lib/node/default-workspace-server.spec.js +3 -2
  41. package/lib/node/default-workspace-server.spec.js.map +1 -1
  42. package/package.json +5 -5
  43. package/src/browser/test/with-workspace-service-defaults.ts +26 -0
  44. package/src/browser/workspace-frontend-contribution.ts +4 -2
  45. package/src/browser/workspace-service.spec.ts +274 -0
  46. package/src/browser/workspace-service.ts +216 -2
  47. package/src/browser/workspace-uri-contribution.spec.ts +45 -3
  48. package/src/browser/workspace-uri-contribution.ts +43 -1
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { injectable, inject, postConstruct, named } from '@theia/core/shared/inversify';
18
18
  import URI from '@theia/core/lib/common/uri';
19
+ import { Path } from '@theia/core/lib/common/path';
19
20
  import { WorkspaceServer, UntitledWorkspaceService, WorkspaceFileService } from '../common';
20
21
  import { WindowService } from '@theia/core/lib/browser/window/window-service';
21
22
  import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window';
@@ -25,7 +26,7 @@ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
25
26
  import { ILogger, Disposable, DisposableCollection, Emitter, environment, Event, MaybePromise, MessageService, nls, ContributionProvider } from '@theia/core';
26
27
  import { WorkspacePreferences } from '../common/workspace-preferences';
27
28
  import * as jsoncparser from 'jsonc-parser';
28
- import * as Ajv from '@theia/core/shared/ajv';
29
+ import Ajv from '@theia/core/shared/ajv';
29
30
  import { FileStat, BaseStat } from '@theia/filesystem/lib/common/files';
30
31
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
31
32
  import { WindowTitleService } from '@theia/core/lib/browser/window/window-title-service';
@@ -312,6 +313,7 @@ export class WorkspaceService implements FrontendApplicationContribution, Worksp
312
313
  }
313
314
  if (rootsChanged) {
314
315
  this._roots = newRoots;
316
+ this.rootNamesCache = undefined;
315
317
  this.deferredRoots.resolve(this._roots); // in order to resolve first
316
318
  this.deferredRoots = new Deferred<FileStat[]>();
317
319
  this.deferredRoots.resolve(this._roots);
@@ -596,6 +598,7 @@ export class WorkspaceService implements FrontendApplicationContribution, Worksp
596
598
  this.windowService.setSafeToShutDown();
597
599
  this._workspace = undefined;
598
600
  this._roots.length = 0;
601
+ this.rootNamesCache = undefined;
599
602
 
600
603
  if (!this.isRemoteSession()) {
601
604
  await this.server.setMostRecentlyUsedWorkspace('');
@@ -833,7 +836,7 @@ export class WorkspaceService implements FrontendApplicationContribution, Worksp
833
836
  if (!rootUri) {
834
837
  return uri.path.fsPath();
835
838
  }
836
- const rootName = rootUri.path.base;
839
+ const rootName = this.getRootName(rootUri) ?? rootUri.path.base;
837
840
  const relative = rootUri.relative(uri);
838
841
  if (!relative || relative.toString() === '') {
839
842
  return rootName;
@@ -841,6 +844,217 @@ export class WorkspaceService implements FrontendApplicationContribution, Worksp
841
844
  return `${rootName}/${relative.toString()}`;
842
845
  }
843
846
 
847
+ /**
848
+ * Returns a unique name for each workspace root, in workspace order.
849
+ *
850
+ * A root is named after its directory basename, e.g. `backend`. Roots that share a basename are
851
+ * treated as peers: each of them is named after the shortest tail of its path that tells them
852
+ * apart, e.g. `alice/app` and `bob/app`. A name that another name starts with, e.g. `alice` next to
853
+ * `alice/app`, is qualified in the same way, e.g. `work/alice`, so that every path resolves to the root
854
+ * it came from. The names are thus always the ending of the real path, so they match what appears in
855
+ * terminal output, compiler errors and the like.
856
+ *
857
+ * These are the root names used by {@link getRootPrefixedPath} and resolved by {@link resolveRootPrefixedPath}.
858
+ */
859
+ getRootNames(): ReadonlyMap<string, URI> {
860
+ return this.getRootNamesCache().names;
861
+ }
862
+
863
+ /**
864
+ * The root names, and the names by root URI, which depend on each other. Reset whenever the roots change.
865
+ */
866
+ protected rootNamesCache: { names: ReadonlyMap<string, URI>; namesByRoot: ReadonlyMap<string, string> } | undefined;
867
+
868
+ protected getRootNamesCache(): { names: ReadonlyMap<string, URI>; namesByRoot: ReadonlyMap<string, string> } {
869
+ if (!this.rootNamesCache) {
870
+ const names = this.computeRootNames(this.tryGetRoots());
871
+ const namesByRoot = new Map(Array.from(names, ([name, uri]) => [uri.toString(), name]));
872
+ this.rootNamesCache = { names, namesByRoot };
873
+ }
874
+ return this.rootNamesCache;
875
+ }
876
+
877
+ protected computeRootNames(workspaceRoots: FileStat[]): Map<string, URI> {
878
+ const roots: URI[] = [];
879
+ for (const root of workspaceRoots) {
880
+ if (!roots.some(other => other.isEqual(root.resource))) {
881
+ roots.push(root.resource);
882
+ }
883
+ }
884
+ const byBasename = new Map<string, URI[]>();
885
+ for (const root of roots) {
886
+ const peers = byBasename.get(root.path.base) ?? [];
887
+ peers.push(root);
888
+ byBasename.set(root.path.base, peers);
889
+ }
890
+ const names = new Map<string, URI>();
891
+ for (const root of roots) {
892
+ names.set(this.computeRootName(root, byBasename.get(root.path.base)!), root);
893
+ }
894
+ return this.qualifyRootNames(names);
895
+ }
896
+
897
+ /**
898
+ * Returns the name of the given workspace root, as assigned by {@link getRootNames}, or
899
+ * `undefined` if the URI is not a workspace root.
900
+ */
901
+ getRootName(rootUri: URI): string | undefined {
902
+ return this.getRootNamesCache().namesByRoot.get(rootUri.toString());
903
+ }
904
+
905
+ /**
906
+ * Resolves a path in the format of {@link getRootPrefixedPath}, i.e. `<rootName>/<relativePath>`
907
+ * or a bare `<rootName>`, to a URI. If more than one root name is a prefix of the path, which is
908
+ * the case when a root named `a` has a subfolder `b` and another root is named `a/b`, the longest
909
+ * one wins.
910
+ *
911
+ * @returns `undefined` if the path is absolute, does not start with a root name or leaves its root.
912
+ */
913
+ resolveRootPrefixedPath(rootPrefixedPath: string): URI | undefined {
914
+ const path = new Path(Path.normalizePathSeparator(rootPrefixedPath)).normalize();
915
+ if (path.isAbsolute || Path.isDrive(path.toString().split('/')[0])) {
916
+ return undefined;
917
+ }
918
+ const segments = path.toString().split('/').filter(segment => segment.length > 0);
919
+ if (segments.includes('..')) {
920
+ return undefined;
921
+ }
922
+ const match = this.splitRootPrefixedPath(segments.join('/'));
923
+ if (!match) {
924
+ return undefined;
925
+ }
926
+ return match.rest ? match.rootUri.resolve(match.rest) : match.rootUri;
927
+ }
928
+
929
+ /**
930
+ * Resolves a path that contains the basename of a workspace root, preceded by nothing or by the
931
+ * trailing segments of the root's parent path, e.g. `bob/app/src` for the root `/home/bob/app`.
932
+ * Unlike {@link resolveRootPrefixedPath}, this does not depend on the current root names, so it
933
+ * also resolves paths that were prefixed with a root name before the workspace roots changed.
934
+ *
935
+ * @returns the distinct candidates in workspace order, which are several if the path is ambiguous,
936
+ * or none if the path is absolute, leaves its root or does not contain the basename of a root.
937
+ */
938
+ resolveSupraRelativePath(supraRelativePath: string): URI[] {
939
+ const path = new Path(Path.normalizePathSeparator(supraRelativePath)).normalize();
940
+ const segments = path.toString().split('/').filter(segment => segment.length > 0);
941
+ if (path.isAbsolute || Path.isDrive(segments[0] ?? '') || segments.includes('..')) {
942
+ return [];
943
+ }
944
+ const candidates: URI[] = [];
945
+ for (const root of this.tryGetRoots()) {
946
+ const candidate = this.resolveSupraRelativeSegments(root.resource, segments);
947
+ if (candidate && !candidates.some(other => other.isEqual(candidate))) {
948
+ candidates.push(candidate);
949
+ }
950
+ }
951
+ return candidates;
952
+ }
953
+
954
+ /**
955
+ * Resolves `segments` against the given root as described in {@link resolveSupraRelativePath}.
956
+ */
957
+ protected resolveSupraRelativeSegments(rootUri: URI, segments: string[]): URI | undefined {
958
+ const rootBasename = rootUri.path.base;
959
+ const rootPathSegments = rootUri.path.toString().split('/').filter(segment => segment.length > 0);
960
+ const rootPrecedingSegments = rootPathSegments.slice(0, rootPathSegments.length - 1);
961
+ for (let i = 0; i < segments.length; i++) {
962
+ if (segments[i] !== rootBasename) {
963
+ continue;
964
+ }
965
+ const pathPrecedingSegments = segments.slice(0, i);
966
+ if (pathPrecedingSegments.length > rootPrecedingSegments.length) {
967
+ continue;
968
+ }
969
+ const rootTail = rootPrecedingSegments.slice(rootPrecedingSegments.length - pathPrecedingSegments.length);
970
+ if (pathPrecedingSegments.every((segment, j) => segment === rootTail[j])) {
971
+ const rest = segments.slice(i + 1).join('/');
972
+ return rest ? rootUri.resolve(rest) : rootUri;
973
+ }
974
+ }
975
+ return undefined;
976
+ }
977
+
978
+ /**
979
+ * Splits a `/`-separated path that starts with a root name, as in {@link getRootPrefixedPath}, into
980
+ * that root and the rest of the path, which is empty for a bare root name. The rest is not normalized,
981
+ * so it may as well be a glob. If more than one root name is a prefix of the path, the longest one wins.
982
+ *
983
+ * @returns `undefined` if the path does not start with a root name.
984
+ */
985
+ splitRootPrefixedPath(rootPrefixedPath: string): { rootName: string; rootUri: URI; rest: string } | undefined {
986
+ const segments = rootPrefixedPath.split('/');
987
+ let match: { rootName: string; rootUri: URI; length: number } | undefined;
988
+ for (const [rootName, rootUri] of this.getRootNames()) {
989
+ const nameSegments = rootName.split('/');
990
+ const matches = nameSegments.length <= segments.length && nameSegments.every((segment, i) => segment === segments[i]);
991
+ if (matches && (!match || nameSegments.length > match.length)) {
992
+ match = { rootName, rootUri, length: nameSegments.length };
993
+ }
994
+ }
995
+ return match && { rootName: match.rootName, rootUri: match.rootUri, rest: segments.slice(match.length).join('/') };
996
+ }
997
+
998
+ /**
999
+ * Computes the name of a root from the roots that share its basename (including itself).
1000
+ */
1001
+ protected computeRootName(root: URI, peers: URI[]): string {
1002
+ if (peers.length === 1) {
1003
+ return root.path.base || root.path.toString();
1004
+ }
1005
+ const segmentsOf = (uri: URI): string[] => uri.path.toString().split('/').filter(segment => segment.length > 0);
1006
+ const rootSegments = segmentsOf(root);
1007
+ const peerSegments = peers.filter(peer => !peer.isEqual(root)).map(segmentsOf);
1008
+ // A root with a shorter path than a peer, e.g. `/app` next to `/x/app`, is named by its whole path, `app`.
1009
+ const maxLength = Math.max(rootSegments.length, ...peerSegments.map(segments => segments.length));
1010
+ for (let length = 2; length <= maxLength; length++) {
1011
+ const tail = rootSegments.slice(-length).join('/');
1012
+ if (peerSegments.every(segments => segments.slice(-length).join('/') !== tail)) {
1013
+ return tail;
1014
+ }
1015
+ }
1016
+ // The paths are the same, e.g. in different URI schemes, so only the full URI tells them apart.
1017
+ return root.toString();
1018
+ }
1019
+
1020
+ /**
1021
+ * Qualifies a root name that another root name starts with, e.g. `alice` next to `alice/app`, by more
1022
+ * segments of its path, as a path in the one root would otherwise resolve to the other. If the root
1023
+ * named `alice/app` is the `app` folder of the root named `alice`, the names are left as they are:
1024
+ * the nested root is then also the one that {@link getRootPrefixedPath} names its files after.
1025
+ */
1026
+ protected qualifyRootNames(names: Map<string, URI>): Map<string, URI> {
1027
+ const entries = Array.from(names, ([name, root]) => ({ name, root }));
1028
+ let qualified = true;
1029
+ while (qualified) {
1030
+ qualified = false;
1031
+ for (const shorter of entries) {
1032
+ for (const longer of entries) {
1033
+ if (longer.name.startsWith(shorter.name + '/')
1034
+ && !shorter.root.resolve(longer.name.substring(shorter.name.length + 1)).isEqual(longer.root)) {
1035
+ qualified = this.qualifyRootName(shorter) || this.qualifyRootName(longer) || qualified;
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ return new Map(entries.map(({ name, root }) => [name, root]));
1041
+ }
1042
+
1043
+ /**
1044
+ * Adds the next parent segment to the name of a root, unless the name is its whole path or its URI already.
1045
+ *
1046
+ * @returns whether the name was qualified
1047
+ */
1048
+ protected qualifyRootName(entry: { name: string; root: URI }): boolean {
1049
+ const segments = entry.root.path.toString().split('/').filter(segment => segment.length > 0);
1050
+ const length = entry.name.split('/').length;
1051
+ if (entry.name === entry.root.toString() || length >= segments.length) {
1052
+ return false;
1053
+ }
1054
+ entry.name = segments.slice(-(length + 1)).join('/');
1055
+ return true;
1056
+ }
1057
+
844
1058
  areWorkspaceRoots(uris: URI[]): boolean {
845
1059
  if (!uris.length) {
846
1060
  return false;
@@ -22,7 +22,7 @@ import * as sinon from 'sinon';
22
22
  import { Container } from '@theia/core/shared/inversify';
23
23
  import { Event } from '@theia/core/lib/common/event';
24
24
  import { ApplicationShell, WidgetManager } from '@theia/core/lib/browser';
25
- import { DefaultUriLabelProviderContribution } from '@theia/core/lib/browser/label-provider';
25
+ import { DefaultUriLabelProviderContribution, DidChangeLabelEvent } from '@theia/core/lib/browser/label-provider';
26
26
  import { WorkspaceUriLabelProviderContribution } from './workspace-uri-contribution';
27
27
  import URI from '@theia/core/lib/common/uri';
28
28
  import { WorkspaceVariableContribution } from './workspace-variable-contribution';
@@ -40,6 +40,12 @@ after(() => disableJSDOM());
40
40
  let container: Container;
41
41
  let labelProvider: WorkspaceUriLabelProviderContribution;
42
42
  let roots: FileStat[];
43
+ let workspaceService: WorkspaceService;
44
+ /** Changes the roots, as `WorkspaceService.updateRoots` does, which resets their names. */
45
+ const setRoots = (newRoots: FileStat[]): void => {
46
+ roots = newRoots;
47
+ workspaceService['rootNamesCache'] = undefined;
48
+ };
43
49
  beforeEach(() => {
44
50
  roots = [FileStat.dir('file:///workspace')];
45
51
 
@@ -53,7 +59,7 @@ beforeEach(() => {
53
59
  onDidCreateWidget: Event.None
54
60
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
61
  } as any);
56
- const workspaceService = new WorkspaceService();
62
+ workspaceService = new WorkspaceService();
57
63
  workspaceService.tryGetRoots = () => roots;
58
64
  container.bind(WorkspaceService).toConstantValue(workspaceService);
59
65
  container.bind(WorkspaceVariableContribution).toSelf().inSingletonScope();
@@ -138,6 +144,42 @@ describe('WorkspaceUriLabelProviderContribution class', () => {
138
144
  const name = labelProvider.getName(file);
139
145
  expect(name).eq('jacques.doc');
140
146
  });
147
+
148
+ it('should return the folder name of a workspace root', () => {
149
+ setRoots([FileStat.dir('file:///home/alice/app'), FileStat.dir('file:///home/bob/lib')]);
150
+ expect(labelProvider.getName(FileStat.dir('file:///home/alice/app'))).eq('app');
151
+ });
152
+
153
+ it('should return the unique names of workspace roots sharing a folder name', () => {
154
+ setRoots([FileStat.dir('file:///home/alice/app'), FileStat.dir('file:///home/bob/app')]);
155
+ expect(labelProvider.getName(FileStat.dir('file:///home/alice/app'))).eq('alice/app');
156
+ expect(labelProvider.getName(new URI('file:///home/bob/app'))).eq('bob/app');
157
+ });
158
+
159
+ it('should return the folder name of a folder that shares its name with a workspace root', () => {
160
+ setRoots([FileStat.dir('file:///home/alice/app'), FileStat.dir('file:///home/bob/app')]);
161
+ expect(labelProvider.getName(FileStat.dir('file:///home/alice/app/app'))).eq('app');
162
+ });
163
+
164
+ it('should notify a change of the name of a workspace root when a root sharing its folder name is added', () => {
165
+ setRoots([FileStat.dir('file:///home/alice/app'), FileStat.dir('file:///home/other')]);
166
+ workspaceService['onWorkspaceChangeEmitter'].fire(roots);
167
+ const events: DidChangeLabelEvent[] = [];
168
+ labelProvider.onDidChange(event => events.push(event));
169
+ setRoots([...roots, FileStat.dir('file:///home/bob/app')]);
170
+ workspaceService['onWorkspaceChangeEmitter'].fire(roots);
171
+ expect(events.some(event => event.affects(new URI('file:///home/alice/app')))).to.be.true;
172
+ expect(events.some(event => event.affects(FileStat.dir('file:///home/alice/app')))).to.be.true;
173
+ expect(events.some(event => event.affects(new URI('file:///home/other')))).to.be.false;
174
+ expect(labelProvider.getName(new URI('file:///home/alice/app'))).eq('alice/app');
175
+ });
176
+ });
177
+
178
+ describe('getDetails()', () => {
179
+ it('should prefix the path of a file with the unique name of its workspace root', () => {
180
+ setRoots([FileStat.dir('file:///home/alice/app'), FileStat.dir('file:///home/bob/app')]);
181
+ expect(labelProvider.getDetails(new URI('file:///home/bob/app/src/index.ts'))).eq('bob/app • src');
182
+ });
141
183
  });
142
184
 
143
185
  describe('getLongName()', () => {
@@ -176,7 +218,7 @@ describe('WorkspaceUriLabelProviderContribution class', () => {
176
218
  });
177
219
 
178
220
  it('should return the path of a file if WorkspaceService returns no roots', () => {
179
- roots = [];
221
+ setRoots([]);
180
222
  const file = new URI('file:///tmp/prout.txt');
181
223
  const longName = labelProvider.getLongName(file);
182
224
 
@@ -27,9 +27,42 @@ export class WorkspaceUriLabelProviderContribution extends DefaultUriLabelProvid
27
27
  @inject(WorkspaceVariableContribution) protected readonly workspaceVariable: WorkspaceVariableContribution;
28
28
  @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
29
29
 
30
+ /**
31
+ * The names of the workspace roots by root URI, as last labeled.
32
+ */
33
+ protected rootNames = new Map<string, string>();
34
+
30
35
  @postConstruct()
31
36
  override init(): void {
32
- // no-op, backward compatibility
37
+ this.rootNames = this.getRootNamesByUri();
38
+ this.workspaceService.onWorkspaceChanged(() => this.updateRootNames());
39
+ }
40
+
41
+ protected getRootNamesByUri(): Map<string, string> {
42
+ return new Map(Array.from(this.workspaceService.getRootNames(), ([name, uri]) => [uri.toString(), name]));
43
+ }
44
+
45
+ /**
46
+ * Notifies a change of the labels of the roots whose names changed, such as a root named `app` that
47
+ * is named `alice/app` once a root `bob/app` is added.
48
+ */
49
+ protected updateRootNames(): void {
50
+ const rootNames = this.getRootNamesByUri();
51
+ const changed = new Set<string>();
52
+ for (const [uri, name] of rootNames) {
53
+ if (this.rootNames.get(uri) !== name) {
54
+ changed.add(uri);
55
+ }
56
+ }
57
+ this.rootNames = rootNames;
58
+ if (changed.size > 0) {
59
+ this.onDidChangeEmitter.fire({
60
+ affects: element => {
61
+ const uri = this.canHandle(element) > 0 ? this.getUri(element as URI | URIIconReference | FileStat) : undefined;
62
+ return !!uri && changed.has(uri.toString());
63
+ }
64
+ });
65
+ }
33
66
  }
34
67
 
35
68
  override canHandle(element: object): number {
@@ -43,7 +76,16 @@ export class WorkspaceUriLabelProviderContribution extends DefaultUriLabelProvid
43
76
  return super.getIcon(this.asURIIconReference(element));
44
77
  }
45
78
 
79
+ /**
80
+ * Names a workspace root by its unique name (see {@link WorkspaceService.getRootNames}), so that
81
+ * roots sharing a folder name can be told apart, e.g. `alice/app` and `bob/app`.
82
+ */
46
83
  override getName(element: URI | URIIconReference | FileStat): string | undefined {
84
+ const uri = this.getUri(element);
85
+ const rootName = uri && this.workspaceService.getRootName(uri);
86
+ if (rootName) {
87
+ return rootName;
88
+ }
47
89
  return super.getName(this.asURIIconReference(element));
48
90
  }
49
91