Skip to content

Commit

Permalink
Provide ability to compare quick open entries
Browse files Browse the repository at this point in the history
Signed-off-by: Roman Nikitenko <[email protected]>
  • Loading branch information
RomanNikitenko committed Jul 16, 2020
1 parent a1ea230 commit f8b69c3
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 5 deletions.
161 changes: 161 additions & 0 deletions packages/monaco/src/browser/monaco-comparers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/********************************************************************************
* Copyright (C) 2020 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

// Copied from https://github.com/theia-ide/vscode/blob/standalone/0.17.x/src/vs/base/common/comparers.ts
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import strings = monaco.strings;
import IdleValue = monaco.async.IdleValue;
import QuickOpenEntry = monaco.quickOpen.QuickOpenEntry;

let intlFileNameCollator: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }>;

export function setFileNameComparer(collator: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }>): void {
intlFileNameCollator = collator;
}

export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
if (intlFileNameCollator) {
const a = one || '';
const b = other || '';
const result = intlFileNameCollator.getValue().collator.compare(a, b);

// Using the numeric option in the collator will
// make compare(`foo1`, `foo01`) === 0. We must disambiguate.
if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && a !== b) {
return a < b ? -1 : 1;
}

return result;
}

return noIntlCompareFileNames(one, other, caseSensitive);
}

const FileNameMatch = /^(.*?)(\.([^.]*))?$/;

export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
if (!caseSensitive) {
one = one && one.toLowerCase();
other = other && other.toLowerCase();
}

const [oneName, oneExtension] = extractNameAndExtension(one);
const [otherName, otherExtension] = extractNameAndExtension(other);

if (oneName !== otherName) {
return oneName < otherName ? -1 : 1;
}

if (oneExtension === otherExtension) {
return 0;
}

return oneExtension < otherExtension ? -1 : 1;
}

function extractNameAndExtension(str?: string | null): [string, string] {
const match = str ? FileNameMatch.exec(str) as Array<string> : ([] as Array<string>);

return [(match && match[1]) || '', (match && match[3]) || ''];
}

export function compareAnything(one: string, other: string, lookFor: string): number {
const elementAName = one.toLowerCase();
const elementBName = other.toLowerCase();

// Sort prefix matches over non prefix matches
const prefixCompare = compareByPrefix(one, other, lookFor);
if (prefixCompare) {
return prefixCompare;
}

// Sort suffix matches over non suffix matches
const elementASuffixMatch = strings.endsWith(elementAName, lookFor);
const elementBSuffixMatch = strings.endsWith(elementBName, lookFor);
if (elementASuffixMatch !== elementBSuffixMatch) {
return elementASuffixMatch ? -1 : 1;
}

// Understand file names
const r = compareFileNames(elementAName, elementBName);
if (r !== 0) {
return r;
}

// Compare by name
return elementAName.localeCompare(elementBName);
}

export function compareByPrefix(one: string, other: string, lookFor: string): number {
const elementAName = one.toLowerCase();
const elementBName = other.toLowerCase();

// Sort prefix matches over non prefix matches
const elementAPrefixMatch = strings.startsWith(elementAName, lookFor);
const elementBPrefixMatch = strings.startsWith(elementBName, lookFor);
if (elementAPrefixMatch !== elementBPrefixMatch) {
return elementAPrefixMatch ? -1 : 1;
} else if (elementAPrefixMatch && elementBPrefixMatch) { // Same prefix: Sort shorter matches to the top to have those on top that match more precisely
if (elementAName.length < elementBName.length) {
return -1;
}

if (elementAName.length > elementBName.length) {
return 1;
}
}

return 0;
}

/**
* A good default sort implementation for quick open entries respecting highlight information
* as well as associated resources.
*/
// copied from vscode: https://github.com/theia-ide/vscode/blob/standalone/0.17.x/src/vs/base/parts/quickopen/browser/quickOpenModel.ts#L584
export function compareEntries(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string): number {

// Give matches with label highlights higher priority over
// those with only description highlights
const labelHighlightsA = elementA.getHighlights()[0] || [];
const labelHighlightsB = elementB.getHighlights()[0] || [];
if (labelHighlightsA.length && !labelHighlightsB.length) {
return -1;
}

if (!labelHighlightsA.length && labelHighlightsB.length) {
return 1;
}

// Fallback to the full path if labels are identical and we have associated resources
let nameA = elementA.getLabel()!;
let nameB = elementB.getLabel()!;
if (nameA === nameB) {
const resourceA = elementA.getResource();
const resourceB = elementB.getResource();

if (resourceA && resourceB) {
nameA = resourceA.fsPath;
nameB = resourceB.fsPath;
}
}

return compareAnything(nameA, nameB, lookFor);
}
10 changes: 8 additions & 2 deletions packages/monaco/src/browser/monaco-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export function loadMonaco(vsRequire: any): Promise<void> {
'vs/platform/contextkey/common/contextkey',
'vs/platform/contextkey/browser/contextKeyService',
'vs/editor/common/model/wordHelper',
'vs/base/common/errors'
'vs/base/common/errors',
'vs/base/common/strings',
'vs/base/common/async',
], (commands: any, actions: any,
keybindingsRegistry: any, keybindingResolver: any, resolvedKeybinding: any, keybindingLabels: any,
keyCodes: any, mime: any, editorExtensions: any, simpleServices: any,
Expand All @@ -89,7 +91,9 @@ export function loadMonaco(vsRequire: any): Promise<void> {
markerService: any,
contextKey: any, contextKeyService: any,
wordHelper: any,
error: any) => {
error: any,
strings: any,
async: any) => {
const global: any = self;
global.monaco.commands = commands;
global.monaco.actions = actions;
Expand All @@ -111,6 +115,8 @@ export function loadMonaco(vsRequire: any): Promise<void> {
global.monaco.mime = mime;
global.monaco.wordHelper = wordHelper;
global.monaco.error = error;
global.monaco.strings = strings;
global.monaco.async = async;
resolve();
});
});
Expand Down
12 changes: 11 additions & 1 deletion packages/monaco/src/browser/monaco-quick-open-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { MonacoContextKeyService } from './monaco-context-key-service';
import { QuickOpenHideReason } from '@theia/core/lib/common/quick-open-service';
import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding';
import { BrowserMenuBarContribution } from '@theia/core/lib/browser/menu/browser-menu-plugin';
import { compareEntries, setFileNameComparer } from './monaco-comparers';

