diff --git a/src/vs/base/common/paths.ts b/src/vs/base/common/paths.ts index d96b7517082474926679b495c90231f0cee508c6..d18519a7731b6207dcc55f2bce3e767c9af5ad12 100644 --- a/src/vs/base/common/paths.ts +++ b/src/vs/base/common/paths.ts @@ -17,6 +17,11 @@ export const sep = '/'; */ export const nativeSep = isWindows ? '\\' : '/'; + +function isPathSeparator(code: number) { + return code === CharCode.Slash || code === CharCode.Backslash; +} + /** * @param path the path to get the dirname from * @param separator the separator to use @@ -24,20 +29,32 @@ export const nativeSep = isWindows ? '\\' : '/'; * */ export function dirname(path: string, separator = nativeSep): string { - const idx = ~path.lastIndexOf('/') || ~path.lastIndexOf('\\'); - if (idx === 0) { + const len = path.length; + if (len === 0) { return '.'; - } else if (~idx === 0) { - return path[0]; - } else if (~idx === path.length - 1) { - return dirname(path.substring(0, path.length - 1)); - } else { - let res = path.substring(0, ~idx); - if (isWindows && res[res.length - 1] === ':') { - res += separator; // make sure drive letters end with backslash + } else if (len === 1) { + return isPathSeparator(path.charCodeAt(0)) ? path : '.'; + } + const root = getRoot(path, separator); + let rootLength = root.length; + if (rootLength >= len) { + return root; // matched the root + } + if (rootLength === 0 && isPathSeparator(path.charCodeAt(0))) { + rootLength = 1; // absolute paths stay absolute paths. + } + + let i = len - 1; + if (i > rootLength) { + i--; // no need to look at the last character. If it's a trailing slash, we ignore it. + while (i > rootLength && !isPathSeparator(path.charCodeAt(i))) { + i--; } - return res; } + if (i === 0) { + return '.'; // it was a relative path with a single segment, no root. Nodejs returns '.' here. + } + return path.substr(0, i); } /** diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index d3170eb406bf400b9fe2c7831854880f423f4c18..a10a3b01e74bc1f155175f5f7e66ef95baca2165 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -76,6 +76,9 @@ export function dirname(resource: URI): URI | null { if (resource.scheme === Schemas.file) { return URI.file(paths.dirname(fsPath(resource))); } + if (resource.path.length === 0) { + return resource; + } let dirname = paths.dirname(resource.path, '/'); if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) { return null; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character diff --git a/src/vs/base/test/common/paths.test.ts b/src/vs/base/test/common/paths.test.ts index e0cf182e378034961fc3ddd5b3bc74f7a91ff270..835e7e5d2ef9138229ed04b11d6781e3bd6d8cb3 100644 --- a/src/vs/base/test/common/paths.test.ts +++ b/src/vs/base/test/common/paths.test.ts @@ -8,21 +8,34 @@ import * as platform from 'vs/base/common/platform'; suite('Paths', () => { - test('dirname', () => { - assert.equal(paths.dirname('foo/bar'), 'foo'); - assert.equal(paths.dirname('foo\\bar'), 'foo'); - assert.equal(paths.dirname('/foo/bar'), '/foo'); - assert.equal(paths.dirname('\\foo\\bar'), '\\foo'); - assert.equal(paths.dirname('/foo'), '/'); - assert.equal(paths.dirname('\\foo'), '\\'); - assert.equal(paths.dirname('/'), '/'); - assert.equal(paths.dirname('\\'), '\\'); - assert.equal(paths.dirname('foo'), '.'); - assert.equal(paths.dirname('/folder/'), '/'); - if (platform.isWindows) { - assert.equal(paths.dirname('c:\\some\\file.txt'), 'c:\\some'); - assert.equal(paths.dirname('c:\\some'), 'c:\\'); + function assertDirname(path: string, expected: string, win = false) { + const actual = paths.dirname(path, win ? '\\' : '/'); + + if (actual !== expected) { + assert.fail(`${path}: expected: ${expected}, ours: ${actual}`); } + } + + test('dirname', () => { + assertDirname('foo/bar', 'foo'); + assertDirname('foo\\bar', 'foo', true); + assertDirname('/foo/bar', '/foo'); + assertDirname('\\foo\\bar', '\\foo', true); + assertDirname('/foo', '/'); + assertDirname('\\foo', '\\', true); + assertDirname('/', '/'); + assertDirname('\\', '\\', true); + assertDirname('foo', '.'); + assertDirname('f', '.'); + assertDirname('f/', '.'); + assertDirname('/folder/', '/'); + assertDirname('c:\\some\\file.txt', 'c:\\some', true); + assertDirname('c:\\some', 'c:\\', true); + assertDirname('c:\\', 'c:\\', true); + assertDirname('c:', 'c:', true); + assertDirname('\\\\server\\share\\some\\path', '\\\\server\\share\\some', true); + assertDirname('\\\\server\\share\\some', '\\\\server\\share\\', true); + assertDirname('\\\\server\\share\\', '\\\\server\\share\\', true); }); test('normalize', () => { diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 919855add12b5c4c6b09393add21e512d315ed97..9816adbbbe980dbb1edf78b458a0bc0a274aeae0 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.test.ts @@ -47,6 +47,7 @@ suite('Resources', () => { assert.equal(dirname(URI.file('c:\\some\\file\\'))!.toString(), 'file:///c%3A/some'); assert.equal(dirname(URI.file('c:\\some'))!.toString(), 'file:///c%3A/'); assert.equal(dirname(URI.file('C:\\some'))!.toString(), 'file:///c%3A/'); + assert.equal(dirname(URI.file('c:\\'))!.toString(), 'file:///c%3A/'); } else { assert.equal(dirname(URI.file('/some/file/test.txt'))!.toString(), 'file:///some/file'); assert.equal(dirname(URI.file('/some/file/'))!.toString(), 'file:///some'); @@ -56,6 +57,8 @@ suite('Resources', () => { assert.equal(dirname(URI.parse('foo://a/some/file/'))!.toString(), 'foo://a/some'); assert.equal(dirname(URI.parse('foo://a/some/file'))!.toString(), 'foo://a/some'); assert.equal(dirname(URI.parse('foo://a/some'))!.toString(), 'foo://a/'); + assert.equal(dirname(URI.parse('foo://a/'))!.toString(), 'foo://a/'); + assert.equal(dirname(URI.parse('foo://a'))!.toString(), 'foo://a'); // does not explode (https://github.com/Microsoft/vscode/issues/41987) dirname(URI.from({ scheme: 'file', authority: '/users/someone/portal.h' }));