| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| export class FilePathScoreFunction { |
| private query: string; |
| private readonly queryUpperCase: string; |
| private score: Int32Array; |
| private sequence: Int32Array; |
| private dataUpperCase: string; |
| private fileNameIndex: number; |
| |
| constructor(query: string) { |
| this.query = query; |
| this.queryUpperCase = query.toUpperCase(); |
| this.score = new Int32Array(20 * 100); |
| this.sequence = new Int32Array(20 * 100); |
| this.dataUpperCase = ''; |
| this.fileNameIndex = 0; |
| } |
| |
| /** |
| * Calculates the score of a given data string against the query string. |
| * |
| * The score is calculated by comparing the characters of the query string to |
| * the characters of the data string. Characters that match are given a score |
| * of 10, while characters that don't match are given a score of 0. The score |
| * of a match is also influenced by the context of the match. For example, |
| * matching the beginning of the file name is worth more than matching a |
| * character in the middle of the file name. |
| * |
| * The score of a match is also influenced by the number of consecutive |
| * matches. The more consecutive matches there are, the higher the score. |
| * |
| * @param data The data string to score. |
| * @param matchIndexes An optional array to store the indexes of matching |
| * characters. If provided, it will be filled with the indexes of the matching |
| * characters in the data string. |
| * @returns The score of the data string. |
| */ |
| calculateScore(data: string, matchIndexes: number[]|null): number { |
| if (!data || !this.query) { |
| return 0; |
| } |
| const queryLength = this.query.length; |
| const dataLength = data.length; |
| if (!this.score || this.score.length < queryLength * dataLength) { |
| this.score = new Int32Array(queryLength * dataLength * 2); |
| this.sequence = new Int32Array(queryLength * dataLength * 2); |
| } |
| const score = this.score; |
| const sequence = (this.sequence); |
| this.dataUpperCase = data.toUpperCase(); |
| this.fileNameIndex = data.lastIndexOf('/'); |
| for (let i = 0; i < queryLength; ++i) { |
| for (let j = 0; j < dataLength; ++j) { |
| const scoreIndex = i * dataLength + j; |
| const skipCharScore = j === 0 ? 0 : score[scoreIndex - 1]; |
| const prevCharScore = i === 0 || j === 0 ? 0 : score[(i - 1) * dataLength + j - 1]; |
| const consecutiveMatch = i === 0 || j === 0 ? 0 : sequence[(i - 1) * dataLength + j - 1]; |
| const pickCharScore = this.match(this.query, data, i, j, consecutiveMatch); |
| if (pickCharScore && prevCharScore + pickCharScore >= skipCharScore) { |
| sequence[scoreIndex] = consecutiveMatch + 1; |
| score[scoreIndex] = (prevCharScore + pickCharScore); |
| } else { |
| sequence[scoreIndex] = 0; |
| score[scoreIndex] = skipCharScore; |
| } |
| } |
| } |
| if (matchIndexes) { |
| this.restoreMatchIndexes(sequence, queryLength, dataLength, matchIndexes); |
| } |
| const maxDataLength = 256; |
| return score[queryLength * dataLength - 1] * maxDataLength + (maxDataLength - data.length); |
| } |
| |
| private testWordStart(data: string, j: number): boolean { |
| if (j === 0) { |
| return true; |
| } |
| |
| const prevChar = data.charAt(j - 1); |
| return prevChar === '_' || prevChar === '-' || prevChar === '/' || prevChar === '.' || prevChar === ' ' || |
| (data[j - 1] !== this.dataUpperCase[j - 1] && data[j] === this.dataUpperCase[j]); |
| } |
| |
| private restoreMatchIndexes(sequence: Int32Array, queryLength: number, dataLength: number, out: number[]): void { |
| let i = queryLength - 1, j = dataLength - 1; |
| while (i >= 0 && j >= 0) { |
| switch (sequence[i * dataLength + j]) { |
| case 0: |
| --j; |
| break; |
| default: |
| out.push(j); |
| --i; |
| --j; |
| break; |
| } |
| } |
| out.reverse(); |
| } |
| |
| private singleCharScore(query: string, data: string, i: number, j: number): number { |
| const isWordStart = this.testWordStart(data, j); |
| const isFileName = j > this.fileNameIndex; |
| const isPathTokenStart = j === 0 || data[j - 1] === '/'; |
| const isCapsMatch = query[i] === data[j] && query[i] === this.queryUpperCase[i]; |
| let score = 10; |
| if (isPathTokenStart) { |
| score += 4; |
| } |
| if (isWordStart) { |
| score += 2; |
| } |
| if (isCapsMatch) { |
| score += 6; |
| } |
| if (isFileName) { |
| score += 4; |
| } |
| // promote the case of making the whole match in the filename |
| if (j === this.fileNameIndex + 1 && i === 0) { |
| score += 5; |
| } |
| if (isFileName && isWordStart) { |
| score += 3; |
| } |
| return score; |
| } |
| |
| private sequenceCharScore(data: string, j: number, sequenceLength: number): number { |
| const isFileName = j > this.fileNameIndex; |
| const isPathTokenStart = j === 0 || data[j - 1] === '/'; |
| let score = 10; |
| if (isFileName) { |
| score += 4; |
| } |
| if (isPathTokenStart) { |
| score += 5; |
| } |
| score += sequenceLength * 4; |
| return score; |
| } |
| |
| private match(query: string, data: string, i: number, j: number, consecutiveMatch: number): number { |
| if (this.queryUpperCase[i] !== this.dataUpperCase[j]) { |
| return 0; |
| } |
| |
| if (!consecutiveMatch) { |
| return this.singleCharScore(query, data, i, j); |
| } |
| return this.sequenceCharScore(data, j - consecutiveMatch, consecutiveMatch); |
| } |
| } |