Skip to content

Commit 37efd5c

Browse files
committed
resources: new APIs: relativePath, hasTrailingPathSeparator, removeTrailingPathSeparator
1 parent 33ef689 commit 37efd5c

3 files changed

Lines changed: 151 additions & 21 deletions

File tree

src/vs/base/common/resources.ts

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,32 +35,32 @@ export function isEqualOrParent(base: URI, parentCandidate: URI, ignoreCase = ha
3535
if (base.scheme === Schemas.file) {
3636
return extpath.isEqualOrParent(fsPath(base), fsPath(parentCandidate), ignoreCase);
3737
}
38-
if (isEqualAuthority(base.authority, parentCandidate.authority, ignoreCase)) {
38+
if (isEqualAuthority(base.authority, parentCandidate.authority)) {
3939
return extpath.isEqualOrParent(base.path, parentCandidate.path, ignoreCase, '/');
4040
}
4141
}
4242
return false;
4343
}
4444

45-
function isEqualAuthority(a1: string, a2: string, ignoreCase?: boolean) {
46-
return a1 === a2 || ignoreCase && a1 && a2 && equalsIgnoreCase(a1, a2);
45+
function isEqualAuthority(a1: string, a2: string) {
46+
return a1 === a2 || equalsIgnoreCase(a1, a2);
4747
}
4848

4949
export function isEqual(first: URI | undefined, second: URI | undefined, ignoreCase = hasToIgnoreCase(first)): boolean {
50-
const identityEquals = (first === second);
51-
if (identityEquals) {
50+
if (first === second) {
5251
return true;
5352
}
5453

5554
if (!first || !second) {
5655
return false;
5756
}
5857

59-
if (ignoreCase) {
60-
return equalsIgnoreCase(first.toString(), second.toString());
58+
if (first.scheme !== second.scheme || !isEqualAuthority(first.authority, second.authority)) {
59+
return false;
6160
}
6261

63-
return first.toString() === second.toString();
62+
const p1 = first.path || '/', p2 = second.path || '/';
63+
return p1 === p2 || ignoreCase && equalsIgnoreCase(p1 || '/', p2 || '/');
6464
}
6565

6666
export function basename(resource: URI): string {
@@ -106,7 +106,7 @@ export function joinPath(resource: URI, ...pathFragment: string[]): URI {
106106
if (resource.scheme === Schemas.file) {
107107
joinedPath = URI.file(paths.join(fsPath(resource), ...pathFragment)).path;
108108
} else {
109-
joinedPath = paths.posix.join(resource.path, ...pathFragment);
109+
joinedPath = paths.posix.join(resource.path || '/', ...pathFragment);
110110
}
111111
return resource.with({
112112
path: joinedPath
@@ -165,10 +165,48 @@ export function fsPath(uri: URI): string {
165165
* Returns true if the URI path is absolute.
166166
*/
167167
export function isAbsolutePath(resource: URI): boolean {
168+
return !!resource.path && resource.path[0] === '/';
169+
}
170+
171+
/**
172+
* Returns true if the URI path has a trailing path separator
173+
*/
174+
export function hasTrailingPathSeparator(resource: URI): boolean {
168175
if (resource.scheme === Schemas.file) {
169-
return paths.isAbsolute(fsPath(resource));
176+
const fsp = fsPath(resource);
177+
return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === paths.sep;
178+
} else {
179+
let p = resource.path;
180+
return p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash; // ignore the slash at offset 0
181+
}
182+
}
183+
184+
185+
/**
186+
* Removes a trailing path seperator, if theres one.
187+
* Important: Doesn't remove the first slash, it would make the URI invalid
188+
*/
189+
export function removeTrailingPathSeparator(resource: URI): URI {
190+
if (hasTrailingPathSeparator(resource)) {
191+
return resource.with({ path: resource.path.substr(0, resource.path.length - 1) });
192+
}
193+
return resource;
194+
}
195+
196+
197+
/**
198+
* Returns a relative path between two URIs. If the URIs don't have the same schema or authority, `undefined` is returned.
199+
* The returned relative path always uses forward slashes.
200+
*/
201+
export function relativePath(from: URI, to: URI): string | undefined {
202+
if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) {
203+
return undefined;
204+
}
205+
if (from.scheme === Schemas.file) {
206+
const relativePath = paths.relative(from.path, to.path);
207+
return isWindows ? extpath.toForwardSlashes(relativePath) : relativePath;
170208
}
171-
return paths.posix.isAbsolute(resource.path);
209+
return paths.posix.relative(from.path || '/', to.path || '/');
172210
}
173211

174212
export function distinctParents<T>(items: T[], resourceAccessor: (item: T) => URI): T[] {

src/vs/base/test/common/resources.test.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55
import * as assert from 'assert';
6-
import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, hasToIgnoreCase, normalizePath, isAbsolutePath, isMalformedFileUri } from 'vs/base/common/resources';
6+
import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, hasToIgnoreCase, normalizePath, isAbsolutePath, isMalformedFileUri, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator } from 'vs/base/common/resources';
77
import { URI, setUriThrowOnMissingScheme } from 'vs/base/common/uri';
88
import { isWindows } from 'vs/base/common/platform';
99

@@ -166,6 +166,102 @@ suite('Resources', () => {
166166
assert.equal(isAbsolutePath(URI.parse('foo://a/foo/.')), true);
167167
});
168168

169+
function assertTrailingSeparator(u1: URI, expected: boolean) {
170+
assert.equal(hasTrailingPathSeparator(u1), expected, u1.toString());
171+
}
172+
173+
function assertRemoveTrailingSeparator(u1: URI, expected: URI) {
174+
assertEqualURI(removeTrailingPathSeparator(u1), expected, u1.toString());
175+
}
176+
177+
test('trailingPathSeparator', () => {
178+
assertTrailingSeparator(URI.parse('foo://a/foo'), false);
179+
assertTrailingSeparator(URI.parse('foo://a/foo/'), true);
180+
assertTrailingSeparator(URI.parse('foo://a/'), false);
181+
assertTrailingSeparator(URI.parse('foo://a'), false);
182+
183+
assertRemoveTrailingSeparator(URI.parse('foo://a/foo'), URI.parse('foo://a/foo'));
184+
assertRemoveTrailingSeparator(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo'));
185+
assertRemoveTrailingSeparator(URI.parse('foo://a/'), URI.parse('foo://a/'));
186+
assertRemoveTrailingSeparator(URI.parse('foo://a'), URI.parse('foo://a'));
187+
188+
if (isWindows) {
189+
assertTrailingSeparator(URI.file('c:\\a\\foo'), false);
190+
assertTrailingSeparator(URI.file('c:\\a\\foo\\'), true);
191+
assertTrailingSeparator(URI.file('c:\\'), false);
192+
assertTrailingSeparator(URI.file('\\\\server\\share\\some\\'), true);
193+
assertTrailingSeparator(URI.file('\\\\server\\share\\'), false);
194+
195+
assertRemoveTrailingSeparator(URI.file('c:\\a\\foo'), URI.file('c:\\a\\foo'));
196+
assertRemoveTrailingSeparator(URI.file('c:\\a\\foo\\'), URI.file('c:\\a\\foo'));
197+
assertRemoveTrailingSeparator(URI.file('c:\\'), URI.file('c:\\'));
198+
assertRemoveTrailingSeparator(URI.file('\\\\server\\share\\some\\'), URI.file('\\\\server\\share\\some'));
199+
assertRemoveTrailingSeparator(URI.file('\\\\server\\share\\'), URI.file('\\\\server\\share\\'));
200+
} else {
201+
assertTrailingSeparator(URI.file('/foo/bar'), false);
202+
assertTrailingSeparator(URI.file('/foo/bar/'), true);
203+
assertTrailingSeparator(URI.file('/'), false);
204+
205+
assertRemoveTrailingSeparator(URI.file('/foo/bar'), URI.file('/foo/bar'));
206+
assertRemoveTrailingSeparator(URI.file('/foo/bar/'), URI.file('/foo/bar'));
207+
assertRemoveTrailingSeparator(URI.file('/'), URI.file('/'));
208+
}
209+
});
210+
211+
function assertEqualURI(actual: URI, expected: URI, message?: string) {
212+
if (!isEqual(expected, actual)) {
213+
assert.equal(expected.toString(), actual.toString(), message);
214+
}
215+
}
216+
217+
function assertRelativePath(u1: URI, u2: URI, expectedPath: string | undefined, ignoreJoin?: boolean) {
218+
assert.equal(relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`);
219+
if (expectedPath !== undefined && !ignoreJoin) {
220+
assertEqualURI(removeTrailingPathSeparator(joinPath(u1, expectedPath)), removeTrailingPathSeparator(u2), 'joinPath on relativePath should be equal');
221+
}
222+
}
223+
224+
test('relativePath', () => {
225+
assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/bar'), 'bar');
226+
assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/bar/'), 'bar');
227+
assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/bar/goo'), 'bar/goo');
228+
assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/foo/bar/goo'), 'foo/bar/goo');
229+
assertRelativePath(URI.parse('foo://a/foo/xoo'), URI.parse('foo://a/foo/bar'), '../bar');
230+
assertRelativePath(URI.parse('foo://a/foo/xoo/yoo'), URI.parse('foo://a'), '../../..');
231+
assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/'), '');
232+
assertRelativePath(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo'), '');
233+
assertRelativePath(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo/'), '');
234+
assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo'), '');
235+
assertRelativePath(URI.parse('foo://a'), URI.parse('foo://a'), '');
236+
assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/'), '');
237+
assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a'), '');
238+
assertRelativePath(URI.parse('foo://a/foo?q'), URI.parse('foo://a/foo/bar#h'), 'bar');
239+
assertRelativePath(URI.parse('foo://'), URI.parse('foo://a/b'), undefined);
240+
assertRelativePath(URI.parse('foo://a2/b'), URI.parse('foo://a/b'), undefined);
241+
assertRelativePath(URI.parse('goo://a/b'), URI.parse('foo://a/b'), undefined);
242+
243+
if (isWindows) {
244+
assertRelativePath(URI.file('c:\\foo\\bar'), URI.file('c:\\foo\\bar'), '');
245+
assertRelativePath(URI.file('c:\\foo\\bar\\huu'), URI.file('c:\\foo\\bar'), '..');
246+
assertRelativePath(URI.file('c:\\foo\\bar\\a1\\a2'), URI.file('c:\\foo\\bar'), '../..');
247+
assertRelativePath(URI.file('c:\\foo\\bar\\'), URI.file('c:\\foo\\bar\\a1\\a2'), 'a1/a2');
248+
assertRelativePath(URI.file('c:\\foo\\bar\\'), URI.file('c:\\foo\\bar\\a1\\a2\\'), 'a1/a2');
249+
assertRelativePath(URI.file('c:\\'), URI.file('c:\\foo\\bar'), 'foo/bar');
250+
assertRelativePath(URI.file('\\\\server\\share\\some\\'), URI.file('\\\\server\\share\\some\\path'), 'path');
251+
assertRelativePath(URI.file('\\\\server\\share\\some\\'), URI.file('\\\\server\\share2\\some\\path'), '../../share2/some/path', true); // ignore joinPath assert: path.join is not root aware
252+
} else {
253+
assertRelativePath(URI.file('/a/foo'), URI.file('/a/foo/bar'), 'bar');
254+
assertRelativePath(URI.file('/a/foo'), URI.file('/a/foo/bar/'), 'bar');
255+
assertRelativePath(URI.file('/a/foo'), URI.file('/a/foo/bar/goo'), 'bar/goo');
256+
assertRelativePath(URI.file('/a/'), URI.file('/a/foo/bar/goo'), 'foo/bar/goo');
257+
assertRelativePath(URI.file('/'), URI.file('/a/foo/bar/goo'), 'a/foo/bar/goo');
258+
assertRelativePath(URI.file('/a/foo/xoo'), URI.file('/a/foo/bar'), '../bar');
259+
assertRelativePath(URI.file('/a/foo/xoo/yoo'), URI.file('/a'), '../../..');
260+
assertRelativePath(URI.file('/a/foo'), URI.file('/a/foo/'), '');
261+
assertRelativePath(URI.file('/a/foo'), URI.file('/b/foo/'), '../../b/foo');
262+
}
263+
});
264+
169265
test('isEqual', () => {
170266
let fileURI = isWindows ? URI.file('c:\\foo\\bar') : URI.file('/foo/bar');
171267
let fileURI2 = isWindows ? URI.file('C:\\foo\\Bar') : URI.file('/foo/Bar');
@@ -184,6 +280,8 @@ suite('Resources', () => {
184280
assert.equal(isEqual(fileURI3, fileURI4, false), false);
185281

186282
assert.equal(isEqual(fileURI, fileURI3, true), false);
283+
284+
assert.equal(isEqual(URI.parse('foo://server'), URI.parse('foo://server/')), true);
187285
});
188286

189287
test('isEqualOrParent', () => {

src/vs/code/electron-main/windows.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ import { normalizeNFC } from 'vs/base/common/normalization';
3434
import { URI } from 'vs/base/common/uri';
3535
import { Queue, timeout } from 'vs/base/common/async';
3636
import { exists } from 'vs/base/node/pfs';
37-
import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, fsPath } from 'vs/base/common/resources';
38-
import { endsWith } from 'vs/base/common/strings';
37+
import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, fsPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources';
3938
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
4039
import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/code/electron-main/windowsStateStorage';
4140

@@ -979,13 +978,8 @@ export class WindowsManager implements IWindowsMainService {
979978

980979

981980
// remove trailing slash
982-
const uriPath = uri.path;
983-
984-
if (endsWith(uriPath, '/')) {
985-
if (uriPath.length > 2) {
986-
// only remove if the path has some content
987-
uri = uri.with({ path: uriPath.substr(0, uriPath.length - 1) });
988-
}
981+
if (hasTrailingPathSeparator(uri)) {
982+
uri = removeTrailingPathSeparator(uri);
989983
if (!typeHint) {
990984
typeHint = 'folder';
991985
}

0 commit comments

Comments
 (0)