Skip to content

Commit 3a60df0

Browse files
committed
add IExtUri, ExtUri default implementation, and IUriIdentityService, microsoft#93368
1 parent 3d30e1a commit 3a60df0

8 files changed

Lines changed: 301 additions & 30 deletions

File tree

src/vs/base/common/resources.ts

Lines changed: 78 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,84 @@ import { CharCode } from 'vs/base/common/charCode';
1313
import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob';
1414
import { TernarySearchTree } from 'vs/base/common/map';
1515

16+
//#region IExtUri
17+
18+
export interface IExtUri {
19+
20+
/**
21+
* Compares two uris.
22+
*
23+
* @param uri1 Uri
24+
* @param uri2 Uri
25+
* @param ignoreFragment Ignore the fragment (defaults to `false`)
26+
*/
27+
compare(uri1: URI, uri2: URI, ignoreFragment?: boolean): number;
28+
29+
/**
30+
* Tests whether two uris are equal
31+
*
32+
* @param uri1 Uri
33+
* @param uri2 Uri
34+
* @param ignoreFragment Ignore the fragment (defaults to `false`)
35+
*/
36+
isEqual(uri1: URI, uri2: URI, ignoreFragment?: boolean): boolean;
37+
38+
/**
39+
* Creates a key from a resource URI to be used to resource comparison and for resource maps.
40+
* @see ResourceMap
41+
* @param uri Uri
42+
* @param ignoreFragment Ignore the fragment (defaults to `false`)
43+
*/
44+
getComparisonKey(uri: URI, ignoreFragment?: boolean): string;
45+
}
46+
47+
export class ExtUri implements IExtUri {
48+
49+
constructor(private _ignorePathCasing: (uri: URI) => boolean) { }
50+
51+
compare(uri1: URI, uri2: URI, ignoreFragment: boolean = false): number {
52+
// scheme
53+
let ret = strCompare(uri1.scheme, uri2.scheme);
54+
if (ret === 0) {
55+
// authority
56+
ret = compareIgnoreCase(uri1.authority, uri2.authority);
57+
if (ret === 0) {
58+
// path
59+
ret = this._ignorePathCasing(uri1) ? compareIgnoreCase(uri1.path, uri2.path) : strCompare(uri1.path, uri2.path);
60+
// query
61+
if (ret === 0) {
62+
ret = strCompare(uri1.query, uri2.query);
63+
// fragment
64+
if (ret === 0 && !ignoreFragment) {
65+
ret = strCompare(uri1.fragment, uri2.fragment);
66+
}
67+
}
68+
}
69+
}
70+
return ret;
71+
}
72+
73+
getComparisonKey(uri: URI, ignoreFragment: boolean = false): string {
74+
return getComparisonKey(uri, this._ignorePathCasing(uri), ignoreFragment);
75+
}
76+
77+
isEqual(uri1: URI, uri2: URI, ignoreFragment: boolean = false): boolean {
78+
return isEqual(uri1, uri2, this._ignorePathCasing(uri1), ignoreFragment);
79+
}
80+
}
81+
82+
/**
83+
* Unbiased utility that takes uris "as they are". This means it can be interchanged with
84+
* uri#toString() usages. The following is true
85+
* ```
86+
* assertEqual(aUri.toString() === bUri.toString(), exturi.isEqual(aUri, bUri))
87+
* ```
88+
*/
89+
export const exturi = new ExtUri(() => false);
90+
91+
92+
//#endregion
93+
1694
export function originalFSPath(uri: URI): string {
1795
return uriToFsPath(uri, true);
1896
}
@@ -60,27 +138,6 @@ export function isEqual(first: URI | undefined, second: URI | undefined, ignoreP
60138
return (p1 === p2 || ignorePathCasing && equalsIgnoreCase(p1, p2)) && first.query === second.query && (ignoreFragment || first.fragment === second.fragment);
61139
}
62140

63-
export function compare(uri1: URI, uri2: URI, ignorePathCasing: boolean = _ignorePathCasingGuess(uri1), ignoreFragment: boolean = false): number {
64-
// scheme
65-
let ret = strCompare(uri1.scheme, uri2.scheme);
66-
if (ret === 0) {
67-
// authority
68-
ret = compareIgnoreCase(uri1.authority, uri2.authority);
69-
if (ret === 0) {
70-
// path
71-
ret = ignorePathCasing ? compareIgnoreCase(uri1.path, uri2.path) : strCompare(uri1.path, uri2.path);
72-
// query
73-
if (ret === 0) {
74-
ret = strCompare(uri1.query, uri2.query);
75-
// fragment
76-
if (ret === 0 && !ignoreFragment) {
77-
ret = strCompare(uri1.fragment, uri2.fragment);
78-
}
79-
}
80-
}
81-
}
82-
return ret;
83-
}
84141

