Skip to content

Commit 3143969

Browse files
Merge pull request #14 from pulsar-edit/rewrite-in-napi
Rewrite in N-API
2 parents 35b299e + 1b116c7 commit 3143969

12 files changed

Lines changed: 366 additions & 390 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66

77
jobs:
88
test:
9-
name: Run tests ('${{ matrix.node_version }}')
9+
name: Run tests (${{ matrix.node_version }})
1010
if: |
1111
!startsWith(github.event.pull_request.title, '[skip-ci]') &&
1212
!startsWith(github.event.pull_request.title, '[skip-package-ci]')

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
logs
33
*.log
44
npm-debug.log*
5+
.tool-versions
56

67
# Runtime data
78
pids

README.md

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,54 +8,7 @@ The scoring algorithm is heavily tuned for file paths, but should work for gener
88

99
## API
1010

11-
```ts
12-
export type MatcherOptions = {
13-
// Default: false
14-
caseSensitive?: boolean,
15-
16-
// Default: infinite
17-
maxResults?: number,
18-
19-
// Maximum gap to allow between consecutive letters in a match.
20-
// Provide a smaller maxGap to speed up query results.
21-
// Default: unlimited
22-
maxGap?: number;
23-
24-
// Default: 1
25-
numThreads?: number,
26-
27-
// Default: false
28-
recordMatchIndexes?: boolean,
29-
30-
// can be either "fuzzaldrin" or anything else to use the file-based option
31-
algorithm?: string
32-
}
33-
34-
export type MatchResult = {
35-
id: number,
36-
value: string,
37-
38-
// A number in the range (0-1]. Higher scores are more relevant.
39-
// 0 denotes "no match" and will never be returned.
40-
score: number,
41-
42-
// Matching character index in `value` for each character in `query`.
43-
// This can be costly, so this is only returned if `recordMatchIndexes` was set in `options`.
44-
matchIndexes?: Array<number>,
45-
}
46-
47-
export class Matcher {
48-
constructor(candidates: Array<string>) {}
49-
50-
// Returns all matching candidates (subject to `options`).
51-
// Will be ordered by score, descending.
52-
match: (query: string, options?: MatcherOptions) => Array<MatchResult>;
53-
54-
addCandidates: (ids: Array<number>, candidates: Array<string>) => void;
55-
removeCandidates: (ids: Array<number>) => void;
56-
setCandidates: (ids: Array<number>, candidates: Array<string>) => void;
57-
}
58-
```
11+
Read `lib/main.d.ts` for the API of the `Matcher` class.
5912

6013
See also the [spec](spec/fuzzy-native-spec.js) for basic usage.
6114

