More control over which collator to use in each situation

This commit is contained in:
Ron Buckton
2017-10-25 16:30:19 -07:00
parent 4d7923af00
commit 130c407708
10 changed files with 296 additions and 143 deletions
+272 -122
View File
@@ -1486,140 +1486,288 @@ namespace ts {
/**
* Compare two values for their order relative to each other.
*/
export function compareValues<T>(a: T | undefined, b: T | undefined) {
if (a === b) return Comparison.EqualTo;
if (a === undefined) return Comparison.LessThan;
if (b === undefined) return Comparison.GreaterThan;
return a < b ? Comparison.LessThan : Comparison.GreaterThan;
export function compareValues<T>(a: T, b: T) {
return a === b ? Comparison.EqualTo :
a === undefined ? Comparison.LessThan :
b === undefined ? Comparison.GreaterThan :
a < b ? Comparison.LessThan :
Comparison.GreaterThan;
}
/**
* Compare two values for their equality.
*/
export function equateValues<T>(a: T | undefined, b: T | undefined) {
export function equateValues<T>(a: T, b: T) {
return a === b;
}
interface StringCollator {
compare(a: string, b: string): number;
equate(a: string, b: string): boolean;
export interface StringCollator {
compare(a: string | undefined, b: string | undefined): number;
equate(a: string | undefined, b: string | undefined): boolean;
}
// Gets string comparers compatible with the current host
function createCaseInsensitiveStringCollator() {
function createIntlStringCollator(): StringCollator {
// Strings that differ in base or accents/diacritic marks compare as unequal.
// An `undefined` locale uses the default locale of the host.
const sortCollator = new Intl.Collator(/*locales*/ undefined, { usage: "sort", sensitivity: "accent" });
const searchCollator = new Intl.Collator(/*locales*/ undefined, { usage: "search", sensitivity: "accent" });
return {
export interface StringCollators {
/**
* Gets a string collator for case-insensitive ordinal comparisons of strings.
*
* Ordinal comparisons are based on the difference between the unicode code points of
* both strings. Characters with multiple unicode representations are considered
* unequal.
*
* Case-insensitive comparisons compare both strings after applying `toUpperCase` to
* each string.
*/
readonly ordinalCaseInsensitive: StringCollator;
/**
* Gets a string collator for case-sensitive ordinal comparisons of strings.
*
* Ordinal comparisons are based on the difference between the unicode code points of
* both strings. Characters with multiple unicode representations are considered
* unequal. They provide predictable ordering, but place "a" after "B".
*/
readonly ordinalCaseSensitive: StringCollator;
/**
* Gets or sets a string collator for case-insensitive comparisons of strings in the host default locale.
*
* UI comparisons are based on the sort order of the host default locale. Ordering is not
* predictable between different host locales, but is best for displaying ordered data
* for UI presentation. Characters with multiple unicode representations may be considered
* equal.
*
* Case-insensitive comparisons compare strings that differ in only base characters or
* accents/diacritic marks as unequal.
*/
readonly uiCaseInsensitive: StringCollator;
/**
* Gets a string collator for case-sensitive comparisons of strings in the host default locale.
*
* UI comparisons are based on the sort order of the host default locale. Ordering is not
* predictable between different host locales, but is best for displaying ordered data
* for UI presentation. Characters with multiple unicode representations may be considered
* equal.
*/
readonly uiCaseSensitive: StringCollator;
/**
* Gets a string collator for case-insensitive comparisons of strings in an invariant locale.
*
* Invariant comparisons are based on the sort order of an invariant locale ('en-US').
* They provide predictable ordering, placing "a" before "B". Characters with multiple
* unicode representations may be considered equal. Invariant comparisons are best used
* when interacting with the file system.
*
* Case-insensitive comparisons compare strings that differ in only base characters or
* accents/diacritic marks as unequal.
*/
readonly invariantCaseInsensitive: StringCollator;
/**
* Gets a string collator for case-sensitive comparisons of strings in an invariant locale.
*
* Invariant comparisons are based on the sort order of an invariant locale ('en-US').
* They provide predictable ordering, placing "a" before "B". Characters with multiple
* unicode representations may be considered equal. Invariant comparisons are best used
* when interacting with the file system.
*/
readonly invariantCaseSensitive: StringCollator;
/**
* Gets or sets the locale for UI collators
*/
uiLocale: string | undefined;
/**
* Creates a `StringCollator` for a specific locale and case sensitivity.
*/
create(locale: string | undefined, caseSensitive: boolean): StringCollator;
/**
* Gets the ordinal `StringCollator` for the provided case sensitivity.
*/
getOrdinalCollator(caseSensitive: boolean): StringCollator;
/**
* Gets the UI `StringCollator` for the provided case sensitivity.
*/
getUICollator(caseSensitive: boolean): StringCollator;
/**
* Gets the invariant `StringCollator` for the provided case sensitivity.
*/
getInvariantCollator(caseSensitive: boolean): StringCollator;
/**
* Gets a `StringCollator` for comparing code fragments for code generation.
*/
getCodeCollator(caseSensitive: boolean): StringCollator;
/**
* Gets a `StringCollator` for comparing paths.
*/
getPathCollator(caseSensitive: boolean): StringCollator;
}
export const StringCollator: StringCollators = (function () {
const invariantLocaleName = "en-US"; // we use en-US for the invariant locale
const create = getStringCollatorFactory();
const ordinalCS: StringCollator = {
compare: compareValues,
equate: equateValues
};
const ordinalCI: StringCollator = {
compare: (a, b) => compareValues(toUpperCase(a), toUpperCase(b)),
equate: (a, b) => toUpperCase(a) === toUpperCase(b)
};
let invariantCI: StringCollator | undefined;
let invariantCS: StringCollator | undefined;
let uiCI: StringCollator | undefined;
let uiCS: StringCollator | undefined;
let uiLocale: string | undefined;
return {
get ordinalCaseInsensitive() { return ordinalCI; },
get ordinalCaseSensitive() { return ordinalCS; },
get uiCaseInsensitive() { return uiCI || (uiCI = create(uiLocale, /*caseInsensitive*/ true)); },
get uiCaseSensitive() { return uiCS || (uiCS = create(uiLocale, /*caseInsensitive*/ false)); },
get invariantCaseInsensitive() { return invariantCI || (invariantCI = create(invariantLocaleName, /*caseInsensitive*/ true)); },
get invariantCaseSensitive() { return invariantCS || (invariantCS = create(invariantLocaleName, /*caseInsensitive*/ false)); },
get uiLocale() { return uiLocale; },
set uiLocale(value) {
if (uiLocale !== value) {
uiLocale = value;
uiCI = undefined;
uiCS = undefined;
}
},
create,
getOrdinalCollator,
getUICollator,
getInvariantCollator,
getCodeCollator: getInvariantCollator,
getPathCollator: getInvariantCollator
};
function getOrdinalCollator(caseInsensitive: boolean) {
return caseInsensitive ? StringCollator.ordinalCaseInsensitive : StringCollator.ordinalCaseSensitive;
}
function getUICollator(caseInsensitive: boolean) {
return caseInsensitive ? StringCollator.uiCaseInsensitive : StringCollator.uiCaseSensitive;
}
function getInvariantCollator(caseInsensitive: boolean) {
return caseInsensitive ? StringCollator.invariantCaseInsensitive : StringCollator.invariantCaseSensitive;
}
function toUpperCase(value: string | undefined): string | undefined {
return value === undefined ? undefined : value.toUpperCase();
}
function compareDefined(a: string, b: string) {
return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo;
}
function compareWithCallback(a: string | undefined, b: string | undefined, comparer: (a: string, b: string) => number) {
return a === b ? Comparison.EqualTo :
a === undefined ? Comparison.LessThan :
b === undefined ? Comparison.GreaterThan :
toComparison(comparer(a, b));
}
function toComparison(value: number) {
return value < 0 ? Comparison.LessThan : value > 0 ? Comparison.GreaterThan : Comparison.EqualTo;
}
function createIntlStringCollator(locale: string | undefined, caseInsensitive: boolean): StringCollator {
// Initialize the sort collator on first use
let sortComparer: Comparer<string> = (a, b) => {
// Intl.Collator.prototype.compare is bound to the collator. See NOTE in
// http://www.ecma-international.org/ecma-402/2.0/#sec-Intl.Collator.prototype.compare
compare: sortCollator.compare,
equate: (a, b) => searchCollator.compare(a, b) === 0
sortComparer = new Intl.Collator(locale, { usage: "sort", sensitivity: caseInsensitive ? "accent" : "variant" }).compare;
return sortComparer(a, b);
};
// Initialize the search collator on first use
let searchComparer: Comparer<string> = (a, b) => {
// Intl.Collator.prototype.compare is bound to the collator. See NOTE in
// http://www.ecma-international.org/ecma-402/2.0/#sec-Intl.Collator.prototype.compare
searchComparer = new Intl.Collator(locale, { usage: "search", sensitivity: caseInsensitive ? "accent" : "variant" }).compare;
return searchComparer(a, b);
};
}
function createLocaleCompareStringCollator(): StringCollator {
// for case-insensitive comparisons we always map both strings to their
// upper-case form as some unicode characters do not properly round-trip to
// lowercase (such as ẞ).
return {
compare: (a, b) => a.toLocaleUpperCase().localeCompare(b.toLocaleUpperCase()),
equate: (a, b) => a.toLocaleUpperCase() === b.toLocaleUpperCase()
compare: (a, b) => compareWithCallback(a, b, sortComparer),
equate: (a, b) => compareWithCallback(a, b, searchComparer) === 0
};
}
function createOrdinalStringCollator(): StringCollator {
// for case-insensitive comparisons we always map both strings to their
// upper-case form as some unicode characters do not properly round-trip to
// lowercase (such as ẞ).
function createLocaleCompareStringCollator(locale: string | undefined, caseInsensitive: boolean): StringCollator {
if (locale !== undefined) return getFallbackStringCollator(/*locale*/ undefined, caseInsensitive);
if (caseInsensitive) {
// for case-insensitive comparisons we always map both strings to their
// upper-case form as some unicode characters do not properly round-trip to
// lowercase (such as `ẞ` (German sharp capital s)).
return {
compare: (a, b) => compareWithCallback(a, b, localeCompareCaseInsensitive),
equate: (a, b) => compareWithCallback(a, b, localeCompareCaseInsensitive) === 0
};
}
else {
return {
compare: (a, b) => compareWithCallback(a, b, localeCompare),
equate: (a, b) => compareWithCallback(a, b, localeCompare) === 0
};
}
function localeCompareCaseInsensitive(a: string, b: string) {
return a.toLocaleUpperCase().localeCompare(b.toLocaleUpperCase());
}
function localeCompare(a: string, b: string) {
return a.localeCompare(b);
}
}
function getFallbackStringCollator(_locale: string | undefined, caseInsensitive: boolean): StringCollator {
if (caseInsensitive) return ordinalCI;
function compareLowerCaseFirst(a: string, b: string) {
// An ordinal comparison puts "A" after "b", but for the UI we want "A" before "b".
// We first sort case insensitively. So "Aaa" will come before "baa".
// Then we sort case sensitively, so "aaa" will come before "Aaa".
return compareDefined(a.toUpperCase(), b.toUpperCase()) || compareDefined(a, b);
}
return {
compare: (a, b) => compareWithCallback(a, b, compareLowerCaseFirst),
equate: ordinalCS.equate
};
}
function getStringCollatorFactory() {
// If the host supports Intl (ECMA-402), we use Intl for comparisons using the default
// locale:
if (typeof Intl === "object" && typeof Intl.Collator === "function") {
return createIntlStringCollator;
}
// If the host does not support Intl, we fall back to localeCompare:
//
// The ordinal comparison cannot properly handle comparison of the Turkish
// (dotted) i and (dotless) ı to the uppercase forms of (dotted) İ and (dotless) I.
// This is best handled by Intl and not supported in the fallback case.
return {
compare: (a, b) => {
const upperA = a.toUpperCase();
const upperB = b.toUpperCase();
return upperA < upperB ? Comparison.LessThan :
upperA > upperB ? Comparison.GreaterThan :
Comparison.EqualTo;
},
equate: (a, b) => a.toUpperCase() === b.toUpperCase()
};
// Node v0.10 provides incorrect results for comparisons using localeCompare, so we must
// verify the implementation.
if (typeof String.prototype.localeCompare === "function" &&
typeof String.prototype.toLocaleUpperCase === "function" &&
"a".localeCompare("B") < 0) {
return createLocaleCompareStringCollator;
}
// Otherwise, fall back to ordinal comparison:
return getFallbackStringCollator;
}
// If the host supports Intl (ECMA-402), we use Intl for comparisons using the default
// locale:
if (typeof Intl === "object" && typeof Intl.Collator === "function") {
return createIntlStringCollator();
}
// If the host does not support Intl, we fall back to localeCompare:
//
// Node v0.10 provides incorrect results for comparisons using localeCompare, so we must
// verify the implementation.
if (typeof String.prototype.localeCompare === "function" &&
typeof String.prototype.toLocaleUpperCase === "function" &&
"a".localeCompare("B") < 0) {
return createLocaleCompareStringCollator();
}
// Otherwise, fall back to ordinal comparison:
return createOrdinalStringCollator();
}
const caseInsensitiveCollator = createCaseInsensitiveStringCollator();
/**
* Performs a case-insensitive comparison between two strings.
*
* If supported by the host, the default locale is used for comparisons. Otherwise, an ordinal
* comparison is used.
*/
export function compareStringsCaseInsensitive(a: string | undefined, b: string | undefined) {
if (a === b) return Comparison.EqualTo;
if (a === undefined) return Comparison.LessThan;
if (b === undefined) return Comparison.GreaterThan;
const result = caseInsensitiveCollator.compare(a, b);
return result < 0 ? Comparison.LessThan : result > 0 ? Comparison.GreaterThan : Comparison.EqualTo;
}
/**
* Performs a case-sensitive comparison between two strings.
*/
export function compareStringsCaseSensitive(a: string | undefined, b: string | undefined) {
return compareValues(a, b);
}
export function compareStrings(a: string | undefined, b: string | undefined, ignoreCase?: boolean): Comparison {
return ignoreCase ? compareStringsCaseInsensitive(a, b) : compareStringsCaseSensitive(a, b);
}
/**
* Performs a case-insensitive equality comparison between two strings.
*
* If supported by the host, the default locale is used for comparisons. Otherwise, an ordinal
* comparison is used.
*/
export function equateStringsCaseInsensitive(a: string | undefined, b: string | undefined) {
return a === b
|| a !== undefined
&& b !== undefined
&& caseInsensitiveCollator.equate(a, b);
}
/**
* Performs a case-sensitive equality comparison between two strings.
*/
export function equateStringsCaseSensitive(a: string | undefined, b: string | undefined) {
return equateValues(a, b);
}
export function equateStrings(a: string | undefined, b: string | undefined, ignoreCase?: boolean) {
return ignoreCase ? equateStringsCaseInsensitive(a, b) : equateStringsCaseSensitive(a, b);
}
})();
function getDiagnosticFileName(diagnostic: Diagnostic): string {
return diagnostic.file ? diagnostic.file.fileName : undefined;
@@ -2003,9 +2151,9 @@ namespace ts {
const aComponents = getNormalizedPathComponents(a, currentDirectory);
const bComponents = getNormalizedPathComponents(b, currentDirectory);
const sharedLength = Math.min(aComponents.length, bComponents.length);
const comparer = ignoreCase ? compareStringsCaseInsensitive : compareStringsCaseSensitive;
const collator = StringCollator.getPathCollator(ignoreCase);
for (let i = 0; i < sharedLength; i++) {
const result = comparer(aComponents[i], bComponents[i]);
const result = collator.compare(aComponents[i], bComponents[i]);
if (result !== Comparison.EqualTo) {
return result;
}
@@ -2026,9 +2174,10 @@ namespace ts {
return false;
}
const equalityComparer = ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive;
// File-system comparisons should use predictable ordering
const collator = StringCollator.getPathCollator(ignoreCase);
for (let i = 0; i < parentComponents.length; i++) {
if (!equalityComparer(parentComponents[i], childComponents[i])) {
if (!collator.equate(parentComponents[i], childComponents[i])) {
return false;
}
}
@@ -2284,7 +2433,7 @@ namespace ts {
// If there are no "includes", then just put everything in results[0].
const results: string[][] = includeFileRegexes ? includeFileRegexes.map(() => []) : [[]];
const comparer = useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive;
const collator = StringCollator.getPathCollator(!useCaseSensitiveFileNames);
for (const basePath of patterns.basePaths) {
visitDirectory(basePath, combinePaths(currentDirectory, basePath), depth);
}
@@ -2293,7 +2442,7 @@ namespace ts {
function visitDirectory(path: string, absolutePath: string, depth: number | undefined) {
let { files, directories } = getFileSystemEntries(path);
files = files.slice().sort(comparer);
files = files.slice().sort(collator.compare);
for (const current of files) {
const name = combinePaths(path, current);
@@ -2318,7 +2467,7 @@ namespace ts {
}
}
directories = directories.slice().sort(comparer);
directories = directories.slice().sort(collator.compare);
for (const current of directories) {
const name = combinePaths(path, current);
const absoluteName = combinePaths(absolutePath, current);
@@ -2349,7 +2498,8 @@ namespace ts {
}
// Sort the offsets array using either the literal or canonical path representations.
includeBasePaths.sort(useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive);
const collator = StringCollator.getPathCollator(!useCaseSensitiveFileNames);
includeBasePaths.sort(collator.compare);
// Iterate over each include base path and include unique base paths that are not a
// subpath of an existing base path
+1 -1
View File
@@ -6069,7 +6069,7 @@ namespace ts {
const checkJsDirectiveMatchResult = checkJsDirectiveRegEx.exec(comment);
if (checkJsDirectiveMatchResult) {
checkJsDirective = {
enabled: equateStrings(checkJsDirectiveMatchResult[1], "@ts-check", /*ignoreCase*/ true),
enabled: StringCollator.ordinalCaseInsensitive.equate(checkJsDirectiveMatchResult[1], "@ts-check"),
end: range.end,
pos: range.pos
};
+5 -3
View File
@@ -1101,12 +1101,14 @@ namespace ts {
// If '--lib' is not specified, include default library file according to '--target'
// otherwise, using options specified in '--lib' instead of '--target' default library file
// File-system ordering should use a predictable order
const collator = StringCollator.getPathCollator(!host.useCaseSensitiveFileNames());
if (!options.lib) {
return equateStrings(file.fileName, getDefaultLibraryFileName(), /*ignoreCase*/ !host.useCaseSensitiveFileNames());
return collator.equate(file.fileName, getDefaultLibraryFileName());
}
else {
const equalityComparer = host.useCaseSensitiveFileNames() ? equateStringsCaseSensitive : equateStringsCaseInsensitive;
return forEach(options.lib, libFileName => equalityComparer(file.fileName, combinePaths(defaultLibraryPath, libFileName)));
return forEach(options.lib, libFileName => collator.equate(file.fileName, combinePaths(defaultLibraryPath, libFileName)));
}
}
+3
View File
@@ -3949,6 +3949,9 @@ namespace ts {
trySetLanguageAndTerritory(language, /*territory*/ undefined, errors);
}
// Set the locale for UI collation
StringCollator.uiLocale = locale;
function trySetLanguageAndTerritory(language: string, territory: string, errors?: Push<Diagnostic>): boolean {
const compilerFilePath = normalizePath(sys.getExecutingFilePath());
const containingDirectoryPath = getDirectoryPath(compilerFilePath);
+3 -1
View File
@@ -1698,7 +1698,9 @@ namespace Harness {
export function *iterateOutputs(outputFiles: Harness.Compiler.GeneratedFile[]): IterableIterator<[string, string]> {
// Collect, test, and sort the fileNames
outputFiles.sort((a, b) => ts.compareStrings(cleanName(a.fileName), cleanName(b.fileName)));
// As this uses the file system, use a predictable order
const collator = ts.StringCollator.getPathCollator(/*ignoreCase*/ false);
outputFiles.sort((a, b) => collator.compare(cleanName(a.fileName), cleanName(b.fileName)));
const dupeCase = ts.createMap<number>();
// Yield them
for (const outputFile of outputFiles) {
+4 -2
View File
@@ -13,8 +13,10 @@ namespace ts.projectSystem {
describe("CompileOnSave affected list", () => {
function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: FileOrFolder[] }[]) {
const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[];
const actualResult = response.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName));
expectedFileList = expectedFileList.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName));
// File-system ordering should use a predictable order
const collator = StringCollator.getPathCollator(/*ignoreCase*/ false);
const actualResult = response.sort((list1, list2) => collator.compare(list1.projectFileName, list2.projectFileName));
expectedFileList = expectedFileList.sort((list1, list2) => collator.compare(list1.projectFileName, list2.projectFileName));
assert.equal(actualResult.length, expectedFileList.length, `Actual result project number is different from the expected project number`);
+2 -1
View File
@@ -1186,6 +1186,7 @@ namespace ts.server {
const completions = project.getLanguageService().getCompletionsAtPosition(file, position);
if (simplifiedResult) {
const comparer = StringCollator.uiCaseSensitive.compare;
return mapDefined<CompletionEntry, protocol.CompletionEntry>(completions && completions.entries, entry => {
if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) {
const { name, kind, kindModifiers, sortText, replacementSpan, hasAction } = entry;
@@ -1193,7 +1194,7 @@ namespace ts.server {
// Use `hasAction || undefined` to avoid serializing `false`.
return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined };
}
}).sort((a, b) => compareStrings(a.name, b.name));
}).sort((a, b) => comparer(a.name, b.name));
}
else {
return completions;
+1 -5
View File
@@ -175,12 +175,8 @@ namespace ts.NavigateTo {
function compareNavigateToItems(i1: RawNavigateToItem, i2: RawNavigateToItem): number {
// TODO(cyrusn): get the gamut of comparisons that VS already uses here.
// Right now we just sort by kind first, and then by name of the item.
// We first sort case insensitively. So "Aaa" will come before "bar".
// Then we sort case sensitively, so "aaa" will come before "Aaa".
return i1.matchKind - i2.matchKind ||
compareStringsCaseInsensitive(i1.name, i2.name) ||
compareStringsCaseSensitive(i1.name, i2.name);
StringCollator.uiCaseSensitive.compare(i1.name, i2.name);
}
function createNavigateToItem(rawItem: RawNavigateToItem): NavigateToItem {
+2 -7
View File
@@ -368,13 +368,8 @@ namespace ts.NavigationBar {
function compareChildren(child1: NavigationBarNode, child2: NavigationBarNode): number {
const name1 = tryGetName(child1.node), name2 = tryGetName(child2.node);
if (name1 && name2) {
const cmp = ts.compareStringsCaseInsensitive(name1, name2);
return cmp !== 0 ? cmp : navigationBarNodeKind(child1) - navigationBarNodeKind(child2);
}
else {
return name1 ? 1 : name2 ? -1 : navigationBarNodeKind(child1) - navigationBarNodeKind(child2);
}
return StringCollator.uiCaseInsensitive.compare(name1, name2)
|| navigationBarNodeKind(child1) - navigationBarNodeKind(child2);
}
/**
+3 -1
View File
@@ -1154,7 +1154,9 @@ namespace ts.refactor.extractSymbol {
const name1 = type1.symbol ? type1.symbol.getName() : "";
const name2 = type2.symbol ? type2.symbol.getName() : "";
const nameDiff = compareStrings(name1, name2);
// This is for code generation, use a predictable comparer.
const nameDiff = StringCollator.invariantCaseSensitive.compare(name1, name2);
if (nameDiff !== 0) {
return nameDiff;
}