@theia/workspace 1.77.0-next.6 → 1.77.0-next.63
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.
- package/lib/browser/metadata-storage/workspace-metadata-storage-service.spec.js +3 -2
- package/lib/browser/metadata-storage/workspace-metadata-storage-service.spec.js.map +1 -1
- package/lib/browser/quick-open-workspace.js +1 -1
- package/lib/browser/quick-open-workspace.js.map +1 -1
- package/lib/browser/test/with-workspace-service-defaults.d.ts +8 -0
- package/lib/browser/test/with-workspace-service-defaults.d.ts.map +1 -0
- package/lib/browser/test/with-workspace-service-defaults.js +28 -0
- package/lib/browser/test/with-workspace-service-defaults.js.map +1 -0
- package/lib/browser/workspace-commands.spec.js +2 -1
- package/lib/browser/workspace-commands.spec.js.map +1 -1
- package/lib/browser/workspace-delete-handler.js +1 -1
- package/lib/browser/workspace-delete-handler.js.map +1 -1
- package/lib/browser/workspace-schema-updater.js +1 -1
- package/lib/browser/workspace-schema-updater.js.map +1 -1
- package/lib/browser/workspace-search-filter-service.spec.js +2 -1
- package/lib/browser/workspace-search-filter-service.spec.js.map +1 -1
- package/lib/browser/workspace-service.d.ts +85 -0
- package/lib/browser/workspace-service.d.ts.map +1 -1
- package/lib/browser/workspace-service.js +203 -5
- package/lib/browser/workspace-service.js.map +1 -1
- package/lib/browser/workspace-service.spec.d.ts +2 -0
- package/lib/browser/workspace-service.spec.d.ts.map +1 -0
- package/lib/browser/workspace-service.spec.js +232 -0
- package/lib/browser/workspace-service.spec.js.map +1 -0
- package/lib/browser/workspace-trust-service.js +1 -1
- package/lib/browser/workspace-trust-service.js.map +1 -1
- package/lib/browser/workspace-trust-service.spec.js +3 -2
- package/lib/browser/workspace-trust-service.spec.js.map +1 -1
- package/lib/browser/workspace-uri-contribution.d.ts +14 -0
- package/lib/browser/workspace-uri-contribution.d.ts.map +1 -1
- package/lib/browser/workspace-uri-contribution.js +44 -2
- package/lib/browser/workspace-uri-contribution.js.map +1 -1
- package/lib/browser/workspace-uri-contribution.spec.js +43 -5
- package/lib/browser/workspace-uri-contribution.spec.js.map +1 -1
- package/lib/node/default-workspace-server.js +4 -4
- package/lib/node/default-workspace-server.js.map +1 -1
- package/lib/node/default-workspace-server.spec.js +3 -2
- package/lib/node/default-workspace-server.spec.js.map +1 -1
- package/package.json +5 -5
- package/src/browser/test/with-workspace-service-defaults.ts +26 -0
- package/src/browser/workspace-service.spec.ts +274 -0
- package/src/browser/workspace-service.ts +216 -2
- package/src/browser/workspace-uri-contribution.spec.ts +45 -3
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|