binding.gyp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
'targets': [
33
{
44
'target_name': 'fuzzy-native',
5-
'include_dirs': [ '<!(node -e "require(\'nan\')")' ],
5+
'include_dirs': [
6+
'<!@(node -p "require(\'node-addon-api\').include")'
7+
],
8+
'defines': ['NAPI_DISABLE_CPP_EXCEPTIONS'],
69
'cflags': [
710
'-std=c++20',
811
'-O3',
@@ -16,7 +19,7 @@
1619
'MACOSX_DEPLOYMENT_TARGET': '10.7',
1720
},
1821
'sources': [
19-
'src/binding.cpp',
22+
'src/fuzzy-native.cpp',
2023
'src/fuzzaldrin_score.cpp',
2124
'src/score_match.cpp',
2225
'src/MatcherBase.cpp',

lib/main.d.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
2+
/**
3+
* The options that can be passed to {@link Matcher#match}.
4+
*/
5+
export type MatcherOptions = {
6+
/** Whether matching is case-sensitive. Defaults to `false`. */
7+
caseSensitive?: boolean;
8+
9+
/** How many results to return at the maximum. Defaults to no limit. */
10+
maxResults?: number;
11+
12+
/**
13+
* Maximum “gap” to allow between consecutive letters for a match candiate.
14+
* Provide a smaller value to speed up query results. Defaults to no limit.
15+
*/
16+
maxGap?: number;
17+
18+
/**
19+
* How many threads to use while searching. Defaults to `1`.
20+
*/
21+
numThreads?: number;
22+
23+
/**
24+
* Whether to return metadata about the indices of the characters that
25+
* matched in each returned max. Defaults to `false`.
26+
*/
27+
recordMatchIndexes?: boolean;
28+
29+
/**
30+
* The algorithm to use for fuzzy-matching. If `"fuzzaldrin"`, will use that
31+
* algorithm for legacy support. Any other value, including the default of
32+
* `undefined`, will trigger use of the default algorithm.
33+
*/
34+
algorithm?: 'fuzzaldrin' | undefined;
35+
};
36+
37+
/**
38+
* A single result returned by {@link Matcher#match}.
39+
*/
40+
export type MatchResult = {
41+
/** A unique identifier for the match. */
42+
id: number;
43+
44+
/** The string value of the match. */
45+
value: string;
46+
47+
/**
48+
* A number in the range (0, 1] — i.e., the maximum value is `1` and the
49+
* minimum value is the smallest possible positive value. Higher scores mean
50+
* more relevant matches. `0` means “no match” and will never be returned.
51+
*/
52+
score: number;
53+
54+
/**
55+
* Matching charcter index in `value` for each character in `query`. This can
56+
* be costly, so this information is returned only when
57+
* {@link MatcherOptions.recordMatchIndexes} is `true`.
58+
*/
59+
matchIndexes?: number[];
60+
}
61+
62+
export class Matcher {
63+
/**
64+
* Construct a new {@link Matcher} object.
65+
*
66+
* You may specify candidates at instantiation time (with the same arguments
67+
* used by {@link addCandidates} and {@link setCandidates}) or you may wait
68+
* and add candidates later.
69+
*
70+
* @param ids A list of numeric IDs. Must correspond to the candidates
71+
* themselves.
72+
* @param candidates A list of candidates against which we will be matching.
73+
*/
74+
constructor();
75+
constructor(ids: number[], candidates: string[]);
76+
77+
/**
78+
* Find all candidates that match the given query.
79+
*
80+
* @param query The input against which candidates will be searched.
81+
* @param options Any {@link MatcherOptions}.
82+
*/
83+
match(query: string, options?: MatcherOptions): MatchResult[];
84+
85+
/**
86+
* Add candidates to the list.
87+
*
88+
* You are responsible for ensuring that the IDs you use do not match the IDs
89+
* of any candidates that are already present in the `Matcher`. Any
90+
* candidates whose IDs already exist in the `Matcher` are silently ignored.
91+
*
92+
* @param ids A list of numeric IDs. Must correspond to the candidates
93+
* themselves.
94+
* @param candidates A list of candidates against which we will be matching.
95+
*/
96+
addCandidates(ids: number[], candidates: string[]): void;
97+
98+
/**
99+
* Remove candidates from the list.
100+
*
101+
* @param ids The unique identifiers for each of the candidates you want to
102+
* remove. Must be an array; if you want to remove only one candidate, wrap
103+
* the value in an array first.
104+
*/
105+
removeCandidates(ids: number[]): void;
106+
107+
/**
108+
* Set a complete list of candidates, removing any candidate that may already
109+
* be defined.
110+
*
111+
* If you want to add candidates instead without removing any that may
112+
* already exist, use {@link addCandidates}.
113+
*
114+
* @param ids A list of numeric IDs. Must correspond to the candidates
115+
* themselves.
116+
* @param candidates A list of candidates against which we will be matching.
117+
*/
118+
setCandidates(ids: number[], candidates: string[]): void;
119+
}

package-lock.json

Lines changed: 12 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "1.3.2",
44
"description": "Native C++ implementation of a fuzzy string matcher.",
55
"main": "lib/main.js",
6+
"types": "lib/main.d.ts",
67
"scripts": {
78
"test": "jasmine-node --captureExceptions spec"
89
},
@@ -21,7 +22,7 @@
2122
"repository": "https://github.com/pulsar-edit/fuzzy-native",
2223
"license": "MIT",
2324
"dependencies": {
24-
"nan": "^2.18.0"
25+
"node-addon-api": "^8.8.0"
2526
},
2627
"devDependencies": {
2728
"jasmine-node": "^1.14.5",

spec/fuzzy-native-spec.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ function values(results) {
88
});
99
}
1010

11+
/** @returns {[number[], string[]]} */
1112
function genIds(values) {
1213
return [Array.from({length: values.length}, (_, i) => i), values];
1314
}
1415

1516
describe('fuzzy-native', function() {
16-
var matcher;
17+
/** @type {import('../lib/main').Matcher} */
18+
let matcher;
1719
beforeEach(function() {
1820
const paths = [
1921
'',
@@ -108,8 +110,12 @@ describe('fuzzy-native', function() {
108110

109111
it('can match empty strings for alternate scoring', function() {
110112
matcher.setCandidates([1, 2, 3], ["hello", "is", "it"]);
113+
// An empty query scores every candidate as 1, and 'is'/'it' are also
114+
// tied on length, so only 'hello' is guaranteed to sort last.
111115
let results = matcher.match('', {algorithm: 'fuzzaldrin'});
112-
expect(values(results)).toEqual([ 'is', 'it', 'hello' ]);
116+
const emptyQueryResults = values(results);
117+
expect(emptyQueryResults.slice(0, 2).sort()).toEqual(['is', 'it']);
118+
expect(emptyQueryResults[2]).toEqual('hello');
113119

114120
results = matcher.match('it', {algorithm: 'fuzzaldrin'});
115121
expect(values(results)).toEqual([ 'it' ]);
@@ -332,13 +338,15 @@ describe('fuzzy-native', function() {
332338
['/path1/path2/path3/zzz', '/path1/path2/path3/zzz_ooo', '/path1/path2/path3/zzz/ooo']
333339
);
334340

335-
expect(
336-
values(matcher.match('path1/path2/path3/zzz', {caseSensitive: true}))
337-
).toEqual([
338-
'/path1/path2/path3/zzz',
341+
// The exact match should rank first; the other two candidates are an
342+
// exact score tie for this query (same matched prefix, same length),
343+
// so their relative order isn't guaranteed.
344+
const exactMatches = values(matcher.match('path1/path2/path3/zzz', {caseSensitive: true}));
345+
expect(exactMatches[0]).toEqual('/path1/path2/path3/zzz');
346+
expect(exactMatches.slice(1).sort()).toEqual([
339347
'/path1/path2/path3/zzz/ooo',
340348
'/path1/path2/path3/zzz_ooo'
341-
]);
349+
].sort());
342350
expect(
343351
values(matcher.match('zzz_ooo', {caseSensitive: true}))
344352
).toEqual([

0 commit comments

Comments
 (0)