Skip to content

Commit e45d2ac

Browse files
committed
feat: implement grep using ripgrep
- Add search and searchFile functions for pattern matching in files and directories. - Introduce validatePattern function to check regex validity. - Create SearchOptions and SearchResult types for better search configuration and results handling. - Update package metadata and dependencies in Cargo.toml and package.json. - Add benchmark scripts and sample files for performance testing. - Remove obsolete simple-test.js file. - Create .envrc and update .gitignore for environment management. - Add flake.nix and flake.lock for Nix package management.
1 parent 3b951da commit e45d2ac

File tree

15 files changed

+784
-25
lines changed

15 files changed

+784
-25
lines changed

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,5 @@ Temporary Items
193193
### macOS Patch ###
194194
# iCloud generated files
195195
*.icloud
196+
197+
.direnv/

Cargo.toml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
[package]
2-
authors = ["LongYinan <lynweklm@gmail.com>"]
2+
authors = ["yaonyan"]
33
edition = "2021"
4-
name = "napi-package-template"
4+
name = "ripgrep-napi"
55
version = "0.1.0"
6+
description = "NAPI bindings for ripgrep - fast line-oriented search"
7+
license = "MIT"
8+
repository = "https://github.com/yaonyan/ripgrep-napi"
69

710
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
811

@@ -13,6 +16,16 @@ crate-type = ["cdylib"]
1316
napi = "3.0.0"
1417
napi-derive = "3.0.0"
1518

19+
# Core grep functionality
20+
grep = "0.3"
21+
grep-searcher = "0.1"
22+
23+
# File system traversal
24+
ignore = "0.4"
25+
26+
# Serialization for NAPI objects
27+
serde = { version = "1.0", features = ["derive"] }
28+
1629
[build-dependencies]
1730
napi-build = "2"
1831

__test__/index.spec.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,42 @@
11
import test from 'ava'
22

3-
import { plus100 } from '../index.js'
3+
import { search, searchFile, validatePattern, getSupportedFileTypes } from '../index.js'
44

