提交 3a60df0c 编写于 作者: J Johannes Rieken

add IExtUri, ExtUri default implementation, and IUriIdentityService,...

add IExtUri, ExtUri default implementation, and IUriIdentityService, https://github.com/microsoft/vscode/issues/93368
上级 3d30e1a7
......@@ -13,6 +13,84 @@ import { CharCode } from 'vs/base/common/charCode';
import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob';
import { TernarySearchTree } from 'vs/base/common/map';
//#region IExtUri
export interface IExtUri {
/**
* Compares two uris.
*
* @param uri1 Uri
* @param uri2 Uri
* @param ignoreFragment Ignore the fragment (defaults to `false`)
*/
compare(uri1: URI, uri2: URI, ignoreFragment?: boolean): number;
/**
* Tests whether two uris are equal
*
* @param uri1 Uri
* @param uri2 Uri
* @param ignoreFragment Ignore the fragment (defaults to `false`)
*/
isEqual(uri1: URI, uri2: URI, ignoreFragment?: boolean): boolean;
/**
* Creates a key from a resource URI to be used to resource comparison and for resource maps.
* @see ResourceMap
* @param uri Uri
* @param ignoreFragment Ignore the fragment (defaults to `false`)
*/
getComparisonKey(uri: URI, ignoreFragment?: boolean): string;
}
export class ExtUri implements IExtUri {
constructor(private _ignorePathCasing: (uri: URI) => boolean) { }
compare(uri1: URI, uri2: URI, ignoreFragment: boolean = false): number {
// scheme
let ret = strCompare(uri1.scheme, uri2.scheme);
if (ret === 0) {
// authority
ret = compareIgnoreCase(uri1.authority, uri2.authority);
if (ret === 0) {
// path
ret = this._ignorePathCasing(uri1) ? compareIgnoreCase(uri1.path, uri2.path) : strCompare(uri1.path, uri2.path);
// query
if (ret === 0) {
ret = strCompare(uri1.query, uri2.query);
// fragment
if (ret === 0 && !ignoreFragment) {
ret = strCompare(uri1.fragment, uri2.fragment);
}
}
}
}
return ret;
}
getComparisonKey(uri: URI, ignoreFragment: boolean = false): string {
return getComparisonKey(uri, this._ignorePathCasing(uri), ignoreFragment);
}
isEqual(uri1: URI, uri2: URI, ignoreFragment: boolean = false): boolean {
return isEqual(uri1, uri2, this._ignorePathCasing(uri1), ignoreFragment);
}
}
/**
* Unbiased utility that takes uris "as they are". This means it can be interchanged with
* uri#toString() usages. The following is true
* ```
* assertEqual(aUri.toString() === bUri.toString(), exturi.isEqual(aUri, bUri))
* ```
*/
export const exturi = new ExtUri(() => false);
//#endregion
export function originalFSPath(uri: URI): string {
return uriToFsPath(uri, true);
}
......@@ -60,27 +138,6 @@ export function isEqual(first: URI | undefined, second: URI | undefined, ignoreP
return (p1 === p2 || ignorePathCasing && equalsIgnoreCase(p1, p2)) && first.query === second.query && (ignoreFragment || first.fragment === second.fragment);
}
export function compare(uri1: URI, uri2: URI, ignorePathCasing: boolean = _ignorePathCasingGuess(uri1), ignoreFragment: boolean = false): number {
// scheme
let ret = strCompare(uri1.scheme, uri2.scheme);
if (ret === 0) {
// authority
ret = compareIgnoreCase(uri1.authority, uri2.authority);
if (ret === 0) {
// path
ret = ignorePathCasing ? compareIgnoreCase(uri1.path, uri2.path) : strCompare(uri1.path, uri2.path);
// query
if (ret === 0) {
ret = strCompare(uri1.query, uri2.query);
// fragment
if (ret === 0 && !ignoreFragment) {
ret = strCompare(uri1.fragment, uri2.fragment);
}
}
}
}
return ret;
}
/**
* Tests whether a `candidate` URI is a parent or equal of a given `base` URI.
......
......@@ -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, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, getComparisonKey, compare } from 'vs/base/common/resources';
import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, getComparisonKey, exturi } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { isWindows } from 'vs/base/common/platform';
import { toSlashes } from 'vs/base/common/extpath';
......@@ -348,8 +348,8 @@ suite('Resources', () => {
function assertIsEqual(u1: URI, u2: URI, ignoreCase: boolean | undefined, expected: boolean) {
assert.equal(isEqual(u1, u2, ignoreCase), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`);
assert.equal(compare(u1, u2, ignoreCase) === 0, expected);
if (!ignoreCase) {
assert.equal(exturi.compare(u1, u2) === 0, expected);
assert.equal(u1.toString() === u2.toString(), expected);
}
assert.equal(getComparisonKey(u1, ignoreCase) === getComparisonKey(u2, ignoreCase), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`);
......
......@@ -5,7 +5,7 @@
import { localize } from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { basename, compare, isEqual } from 'vs/base/common/resources';
import { basename, exturi } from 'vs/base/common/resources';
import { IDisposable, dispose, IReference, DisposableStore } from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
......@@ -151,7 +151,7 @@ export class ReferencesModel implements IDisposable {
let current: FileReferences | undefined;
for (let link of links) {
if (!current || !isEqual(current.uri, link.uri, false, true)) {
if (!current || !exturi.isEqual(current.uri, link.uri, true)) {
// new group
current = new FileReferences(this, link.uri);
this.groups.push(current);
......@@ -281,6 +281,6 @@ export class ReferencesModel implements IDisposable {
}
private static _compareReferences(a: Location, b: Location): number {
return compare(a.uri, b.uri, false, false) || Range.compareRangesUsingStarts(a.range, b.range);
return exturi.compare(a.uri, b.uri) || Range.compareRangesUsingStarts(a.range, b.range);
}
}
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { basename, compare, getComparisonKey } from 'vs/base/common/resources';
import { basename, exturi } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { Range, IRange } from 'vs/editor/common/core/range';
import { IMarker, MarkerSeverity, IRelatedInformation, IMarkerData } from 'vs/platform/markers/common/markers';
......@@ -15,7 +15,7 @@ import { withUndefinedAsNull } from 'vs/base/common/types';
export function compareMarkersByUri(a: IMarker, b: IMarker) {
return compare(a.resource, b.resource, false, false);
return exturi.compare(a.resource, b.resource);
}
function compareResourceMarkers(a: ResourceMarkers, b: ResourceMarkers): number {
......@@ -148,14 +148,14 @@ export class MarkersModel {
}
getResourceMarkers(resource: URI): ResourceMarkers | null {
return withUndefinedAsNull(this.resourcesByUri.get(getComparisonKey(resource, false, true)));
return withUndefinedAsNull(this.resourcesByUri.get(exturi.getComparisonKey(resource, true)));
}
setResourceMarkers(resourcesMarkers: [URI, IMarker[]][]): void {
const change: MarkerChangesEvent = { added: new Set(), removed: new Set(), updated: new Set() };
for (const [resource, rawMarkers] of resourcesMarkers) {
const key = getComparisonKey(resource, false, true);
const key = exturi.getComparisonKey(resource, true);
let resourceMarkers = this.resourcesByUri.get(key);
if (isNonEmptyArray(rawMarkers)) {
......
/*---------------------------------------------------------------------------------------------
* 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;
}
/*---------------------------------------------------------------------------------------------
* 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);
/*---------------------------------------------------------------------------------------------
* 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
});
});
......@@ -53,6 +53,7 @@ import 'vs/workbench/browser/parts/views/viewsService';
//#region --- workbench services
import 'vs/platform/undoRedo/common/undoRedoService';
import 'vs/workbench/services/uriIdentity/common/uriIdentityService';
import 'vs/workbench/services/extensions/browser/extensionUrlHandler';
import 'vs/workbench/services/bulkEdit/browser/bulkEditService';
import 'vs/workbench/services/keybinding/common/keybindingEditing';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册