85142
/**
86143
* Tests whether a `candidate` URI is a parent or equal of a given `base` URI.

src/vs/base/test/common/resources.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55
import * as assert from 'assert';
6-
import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, getComparisonKey, compare } from 'vs/base/common/resources';
6+
import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, getComparisonKey, exturi } from 'vs/base/common/resources';
77
import { URI } from 'vs/base/common/uri';
88
import { isWindows } from 'vs/base/common/platform';
99
import { toSlashes } from 'vs/base/common/extpath';
@@ -348,8 +348,8 @@ suite('Resources', () => {
348348

349349
function assertIsEqual(u1: URI, u2: URI, ignoreCase: boolean | undefined, expected: boolean) {
350350
assert.equal(isEqual(u1, u2, ignoreCase), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`);
351-
assert.equal(compare(u1, u2, ignoreCase) === 0, expected);
352351
if (!ignoreCase) {
352+
assert.equal(exturi.compare(u1, u2) === 0, expected);
353353
assert.equal(u1.toString() === u2.toString(), expected);
354354
}
355355
assert.equal(getComparisonKey(u1, ignoreCase) === getComparisonKey(u2, ignoreCase), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`);

src/vs/editor/contrib/gotoSymbol/referencesModel.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { localize } from 'vs/nls';
77
import { Event, Emitter } from 'vs/base/common/event';
8-
import { basename, compare, isEqual } from 'vs/base/common/resources';
8+
import { basename, exturi } from 'vs/base/common/resources';
99
import { IDisposable, dispose, IReference, DisposableStore } from 'vs/base/common/lifecycle';
1010
import * as strings from 'vs/base/common/strings';
1111
import { URI } from 'vs/base/common/uri';
@@ -151,7 +151,7 @@ export class ReferencesModel implements IDisposable {
151151

152152
let current: FileReferences | undefined;
153153
for (let link of links) {
154-
if (!current || !isEqual(current.uri, link.uri, false, true)) {
154+
if (!current || !exturi.isEqual(current.uri, link.uri, true)) {
155155
// new group
156156
current = new FileReferences(this, link.uri);
157157
this.groups.push(current);
@@ -281,6 +281,6 @@ export class ReferencesModel implements IDisposable {
281281
}
282282

283283
private static _compareReferences(a: Location, b: Location): number {
284-
return compare(a.uri, b.uri, false, false) || Range.compareRangesUsingStarts(a.range, b.range);
284+
return exturi.compare(a.uri, b.uri) || Range.compareRangesUsingStarts(a.range, b.range);
285285
}
286286
}

src/vs/workbench/contrib/markers/browser/markersModel.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { basename, compare, getComparisonKey } from 'vs/base/common/resources';
6+
import { basename, exturi } from 'vs/base/common/resources';
77
import { URI } from 'vs/base/common/uri';
88
import { Range, IRange } from 'vs/editor/common/core/range';
99
import { IMarker, MarkerSeverity, IRelatedInformation, IMarkerData } from 'vs/platform/markers/common/markers';
@@ -15,7 +15,7 @@ import { withUndefinedAsNull } from 'vs/base/common/types';
1515

1616

1717
export function compareMarkersByUri(a: IMarker, b: IMarker) {
18-
return compare(a.resource, b.resource, false, false);
18+
return exturi.compare(a.resource, b.resource);
1919
}
2020

2121
function compareResourceMarkers(a: ResourceMarkers, b: ResourceMarkers): number {
@@ -148,14 +148,14 @@ export class MarkersModel {
148148
}
149149

150150
getResourceMarkers(resource: URI): ResourceMarkers | null {
151-
return withUndefinedAsNull(this.resourcesByUri.get(getComparisonKey(resource, false, true)));
151+
return withUndefinedAsNull(this.resourcesByUri.get(exturi.getComparisonKey(resource, true)));
152152
}
153153

154154
setResourceMarkers(resourcesMarkers: [URI, IMarker[]][]): void {
155155
const change: MarkerChangesEvent = { added: new Set(), removed: new Set(), updated: new Set() };
156156
for (const [resource, rawMarkers] of resourcesMarkers) {
157157

158-
const key = getComparisonKey(resource, false, true);
158+
const key = exturi.getComparisonKey(resource, true);
159159
let resourceMarkers = this.resourcesByUri.get(key);
160160

161161
if (isNonEmptyArray(rawMarkers)) {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { URI } from 'vs/base/common/uri';
7+
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
8+
import { IExtUri } from 'vs/base/common/resources';
9+
10+
export interface IUriIdentity {
11+
readonly pathHierarchical: boolean;
12+
readonly ignorePathCasing: boolean;
13+
}
14+
15+
export const IUriIdentityService = createDecorator<IUriIdentityService>('IUriIdentityService');
16+
17+
export interface IUriIdentityService {
18+
19+
_serviceBrand: undefined;
20+
21+
/**
22+
* Uri extensions that are aware of casing.
23+
*/
24+
readonly extUri: IExtUri;
25+
26+
/**
27+
* Returns a canonical uri for the given resource. Different uris can point to the same
28+
* resource. That's because of casing or missing normalization, e.g the following uris
29+
* are different but refer to the same document (because windows paths are not case-sensitive)
30+
*
31+
* ```txt
32+
* file:///c:/foo/bar.txt
33+
* file:///c:/FOO/BAR.txt
34+
* ```
35+
*
36+
* This function should be invoked when feeding uris into the system that represent the truth,
37+
* e.g document uris or marker-to-document associations etc. This function should NOT be called
38+
* to pretty print a label nor to sanitize a uri.
39+
*
40+
* Samples:
41+
*
42+
* | in | out | |
43+
* |---|---|---|
44+
* | `file:///foo/bar/../bar` | `file:///foo/bar` | n/a |
45+
* | `file:///foo/bar/../bar#frag` | `file:///foo/bar#frag` | keep fragment |
46+
* | `file:///foo/BAR` | `file:///foo/bar` | assume ignore case |
47+
* | `file:///foo/bar/../BAR?q=2` | `file:///foo/BAR?q=2` | query makes it a different document |
48+
*/
49+
asCanonicalUri(uri: URI): URI;
50+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
7+
import { URI } from 'vs/base/common/uri';
8+
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
9+
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
10+
import { binarySearch } from 'vs/base/common/arrays';
11+
import { ExtUri, IExtUri, normalizePath } from 'vs/base/common/resources';
12+
13+
export class UriIdentityService implements IUriIdentityService {
14+
15+
_serviceBrand: undefined;
16+
17+
readonly extUri: IExtUri;
18+
private _canonicalUris: URI[] = []; // use SkipList or BinaryTree instead of array...
19+
20+
constructor(@IFileService private readonly _fileService: IFileService) {
21+
22+
// assume path casing matters unless the file system provider spec'ed the opposite
23+
const ignorePathCasing = (uri: URI): boolean => {
24+
25+
// perf@jrieken cache this information
26+
if (this._fileService.canHandleResource(uri)) {
27+
return !this._fileService.hasCapability(uri, FileSystemProviderCapabilities.PathCaseSensitive);
28+
}
29+
30+
// this defaults to false which is a good default for
31+
// * virtual documents
32+
// * in-memory uris
33+
// * all kind of "private" schemes
34+
return false;
35+
};
36+
this.extUri = new ExtUri(ignorePathCasing);
37+
}
38+
39+
asCanonicalUri(uri: URI): URI {
40+
41+
// todo@jrieken there is more to it than just comparing
42+
// * ASYNC!?
43+
// * windows 8.3-filenames
44+
// * substr-drives...
45+
// * sym links?
46+
// * fetch real casing?
47+
48+
// (1) normalize URI
49+
if (this._fileService.canHandleResource(uri)) {
50+
uri = normalizePath(uri);
51+
}
52+
53+
// (2) find the uri in its canonical form or use this uri to define it
54+
// perf@jrieken
55+
// * using a SkipList or BinaryTree for faster insertion
56+
const idx = binarySearch(this._canonicalUris, uri, (a, b) => this.extUri.compare(a, b, true));
57+
if (idx >= 0) {
58+
return this._canonicalUris[idx].with({ fragment: uri.fragment });
59+
}
60+
61+
// using slice/concat is faster than splice
62+
const before = this._canonicalUris.slice(0, ~idx);
63+
const after = this._canonicalUris.slice(~idx);
64+
this._canonicalUris = before.concat(uri.with({ fragment: null }), after);
65+
return uri;
66+
}
67+
}
68+
69+
registerSingleton(IUriIdentityService, UriIdentityService, true);

0 commit comments

Comments
 (0)