add IExtUri, ExtUri default implementation, and IUriIdentityService, https://github.com/microsoft/vscode/issues/93368
This commit is contained in:
parent
3d30e1a77e
commit
3a60df0cf3
8 changed files with 301 additions and 30 deletions
|
@ -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)) {
|
||||
|
|
50
src/vs/workbench/services/uriIdentity/common/uriIdentity.ts
Normal file
50
src/vs/workbench/services/uriIdentity/common/uriIdentity.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
|
@ -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
|
||||
});
|
||||
|
||||
});
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue