diff --git a/appveyor.yml b/appveyor.yml index 2b0fde43..3ab50c6e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,5 @@ +image: Visual Studio 2015 + version: "{build}" platform: x64 diff --git a/lib/fuzzy-finder-view.js b/lib/fuzzy-finder-view.js index 05617444..3b6d9d23 100644 --- a/lib/fuzzy-finder-view.js +++ b/lib/fuzzy-finder-view.js @@ -2,20 +2,34 @@ const {Point, CompositeDisposable} = require('atom') const fs = require('fs-plus') const fuzzaldrin = require('fuzzaldrin') const fuzzaldrinPlus = require('fuzzaldrin-plus') +const NativeFuzzy = require('@atom/fuzzy-native') + const path = require('path') const SelectListView = require('atom-select-list') const {repositoryForPath} = require('./helpers') const getIconServices = require('./get-icon-services') -module.exports = -class FuzzyFinderView { +/** + * These scoring systems should be synchronized with the enum values + * on the scoringSystem config option defined in package.json +**/ +const SCORING_SYSTEMS = { + STANDARD: 'standard', + ALTERNATE: 'alternate', + FAST: 'fast' +} + +const MAX_RESULTS = 10 + +module.exports = class FuzzyFinderView { constructor () { this.previousQueryWasLineJump = false this.items = [] + this.selectListView = new SelectListView({ items: this.items, - maxResults: 10, + maxResults: MAX_RESULTS, emptyMessage: this.getEmptyMessage(), filterKeyForItem: (item) => item.label, filterQuery: (query) => { @@ -74,9 +88,26 @@ class FuzzyFinderView { }, elementForItem: ({filePath, label, ownerGitHubUsername}) => { const filterQuery = this.selectListView.getFilterQuery() - const matches = this.useAlternateScoring - ? fuzzaldrinPlus.match(label, filterQuery) - : fuzzaldrin.match(label, filterQuery) + + let matches + + switch (this.scoringSystem) { + case SCORING_SYSTEMS.STANDARD: + matches = fuzzaldrin.match(label, filterQuery) + break + case SCORING_SYSTEMS.FAST: + this.nativeFuzzyForResults.setCandidates([label]) + const items = this.nativeFuzzyForResults.match( + filterQuery, + {maxResults: 1, recordMatchIndexes: true} + ) + matches = items.length ? items[0].matchIndexes : [] + break + default: + matches = fuzzaldrinPlus.match(label, filterQuery) + break + } + const repository = repositoryForPath(filePath) return new FuzzyFinderItem({ @@ -118,16 +149,38 @@ class FuzzyFinderView { this.subscriptions = new CompositeDisposable() this.subscriptions.add( - atom.config.observe('fuzzy-finder.useAlternateScoring', (newValue) => { - this.useAlternateScoring = newValue - if (this.useAlternateScoring) { + atom.config.observe('fuzzy-finder.scoringSystem', (newValue) => { + this.scoringSystem = newValue + + if (this.scoringSystem === SCORING_SYSTEMS.STANDARD) { + this.selectListView.update({filter: null}) + } else if (this.scoringSystem === SCORING_SYSTEMS.FAST) { + if (!this.nativeFuzzy) { + this.nativeFuzzy = new NativeFuzzy.Matcher(this.items.map(el => el.label)) + + // We need a separate instance of the fuzzy finder to calculate the + // matched paths only for the returned results. This speeds up considerably + // the filtering of items. + this.nativeFuzzyForResults = new NativeFuzzy.Matcher([]) + } + this.selectListView.update({ filter: (items, query) => { - return query ? fuzzaldrinPlus.filter(items, query, {key: 'label'}) : items + if (!query) { + return items + } + + return this.nativeFuzzy.match(query, {maxResults: MAX_RESULTS}).map( + result => this.convertPathToSelectViewObject(this.getAbsolutePath(result.value)) + ) } }) } else { - this.selectListView.update({filter: null}) + this.selectListView.update({ + filter: (items, query) => { + return query ? fuzzaldrinPlus.filter(items, query, {key: 'label'}) : items + } + }) } }) ) @@ -289,6 +342,12 @@ class FuzzyFinderView { setItems (items) { this.items = items + + if (this.scoringSystem === SCORING_SYSTEMS.FAST) { + // Beware: this operation is quite slow for large projects! + this.nativeFuzzy.setCandidates(this.items.map(item => item.label)) + } + if (this.isQueryALineJump()) { return this.selectListView.update({items: [], loadingMessage: null, loadingBadge: null}) } else { @@ -299,21 +358,49 @@ class FuzzyFinderView { projectRelativePathsForFilePaths (filePaths) { // Don't regenerate project relative paths unless the file paths have changed if (filePaths !== this.filePaths) { - const projectHasMultipleDirectories = atom.project.getDirectories().length > 1 this.filePaths = filePaths - this.projectRelativePaths = this.filePaths.map((filePath) => { - const [rootPath, projectRelativePath] = atom.project.relativizePath(filePath) - const label = - rootPath && projectHasMultipleDirectories - ? path.join(path.basename(rootPath), projectRelativePath) - : projectRelativePath - - return {uri: filePath, filePath, label} - }) + this.projectRelativePaths = this.filePaths.map( + (filePath) => this.convertPathToSelectViewObject(filePath) + ) } return this.projectRelativePaths } + + convertPathToSelectViewObject (filePath) { + const projectHasMultipleDirectories = atom.project.getDirectories().length > 1 + + const [rootPath, projectRelativePath] = atom.project.relativizePath(filePath) + const label = + rootPath && projectHasMultipleDirectories + ? path.join(path.basename(rootPath), projectRelativePath) + : projectRelativePath + + return {uri: filePath, filePath, label} + } + + getAbsolutePath (relativePath) { + const directories = atom.project.getDirectories() + + if (directories.length === 1) { + return path.join(directories[0].getPath(), relativePath) + } + + // Remove the first part of the relative path, since it contains the project + // directory name if there are many directories opened. + relativePath = path.join(...relativePath.split(path.sep).slice(1)) + + for (const directory of directories) { + const absolutePath = path.join(directory.getPath(), relativePath) + + if (fs.existsSync(absolutePath)) { + return absolutePath + } + } + + // Best effort: just return the relative path. + return relativePath + } } function highlight (path, matches, offsetIndex) { diff --git a/package-lock.json b/package-lock.json index 435fea88..aef5c0ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,729 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@atom/fuzzy-native": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@atom/fuzzy-native/-/fuzzy-native-0.7.0.tgz", + "integrity": "sha512-FupfyfBTN12/WL6zJ/GXWWMCpFAItFPkK85vEF/96YY1utSvpk3eaLTIvrwO9afbR2nJnmPpjW6YsMcat4PqOw==", + "requires": { + "nan": "^2.0.0", + "node-pre-gyp": "^0.6.30", + "semver": "^5.0.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "asn1": { + "version": "0.2.4", + "bundled": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true + }, + "aws4": { + "version": "1.8.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "bundled": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "bundled": true + }, + "co": { + "version": "4.6.0", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "combined-stream": { + "version": "1.0.7", + "bundled": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "requires": { + "boom": "2.x.x" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "bundled": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "bundled": true + }, + "extsprintf": { + "version": "1.3.0", + "bundled": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "requires": { + "fstream": "^1.0.0", + "inherits": "2", + "minimatch": "^3.0.0" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "bundled": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "requires": { + "ajv": "^4.9.1", + "har-schema": "^1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.5", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true + }, + "jsbn": { + "version": "0.1.1", + "bundled": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true + }, + "jsprim": { + "version": "1.4.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "mime-db": { + "version": "1.38.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.22", + "bundled": true, + "requires": { + "mime-db": "~1.38.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "node-pre-gyp": { + "version": "0.6.39", + "bundled": true, + "requires": { + "detect-libc": "^1.0.2", + "hawk": "3.1.3", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "request": "2.81.0", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^2.2.1", + "tar-pack": "^3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true + }, + "qs": { + "version": "6.4.0", + "bundled": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.12.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~4.2.1", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "performance-now": "^0.2.0", + "qs": "~6.4.0", + "safe-buffer": "^5.0.1", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.0.0" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true + }, + "semver": { + "version": "5.7.0", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "requires": { + "hoek": "2.x.x" + } + }, + "sshpk": { + "version": "1.16.1", + "bundled": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "stringstream": { + "version": "0.0.6", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.2", + "inherits": "2" + } + }, + "tar-pack": { + "version": "3.4.1", + "bundled": true, + "requires": { + "debug": "^2.2.0", + "fstream": "^1.0.10", + "fstream-ignore": "^1.0.5", + "once": "^1.3.3", + "readable-stream": "^2.1.4", + "rimraf": "^2.5.1", + "tar": "^2.2.1", + "uid-number": "^0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.4", + "bundled": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "uuid": { + "version": "3.3.2", + "bundled": true + }, + "verror": { + "version": "1.10.0", + "bundled": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + } + } + }, "acorn": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", @@ -1165,6 +1888,11 @@ "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", "dev": true }, + "nan": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", + "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/package.json b/package.json index ae9f9763..9a3ea82f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "fs-plus": "^3.0.0", "fuzzaldrin": "^2.0", "fuzzaldrin-plus": "^0.6.0", + "@atom/fuzzy-native": "^0.7.0", "humanize-plus": "~1.8.2", "minimatch": "~3.0.3", "temp": "~0.8.1", @@ -77,10 +78,15 @@ "default": false, "description": "Remember the typed query when closing the fuzzy finder and use that as the starting query next time the fuzzy finder is opened." }, - "useAlternateScoring": { - "type": "boolean", - "default": true, - "description": "Use an alternative scoring approach which prefers run of consecutive characters, acronyms and start of words. (Experimental)" + "scoringSystem": { + "type": "string", + "default": "alternate", + "description": "Scoring system to use. \"standard\" is the system used by other modals in Atom. \"alternate\" is an improved scoring system. \"fast\" is a much faster system, specially suitable for large projects. Default: \"alternate\"", + "enum": [ + "standard", + "alternate", + "fast" + ] }, "useRipGrep": { "type": "boolean", diff --git a/spec/fuzzy-finder-spec.js b/spec/fuzzy-finder-spec.js index 3d15677e..0118b0e0 100644 --- a/spec/fuzzy-finder-spec.js +++ b/spec/fuzzy-finder-spec.js @@ -93,11 +93,19 @@ describe('FuzzyFinder', () => { } } - for (const useRipGrep of [false, true]) { - describe(`file-finder behavior (ripgrep=${useRipGrep})`, () => { + const testPermutations = [ + [ false, 'standard' ], + [ true, 'standard' ], + [ false, 'alternate' ], + [ false, 'fast' ] + ] + + for (const [useRipGrep, scoringSystem] of testPermutations) { + describe(`file-finder behavior (ripgrep=${useRipGrep}, scoringSystem=${scoringSystem})`, () => { beforeEach(async () => { projectView = fuzzyFinder.createProjectView() + atom.config.set('fuzzy-finder.scoringSystem', scoringSystem) atom.config.set('fuzzy-finder.useRipGrep', useRipGrep) sinon.stub(os, 'cpus').returns({length: 1}) @@ -1214,8 +1222,8 @@ describe('FuzzyFinder', () => { await bufferView.setItems([ { - filePath: '/test/root-dir1/sample.js', - label: 'root-dir1/sample.js' + filePath: path.join('test', 'root-dir1', 'sample.js'), + label: path.join('root-dir1', 'sample.js') } ])