add IExtUri, ExtUri default implementation, and IUriIdentityService, https://github.com/microsoft/vscode/issues/93368

This commit is contained in:
Johannes Rieken
2020-05-20 16:34:10 +02:00
parent 3d30e1a77e
commit 3a60df0cf3
8 changed files with 301 additions and 30 deletions

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtUri } from 'vs/base/common/resources';
export interface IUriIdentity {
readonly pathHierarchical: boolean;
readonly ignorePathCasing: boolean;
}
export const IUriIdentityService = createDecorator<IUriIdentityService>('IUriIdentityService');
export interface IUriIdentityService {
_serviceBrand: undefined;
/**
* Uri extensions that are aware of casing.
*/
readonly extUri: IExtUri;
/**
* Returns a canonical uri for the given resource. Different uris can point to the same
* resource. That's because of casing or missing normalization, e.g the following uris
* are different but refer to the same document (because windows paths are not case-sensitive)
*
* ```txt
* file:///c:/foo/bar.txt
* file:///c:/FOO/BAR.txt
* ```
*
* This function should be invoked when feeding uris into the system that represent the truth,
* e.g document uris or marker-to-document associations etc. This function should NOT be called
* to pretty print a label nor to sanitize a uri.
*
* Samples:
*
* | in | out | |
* |---|---|---|
* | `file:///foo/bar/../bar` | `file:///foo/bar` | n/a |
* | `file:///foo/bar/../bar#frag` | `file:///foo/bar#frag` | keep fragment |
* | `file:///foo/BAR` | `file:///foo/bar` | assume ignore case |
* | `file:///foo/bar/../BAR?q=2` | `file:///foo/BAR?q=2` | query makes it a different document |
*/
asCanonicalUri(uri: URI): URI;
}

View File

@@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
import { URI } from 'vs/base/common/uri';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { binarySearch } from 'vs/base/common/arrays';
import { ExtUri, IExtUri, normalizePath } from 'vs/base/common/resources';
export class UriIdentityService implements IUriIdentityService {
_serviceBrand: undefined;
readonly extUri: IExtUri;
private _canonicalUris: URI[] = []; // use SkipList or BinaryTree instead of array...
constructor(@IFileService private readonly _fileService: IFileService) {
// assume path casing matters unless the file system provider spec'ed the opposite
const ignorePathCasing = (uri: URI): boolean => {
// perf@jrieken cache this information
if (this._fileService.canHandleResource(uri)) {
return !this._fileService.hasCapability(uri, FileSystemProviderCapabilities.PathCaseSensitive);
}
// this defaults to false which is a good default for
// * virtual documents
// * in-memory uris
// * all kind of "private" schemes
return false;
};
this.extUri = new ExtUri(ignorePathCasing);
}
asCanonicalUri(uri: URI): URI {
// todo@jrieken there is more to it than just comparing
// * ASYNC!?
// * windows 8.3-filenames
// * substr-drives...
// * sym links?
// * fetch real casing?
// (1) normalize URI
if (this._fileService.canHandleResource(uri)) {
uri = normalizePath(uri);
}
// (2) find the uri in its canonical form or use this uri to define it
// perf@jrieken
// * using a SkipList or BinaryTree for faster insertion
const idx = binarySearch(this._canonicalUris, uri, (a, b) => this.extUri.compare(a, b, true));
if (idx >= 0) {
return this._canonicalUris[idx].with({ fragment: uri.fragment });
}
// using slice/concat is faster than splice
const before = this._canonicalUris.slice(0, ~idx);
const after = this._canonicalUris.slice(~idx);
this._canonicalUris = before.concat(uri.with({ fragment: null }), after);
return uri;
}
}
registerSingleton(IUriIdentityService, UriIdentityService, true);

View File

@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService';
import { mock } from 'vs/workbench/test/common/workbenchTestServices';
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
suite('URI Identity', function () {
class FakeFileService extends mock<IFileService>() {
constructor(readonly data: Map<string, FileSystemProviderCapabilities>) {
super();
}
canHandleResource(uri: URI) {
return this.data.has(uri.scheme);
}
hasCapability(uri: URI, flag: FileSystemProviderCapabilities): boolean {
const mask = this.data.get(uri.scheme) ?? 0;
return Boolean(mask & flag);
}
}
let _service: UriIdentityService;
setup(function () {
_service = new UriIdentityService(new FakeFileService(new Map([
['bar', FileSystemProviderCapabilities.PathCaseSensitive],
['foo', 0]
])));
});
function assertCanonical(input: URI, expected: URI, service: UriIdentityService = _service) {
const actual = service.asCanonicalUri(input);
assert.equal(actual.toString(), expected.toString());
assert.ok(service.extUri.isEqual(actual, expected));
}
test('extUri (isEqual)', function () {
let a = URI.parse('foo://bar/bang');
let a1 = URI.parse('foo://bar/BANG');
let b = URI.parse('bar://bar/bang');
let b1 = URI.parse('bar://bar/BANG');
assert.equal(_service.extUri.isEqual(a, a1), true);
assert.equal(_service.extUri.isEqual(a1, a), true);
assert.equal(_service.extUri.isEqual(b, b1), false);
assert.equal(_service.extUri.isEqual(b1, b), false);
});
test('asCanonicalUri (casing)', function () {
let a = URI.parse('foo://bar/bang');
let a1 = URI.parse('foo://bar/BANG');
let b = URI.parse('bar://bar/bang');
let b1 = URI.parse('bar://bar/BANG');
assertCanonical(a, a);
assertCanonical(a1, a);
assertCanonical(b, b);
assertCanonical(b1, b1); // case sensitive
});
test('asCanonicalUri (normalization)', function () {
let a = URI.parse('foo://bar/bang');
assertCanonical(a, a);
assertCanonical(URI.parse('foo://bar/./bang'), a);
assertCanonical(URI.parse('foo://bar/./bang'), a);
assertCanonical(URI.parse('foo://bar/./foo/../bang'), a);
});
test('asCanonicalUri (keep fragement)', function () {
let a = URI.parse('foo://bar/bang');
assertCanonical(a, a);
assertCanonical(URI.parse('foo://bar/./bang#frag'), a.with({ fragment: 'frag' }));
assertCanonical(URI.parse('foo://bar/./bang#frag'), a.with({ fragment: 'frag' }));
assertCanonical(URI.parse('foo://bar/./bang#frag'), a.with({ fragment: 'frag' }));
assertCanonical(URI.parse('foo://bar/./foo/../bang#frag'), a.with({ fragment: 'frag' }));
let b = URI.parse('foo://bar/bazz#frag');
assertCanonical(b, b);
assertCanonical(URI.parse('foo://bar/bazz'), b.with({ fragment: '' }));
assertCanonical(URI.parse('foo://bar/BAZZ#DDD'), b.with({ fragment: 'DDD' })); // lower-case path, but fragment is kept
});
});