diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index bd4ffbac6c18bb45e99908295f3556d2efa58849..7e449cbd744aef83a10a8932e37701e10ea37545 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -35,20 +35,19 @@ export function isEqualOrParent(base: URI, parentCandidate: URI, ignoreCase = ha if (base.scheme === Schemas.file) { return extpath.isEqualOrParent(fsPath(base), fsPath(parentCandidate), ignoreCase); } - if (isEqualAuthority(base.authority, parentCandidate.authority, ignoreCase)) { + if (isEqualAuthority(base.authority, parentCandidate.authority)) { return extpath.isEqualOrParent(base.path, parentCandidate.path, ignoreCase, '/'); } } return false; } -function isEqualAuthority(a1: string, a2: string, ignoreCase?: boolean) { - return a1 === a2 || ignoreCase && a1 && a2 && equalsIgnoreCase(a1, a2); +function isEqualAuthority(a1: string, a2: string) { + return a1 === a2 || equalsIgnoreCase(a1, a2); } export function isEqual(first: URI | undefined, second: URI | undefined, ignoreCase = hasToIgnoreCase(first)): boolean { - const identityEquals = (first === second); - if (identityEquals) { + if (first === second) { return true; } @@ -56,11 +55,12 @@ export function isEqual(first: URI | undefined, second: URI | undefined, ignoreC return false; } - if (ignoreCase) { - return equalsIgnoreCase(first.toString(), second.toString()); + if (first.scheme !== second.scheme || !isEqualAuthority(first.authority, second.authority)) { + return false; } - return first.toString() === second.toString(); + const p1 = first.path || '/', p2 = second.path || '/'; + return p1 === p2 || ignoreCase && equalsIgnoreCase(p1 || '/', p2 || '/'); } export function basename(resource: URI): string { @@ -106,7 +106,7 @@ export function joinPath(resource: URI, ...pathFragment: string[]): URI { if (resource.scheme === Schemas.file) { joinedPath = URI.file(paths.join(fsPath(resource), ...pathFragment)).path; } else { - joinedPath = paths.posix.join(resource.path, ...pathFragment); + joinedPath = paths.posix.join(resource.path || '/', ...pathFragment); } return resource.with({ path: joinedPath @@ -165,10 +165,48 @@ export function fsPath(uri: URI): string { * Returns true if the URI path is absolute. */ export function isAbsolutePath(resource: URI): boolean { + return !!resource.path && resource.path[0] === '/'; +} + +/** + * Returns true if the URI path has a trailing path separator + */ +export function hasTrailingPathSeparator(resource: URI): boolean { if (resource.scheme === Schemas.file) { - return paths.isAbsolute(fsPath(resource)); + const fsp = fsPath(resource); + return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === paths.sep; + } else { + let p = resource.path; + return p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash; // ignore the slash at offset 0 + } +} + + +/** + * Removes a trailing path seperator, if theres one. + * Important: Doesn't remove the first slash, it would make the URI invalid + */ +export function removeTrailingPathSeparator(resource: URI): URI { + if (hasTrailingPathSeparator(resource)) { + return resource.with({ path: resource.path.substr(0, resource.path.length - 1) }); + } + return resource; +} + + +/** + * Returns a relative path between two URIs. If the URIs don't have the same schema or authority, `undefined` is returned. + * The returned relative path always uses forward slashes. + */ +export function relativePath(from: URI, to: URI): string | undefined { + if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) { + return undefined; + } + if (from.scheme === Schemas.file) { + const relativePath = paths.relative(from.path, to.path); + return isWindows ? extpath.toForwardSlashes(relativePath) : relativePath; } - return paths.posix.isAbsolute(resource.path); + return paths.posix.relative(from.path || '/', to.path || '/'); } export function distinctParents(items: T[], resourceAccessor: (item: T) => URI): T[] { diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index b8458df97e078dbaff0bcbd6f1d4abc506c23149..c4fc9cc0f2ec48afbacbe5588d91a39d3a33acef 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, hasToIgnoreCase, normalizePath, isAbsolutePath, isMalformedFileUri } from 'vs/base/common/resources'; +import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, hasToIgnoreCase, normalizePath, isAbsolutePath, isMalformedFileUri, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator } from 'vs/base/common/resources'; import { URI, setUriThrowOnMissingScheme } from 'vs/base/common/uri'; import { isWindows } from 'vs/base/common/platform'; @@ -166,6 +166,102 @@ suite('Resources', () => { assert.equal(isAbsolutePath(URI.parse('foo://a/foo/.')), true); }); + function assertTrailingSeparator(u1: URI, expected: boolean) { + assert.equal(hasTrailingPathSeparator(u1), expected, u1.toString()); + } + + function assertRemoveTrailingSeparator(u1: URI, expected: URI) { + assertEqualURI(removeTrailingPathSeparator(u1), expected, u1.toString()); + } + + test('trailingPathSeparator', () => { + assertTrailingSeparator(URI.parse('foo://a/foo'), false); + assertTrailingSeparator(URI.parse('foo://a/foo/'), true); + assertTrailingSeparator(URI.parse('foo://a/'), false); + assertTrailingSeparator(URI.parse('foo://a'), false); + + assertRemoveTrailingSeparator(URI.parse('foo://a/foo'), URI.parse('foo://a/foo')); + assertRemoveTrailingSeparator(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo')); + assertRemoveTrailingSeparator(URI.parse('foo://a/'), URI.parse('foo://a/')); + assertRemoveTrailingSeparator(URI.parse('foo://a'), URI.parse('foo://a')); + + if (isWindows) { + assertTrailingSeparator(URI.file('c:\\a\\foo'), false); + assertTrailingSeparator(URI.file('c:\\a\\foo\\'), true); + assertTrailingSeparator(URI.file('c:\\'), false); + assertTrailingSeparator(URI.file('\\\\server\\share\\some\\'), true); + assertTrailingSeparator(URI.file('\\\\server\\share\\'), false); + + assertRemoveTrailingSeparator(URI.file('c:\\a\\foo'), URI.file('c:\\a\\foo')); + assertRemoveTrailingSeparator(URI.file('c:\\a\\foo\\'), URI.file('c:\\a\\foo')); + assertRemoveTrailingSeparator(URI.file('c:\\'), URI.file('c:\\')); + assertRemoveTrailingSeparator(URI.file('\\\\server\\share\\some\\'), URI.file('\\\\server\\share\\some')); + assertRemoveTrailingSeparator(URI.file('\\\\server\\share\\'), URI.file('\\\\server\\share\\')); + } else { + assertTrailingSeparator(URI.file('/foo/bar'), false); + assertTrailingSeparator(URI.file('/foo/bar/'), true); + assertTrailingSeparator(URI.file('/'), false); + + assertRemoveTrailingSeparator(URI.file('/foo/bar'), URI.file('/foo/bar')); + assertRemoveTrailingSeparator(URI.file('/foo/bar/'), URI.file('/foo/bar')); + assertRemoveTrailingSeparator(URI.file('/'), URI.file('/')); + } + }); + + function assertEqualURI(actual: URI, expected: URI, message?: string) { + if (!isEqual(expected, actual)) { + assert.equal(expected.toString(), actual.toString(), message); + } + } + + function assertRelativePath(u1: URI, u2: URI, expectedPath: string | undefined, ignoreJoin?: boolean) { + assert.equal(relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`); + if (expectedPath !== undefined && !ignoreJoin) { + assertEqualURI(removeTrailingPathSeparator(joinPath(u1, expectedPath)), removeTrailingPathSeparator(u2), 'joinPath on relativePath should be equal'); + } + } + + test('relativePath', () => { + assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/bar'), 'bar'); + assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/bar/'), 'bar'); + assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/bar/goo'), 'bar/goo'); + assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/foo/bar/goo'), 'foo/bar/goo'); + assertRelativePath(URI.parse('foo://a/foo/xoo'), URI.parse('foo://a/foo/bar'), '../bar'); + assertRelativePath(URI.parse('foo://a/foo/xoo/yoo'), URI.parse('foo://a'), '../../..'); + assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/'), ''); + assertRelativePath(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo'), ''); + assertRelativePath(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo/'), ''); + assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo'), ''); + assertRelativePath(URI.parse('foo://a'), URI.parse('foo://a'), ''); + assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/'), ''); + assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a'), ''); + assertRelativePath(URI.parse('foo://a/foo?q'), URI.parse('foo://a/foo/bar#h'), 'bar'); + assertRelativePath(URI.parse('foo://'), URI.parse('foo://a/b'), undefined); + assertRelativePath(URI.parse('foo://a2/b'), URI.parse('foo://a/b'), undefined); + assertRelativePath(URI.parse('goo://a/b'), URI.parse('foo://a/b'), undefined); + + if (isWindows) { + assertRelativePath(URI.file('c:\\foo\\bar'), URI.file('c:\\foo\\bar'), ''); + assertRelativePath(URI.file('c:\\foo\\bar\\huu'), URI.file('c:\\foo\\bar'), '..'); + assertRelativePath(URI.file('c:\\foo\\bar\\a1\\a2'), URI.file('c:\\foo\\bar'), '../..'); + assertRelativePath(URI.file('c:\\foo\\bar\\'), URI.file('c:\\foo\\bar\\a1\\a2'), 'a1/a2'); + assertRelativePath(URI.file('c:\\foo\\bar\\'), URI.file('c:\\foo\\bar\\a1\\a2\\'), 'a1/a2'); + assertRelativePath(URI.file('c:\\'), URI.file('c:\\foo\\bar'), 'foo/bar'); + assertRelativePath(URI.file('\\\\server\\share\\some\\'), URI.file('\\\\server\\share\\some\\path'), 'path'); + 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 + } else { + assertRelativePath(URI.file('/a/foo'), URI.file('/a/foo/bar'), 'bar'); + assertRelativePath(URI.file('/a/foo'), URI.file('/a/foo/bar/'), 'bar'); + assertRelativePath(URI.file('/a/foo'), URI.file('/a/foo/bar/goo'), 'bar/goo'); + assertRelativePath(URI.file('/a/'), URI.file('/a/foo/bar/goo'), 'foo/bar/goo'); + assertRelativePath(URI.file('/'), URI.file('/a/foo/bar/goo'), 'a/foo/bar/goo'); + assertRelativePath(URI.file('/a/foo/xoo'), URI.file('/a/foo/bar'), '../bar'); + assertRelativePath(URI.file('/a/foo/xoo/yoo'), URI.file('/a'), '../../..'); + assertRelativePath(URI.file('/a/foo'), URI.file('/a/foo/'), ''); + assertRelativePath(URI.file('/a/foo'), URI.file('/b/foo/'), '../../b/foo'); + } + }); + test('isEqual', () => { let fileURI = isWindows ? URI.file('c:\\foo\\bar') : URI.file('/foo/bar'); let fileURI2 = isWindows ? URI.file('C:\\foo\\Bar') : URI.file('/foo/Bar'); @@ -184,6 +280,8 @@ suite('Resources', () => { assert.equal(isEqual(fileURI3, fileURI4, false), false); assert.equal(isEqual(fileURI, fileURI3, true), false); + + assert.equal(isEqual(URI.parse('foo://server'), URI.parse('foo://server/')), true); }); test('isEqualOrParent', () => { diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 4e4f11e70264688b357637034c192dbe80e19298..77ed0fe8b585f68aa489fb77b05f9b00291599f8 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -34,8 +34,7 @@ import { normalizeNFC } from 'vs/base/common/normalization'; import { URI } from 'vs/base/common/uri'; import { Queue, timeout } from 'vs/base/common/async'; import { exists } from 'vs/base/node/pfs'; -import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, fsPath } from 'vs/base/common/resources'; -import { endsWith } from 'vs/base/common/strings'; +import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, fsPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData } from 'vs/code/electron-main/windowsStateStorage'; @@ -979,13 +978,8 @@ export class WindowsManager implements IWindowsMainService { // remove trailing slash - const uriPath = uri.path; - - if (endsWith(uriPath, '/')) { - if (uriPath.length > 2) { - // only remove if the path has some content - uri = uri.with({ path: uriPath.substr(0, uriPath.length - 1) }); - } + if (hasTrailingPathSeparator(uri)) { + uri = removeTrailingPathSeparator(uri); if (!typeHint) { typeHint = 'folder'; }