export interface MonacoQuickOpenControllerOpts extends monaco.quickOpen.IQuickOpenControllerOpts {
valueSelection?: Readonly<[number, number]>;
Expand Down Expand Up @@ -74,6 +75,15 @@ export class MonacoQuickOpenService extends QuickOpenService {
@postConstruct()
protected init(): void {
this.inQuickOpenKey = this.contextKeyService.createKey<boolean>('inQuickOpen', false);

setFileNameComparer(new monaco.async.IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
const isNumeric = collator.resolvedOptions().numeric;
return {
collator: collator,
collatorIsNumeric: isNumeric
};
}));
}

open(model: QuickOpenModel, options?: QuickOpenOptions): void {
Expand Down Expand Up @@ -355,7 +365,7 @@ export class MonacoQuickOpenControllerOptsImpl implements MonacoQuickOpenControl
}
}
if (this.options.fuzzySort) {
entries.sort((a, b) => monaco.quickOpen.compareEntries(a, b, lookFor));
entries.sort((a, b) => compareEntries(a, b, lookFor));
}
return new monaco.quickOpen.QuickOpenModel(entries, actionProvider ? new MonacoQuickOpenActionProvider(actionProvider) : undefined);
}
Expand Down
18 changes: 16 additions & 2 deletions packages/monaco/src/typings/monaco/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,8 +1059,6 @@ declare module monaco.quickOpen {
run(mode: Mode, context: IEntryRunContext): boolean;
}

export function compareEntries(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string): number;

// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/base/parts/quickopen/browser/quickOpenModel.ts#L197
export class QuickOpenEntryGroup extends QuickOpenEntry {
constructor(entry?: QuickOpenEntry, groupLabel?: string, withBorder?: boolean);
Expand Down Expand Up @@ -1354,6 +1352,22 @@ declare module monaco.wordHelper {
export const DEFAULT_WORD_REGEXP: RegExp;
}

declare module monaco.strings {
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/base/common/strings.ts#L150
export function startsWith(haystack: string, needle: string): boolean;

// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/base/common/strings.ts#L171
export function endsWith(haystack: string, needle: string): boolean;
}

declare module monaco.async {
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/base/common/async.ts#L721
export class IdleValue<T> {
constructor(executor: () => T) { }
getValue(): T;
}
}

/**
* overloading languages register functions to accept LanguageSelector,
* check that all register functions passing a selector to registries:
Expand Down

0 comments on commit f8b69c3

Please sign in to comment.