5-
test('sync function from native code', (t) => {
6-
const fixture = 42
7-
t.is(plus100(fixture), fixture + 100)
5+
test('validate pattern function', (t) => {
6+
t.true(validatePattern('hello'))
7+
t.true(validatePattern('\\d+'))
8+
t.false(validatePattern('['))
9+
})
10+
11+
test('get supported file types function', (t) => {
12+
const types = getSupportedFileTypes()
13+
t.true(Array.isArray(types))
14+
t.true(types.length > 0)
15+
t.true(types.includes('rust'))
16+
t.true(types.includes('javascript'))
17+
})
18+
19+
test('search function with basic pattern', async (t) => {
20+
// Search for 'use' in the current source file
21+
const result = search('use', ['./src/lib.rs'])
22+
23+
t.true(result.success)
24+
t.is(typeof result.filesSearched, 'number')
25+
t.is(typeof result.filesWithMatches, 'number')
26+
t.is(typeof result.totalMatches, 'number')
27+
t.true(Array.isArray(result.matches))
28+
})
29+
30+
test('search file function', async (t) => {
31+
// Search for 'fn' in the source file
32+
const result = searchFile('fn', './src/lib.rs')
33+
34+
t.true(result.success)
35+
t.true(result.matches.length > 0)
36+
37+
// Check first match structure
38+
const firstMatch = result.matches[0]
39+
t.is(typeof firstMatch.path, 'string')
40+
t.is(typeof firstMatch.lineNumber, 'number')
41+
t.is(typeof firstMatch.line, 'string')
842
})

benchmark/bench.ts

Lines changed: 192 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,206 @@
11
import { Bench } from 'tinybench'
2+
import fs from 'fs'
23

3-
import { plus100 } from '../index.js'
4+
import { search, searchFile, validatePattern } from '../index.js'
45

5-
function add(a: number) {
6-
return a + 100
6+
// Create large test content to showcase Rust's performance
7+
const createLargeContent = (baseContent: string, multiplier: number) => {
8+
return baseContent.repeat(multiplier)
79
}
810

11+
// JavaScript implementation for comparison (including file read time)
12+
function jsSearchWithFileRead(pattern: string, filePath: string) {
13+
const content = fs.readFileSync(filePath, 'utf-8')
14+
const lines = content.split('\n')
15+
const matches = []
16+
17+
for (let i = 0; i < lines.length; i++) {
18+
const line = lines[i]
19+
if (line.includes(pattern)) {
20+
matches.push({
21+
lineNumber: i + 1,
22+
line: line,
23+
start: line.indexOf(pattern),
24+
end: line.indexOf(pattern) + pattern.length
25+
})
26+
}
27+
}
28+
29+
return matches
30+
}
31+
32+
// JavaScript multi-file search (naive implementation)
33+
function jsMultiFileSearch(pattern: string, filePaths: string[]) {
34+
const allMatches = []
35+
let filesSearched = 0
36+
let filesWithMatches = 0
37+
38+
for (const filePath of filePaths) {
39+
try {
40+
const content = fs.readFileSync(filePath, 'utf-8')
41+
const lines = content.split('\n')
42+
const fileMatches = []
43+
44+
for (let i = 0; i < lines.length; i++) {
45+
const line = lines[i]
46+
if (line.includes(pattern)) {
47+
fileMatches.push({
48+
path: filePath,
49+
lineNumber: i + 1,
50+
line: line,
51+
start: line.indexOf(pattern),
52+
end: line.indexOf(pattern) + pattern.length
53+
})
54+
}
55+
}
56+
57+
filesSearched++
58+
if (fileMatches.length > 0) {
59+
filesWithMatches++
60+
allMatches.push(...fileMatches)
61+
}
62+
} catch (err) {
63+
// Skip files that can't be read
64+
}
65+
}
66+
67+
return {
68+
matches: allMatches,
69+
filesSearched,
70+
filesWithMatches,
71+
totalMatches: allMatches.length
72+
}
73+
}
74+
75+
// Complex regex search in JavaScript
76+
function jsComplexRegexSearch(pattern: string, filePath: string) {
77+
const content = fs.readFileSync(filePath, 'utf-8')
78+
const regex = new RegExp(pattern, 'gm')
79+
const lines = content.split('\n')
80+
const matches = []
81+
82+
for (let i = 0; i < lines.length; i++) {
83+
const line = lines[i]
84+
let match
85+
regex.lastIndex = 0
86+
while ((match = regex.exec(line)) !== null) {
87+
matches.push({
88+
lineNumber: i + 1,
89+
line: line,
90+
start: match.index,
91+
end: match.index + match[0].length
92+
})
93+
}
94+
}
95+
96+
return matches
97+
}
98+
99+
// Setup: Create large test files to showcase performance differences
100+
const baseContent = fs.readFileSync('./src/lib.rs', 'utf-8')
101+
const largeContent = createLargeContent(baseContent, 50) // 50x larger
102+
const veryLargeContent = createLargeContent(baseContent, 200) // 200x larger
103+
104+
// Write test files
105+
if (!fs.existsSync('./benchmark/temp')) {
106+
fs.mkdirSync('./benchmark/temp')
107+
}
108+
109+
fs.writeFileSync('./benchmark/temp/large.rs', largeContent)
110+
fs.writeFileSync('./benchmark/temp/very_large.rs', veryLargeContent)
111+
112+
// Create multiple test files for multi-file search
113+
for (let i = 0; i < 20; i++) {
114+
fs.writeFileSync(`./benchmark/temp/test_${i}.rs`, baseContent)
115+
}
116+
117+
const testFiles = Array.from({ length: 20 }, (_, i) => `./benchmark/temp/test_${i}.rs`)
118+
9119
const bench = new Bench()
10120

11-
bench.add('Native a + 100', () => {
12-
plus100(10)
121+
// 1. Large file search - where Rust's memory efficiency shines
122+
bench.add('🦀 ripgrep-napi: search in large file (50x)', () => {
123+
searchFile('pub fn', './benchmark/temp/large.rs')
124+
})
125+
126+
bench.add('🐌 JavaScript: search in large file (50x)', () => {
127+
jsSearchWithFileRead('pub fn', './benchmark/temp/large.rs')
128+
})
129+
130+
// 2. Very large file search - pushing the limits
131+
bench.add('🦀 ripgrep-napi: search in very large file (200x)', () => {
132+
searchFile('struct', './benchmark/temp/very_large.rs')
133+
})
134+
135+
bench.add('🐌 JavaScript: search in very large file (200x)', () => {
136+
jsSearchWithFileRead('struct', './benchmark/temp/very_large.rs')
13137
})
14138

15-
bench.add('JavaScript a + 100', () => {
16-
add(10)
139+
// 3. Multi-file search - ripgrep's bread and butter
140+
bench.add('🦀 ripgrep-napi: search across 20 files', () => {
141+
search('fn', testFiles)
142+
})
143+
144+
bench.add('🐌 JavaScript: search across 20 files', () => {
145+
jsMultiFileSearch('fn', testFiles)
146+
})
147+
148+
// 4. Complex regex patterns - where ripgrep's regex engine excels
149+
const complexPattern = '(?:pub\\s+)?(?:async\\s+)?fn\\s+\\w+\\s*\\([^)]*\\)\\s*(?:->\\s*[^{]+)?\\s*\\{'
150+
151+
bench.add('🦀 ripgrep-napi: complex regex pattern', () => {
152+
searchFile(complexPattern, './src/lib.rs')
153+
})
154+
155+
bench.add('🐌 JavaScript: complex regex pattern', () => {
156+
jsComplexRegexSearch(complexPattern, './src/lib.rs')
157+
})
158+
159+
// 5. Case-insensitive search with options
160+
bench.add('🦀 ripgrep-napi: case-insensitive + word boundaries', () => {
161+
searchFile('FUNCTION', './benchmark/temp/large.rs', {
162+
caseSensitive: false,
163+
wordRegexp: true
164+
})
165+
})
166+
167+
// 6. Search with file traversal (directory search)
168+
bench.add('🦀 ripgrep-napi: directory traversal search', () => {
169+
search('use', ['./src', './__test__'], {
170+
maxDepth: 3,
171+
hidden: false
172+
})
173+
})
174+
175+
// 7. Pattern validation (Rust's regex compilation)
176+
const patterns = [
177+
'\\d{3}-\\d{2}-\\d{4}',
178+
'(?i)hello\\s+world',
179+
'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}',
180+
'\\b(?:pub|private|protected)\\s+(?:static\\s+)?\\w+\\s*\\(',
181+
'^\\s*(?:#\\[\\w+(?:\\([^)]*\\))?\\]\\s*)*(?:pub\\s+)?(?:async\\s+)?fn\\s+\\w+'
182+
]
183+
184+
bench.add('🦀 ripgrep-napi: validate complex patterns', () => {
185+
patterns.forEach(pattern => validatePattern(pattern))
186+
})
187+
188+
// 8. Large directory with ignore patterns
189+
bench.add('🦀 ripgrep-napi: search with ignore patterns', () => {
190+
search('fn', ['./'], {
191+
maxDepth: 2,
192+
ignorePatterns: ['target', 'node_modules', '*.lock', '*.log']
193+
})
17194
})
18195

19196
await bench.run()
20197

198+
console.log('\n🏆 Performance Results:')
21199
console.table(bench.table())
200+
201+
// Cleanup
202+
try {
203+
fs.rmSync('./benchmark/temp', { recursive: true, force: true })
204+
} catch (err) {
205+
console.log('Note: Cleanup failed, you may need to manually remove ./benchmark/temp')
206+
}

benchmark/create-samples.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import fs from 'fs';
2+
3+
// Create sample files for benchmarking
4+
const sampleData = `
5+
function hello() {
6+
console.log("Hello, world!");
7+
}
8+
9+
function goodbye() {
10+
console.log("Goodbye, world!");
11+
}
12+
13+
const message = "This is a test file for benchmarking ripgrep-napi";
14+
const numbers = [1, 2, 3, 4, 5];
15+
16+
// Some patterns to search for
17+
const patterns = ["function", "console", "test", "hello", "world"];
18+
`;
19+
20+
// Create benchmark sample files
21+
fs.writeFileSync('./benchmark/sample1.js', sampleData);
22+
fs.writeFileSync('./benchmark/sample2.js', sampleData.repeat(10));
23+
fs.writeFileSync('./benchmark/sample3.js', sampleData.repeat(100));
24+
25+
console.log('Sample files created for benchmarking');

flake.lock

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

0 commit comments

Comments
 (0)