9 "github.com/stretchr/testify/require"
12func TestRegexCache(t *testing.T) {
13 cache := newRegexCache()
16 pattern := "test.*pattern"
17 regex1, err := cache.get(pattern)
19 t.Fatalf("Failed to compile regex: %v", err)
22 regex2, err := cache.get(pattern)
24 t.Fatalf("Failed to get cached regex: %v", err)
27 // Should be the same instance (cached)
29 t.Error("Expected cached regex to be the same instance")
32 // Test that it actually works
33 if !regex1.MatchString("test123pattern") {
34 t.Error("Regex should match test string")
38func TestGlobToRegexCaching(t *testing.T) {
39 // Test that globToRegex uses pre-compiled regex
40 pattern1 := globToRegex("*.{js,ts}")
42 // Should not panic and should work correctly
43 regex1, err := regexp.Compile(pattern1)
45 t.Fatalf("Failed to compile glob regex: %v", err)
48 if !regex1.MatchString("test.js") {
49 t.Error("Glob regex should match .js files")
51 if !regex1.MatchString("test.ts") {
52 t.Error("Glob regex should match .ts files")
54 if regex1.MatchString("test.go") {
55 t.Error("Glob regex should not match .go files")
59func TestGrepWithIgnoreFiles(t *testing.T) {
61 tempDir := t.TempDir()
64 testFiles := map[string]string{
65 "file1.txt": "hello world",
66 "file2.txt": "hello world",
67 "ignored/file3.txt": "hello world",
68 "node_modules/lib.js": "hello world",
69 "secret.key": "hello world",
72 for path, content := range testFiles {
73 fullPath := filepath.Join(tempDir, path)
74 require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755))
75 require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644))
78 // Create .gitignore file
79 gitignoreContent := "ignored/\n*.key\n"
80 require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".gitignore"), []byte(gitignoreContent), 0o644))
82 // Create .crushignore file
83 crushignoreContent := "node_modules/\n"
84 require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".crushignore"), []byte(crushignoreContent), 0o644))
86 // Test both implementations
87 for name, fn := range map[string]func(pattern, path, include string) ([]grepMatch, error){
88 "regex": searchFilesWithRegex,
89 "rg": func(pattern, path, include string) ([]grepMatch, error) {
90 return searchWithRipgrep(t.Context(), pattern, path, include)
93 t.Run(name, func(t *testing.T) {
96 if name == "rg" && getRg() == "" {
97 t.Skip("rg is not in $PATH")
100 matches, err := fn("hello world", tempDir, "")
101 require.NoError(t, err)
103 // Convert matches to a set of file paths for easier testing
104 foundFiles := make(map[string]bool)
105 for _, match := range matches {
106 foundFiles[filepath.Base(match.path)] = true
109 // Should find file1.txt and file2.txt
110 require.True(t, foundFiles["file1.txt"], "Should find file1.txt")
111 require.True(t, foundFiles["file2.txt"], "Should find file2.txt")
113 // Should NOT find ignored files
114 require.False(t, foundFiles["file3.txt"], "Should not find file3.txt (ignored by .gitignore)")
115 require.False(t, foundFiles["lib.js"], "Should not find lib.js (ignored by .crushignore)")
116 require.False(t, foundFiles["secret.key"], "Should not find secret.key (ignored by .gitignore)")
118 // Should find exactly 2 matches
119 require.Equal(t, 2, len(matches), "Should find exactly 2 matches")
124func TestSearchImplementations(t *testing.T) {
126 tempDir := t.TempDir()
128 for path, content := range map[string]string{
129 "file1.go": "package main\nfunc main() {\n\tfmt.Println(\"hello world\")\n}",
130 "file2.js": "console.log('hello world');",
131 "file3.txt": "hello world from text file",
132 "binary.exe": "\x00\x01\x02\x03",
134 "subdir/nested.go": "package nested\n// hello world comment",
135 ".hidden.txt": "hello world in hidden file",
136 "file4.txt": "hello world from a banana",
137 "file5.txt": "hello world from a grape",
139 fullPath := filepath.Join(tempDir, path)
140 require.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o755))
141 require.NoError(t, os.WriteFile(fullPath, []byte(content), 0o644))
144 require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".gitignore"), []byte("file4.txt\n"), 0o644))
145 require.NoError(t, os.WriteFile(filepath.Join(tempDir, ".crushignore"), []byte("file5.txt\n"), 0o644))
147 for name, fn := range map[string]func(pattern, path, include string) ([]grepMatch, error){
148 "regex": searchFilesWithRegex,
149 "rg": func(pattern, path, include string) ([]grepMatch, error) {
150 return searchWithRipgrep(t.Context(), pattern, path, include)
153 t.Run(name, func(t *testing.T) {
156 if name == "rg" && getRg() == "" {
157 t.Skip("rg is not in $PATH")
160 matches, err := fn("hello world", tempDir, "")
161 require.NoError(t, err)
163 require.Equal(t, len(matches), 4)
164 for _, match := range matches {
165 require.NotEmpty(t, match.path)
166 require.NotZero(t, match.lineNum)
167 require.NotEmpty(t, match.lineText)
168 require.NotZero(t, match.modTime)
169 require.NotContains(t, match.path, ".hidden.txt")
170 require.NotContains(t, match.path, "file4.txt")
171 require.NotContains(t, match.path, "file5.txt")
172 require.NotContains(t, match.path, "binary.exe")
178// Benchmark to show performance improvement
179func BenchmarkRegexCacheVsCompile(b *testing.B) {
180 cache := newRegexCache()
181 pattern := "test.*pattern.*[0-9]+"
183 b.Run("WithCache", func(b *testing.B) {
185 _, err := cache.get(pattern)
192 b.Run("WithoutCache", func(b *testing.B) {
194 _, err := regexp.Compile(pattern)
202func TestIsTextFile(t *testing.T) {
204 tempDir := t.TempDir()
215 content: []byte("package main\n\nfunc main() {}\n"),
220 filename: "config.yaml",
221 content: []byte("key: value\nlist:\n - item1\n - item2\n"),
226 filename: "config.yml",
227 content: []byte("key: value\n"),
232 filename: "data.json",
233 content: []byte(`{"key": "value"}`),
237 name: "javascript file",
238 filename: "script.js",
239 content: []byte("console.log('hello');\n"),
243 name: "typescript file",
244 filename: "script.ts",
245 content: []byte("const x: string = 'hello';\n"),
249 name: "markdown file",
250 filename: "README.md",
251 content: []byte("# Title\n\nSome content\n"),
255 name: "shell script",
256 filename: "script.sh",
257 content: []byte("#!/bin/bash\necho 'hello'\n"),
262 filename: "script.py",
263 content: []byte("print('hello')\n"),
268 filename: "data.xml",
269 content: []byte("<?xml version=\"1.0\"?>\n<root></root>\n"),
274 filename: "file.txt",
275 content: []byte("plain text content\n"),
280 filename: "style.css",
281 content: []byte("body { color: red; }\n"),
286 filename: "style.scss",
287 content: []byte("$primary: blue;\nbody { color: $primary; }\n"),
292 filename: "style.sass",
293 content: []byte("$primary: blue\nbody\n color: $primary\n"),
299 content: []byte("fn main() {\n println!(\"Hello, world!\");\n}\n"),
304 filename: "main.zig",
305 content: []byte("const std = @import(\"std\");\npub fn main() void {}\n"),
310 filename: "Main.java",
311 content: []byte("public class Main {\n public static void main(String[] args) {}\n}\n"),
317 content: []byte("#include <stdio.h>\nint main() { return 0; }\n"),
322 filename: "main.cpp",
323 content: []byte("#include <iostream>\nint main() { return 0; }\n"),
328 filename: "script.fish",
329 content: []byte("#!/usr/bin/env fish\necho 'hello'\n"),
333 name: "powershell file",
334 filename: "script.ps1",
335 content: []byte("Write-Host 'Hello, World!'\n"),
339 name: "cmd batch file",
340 filename: "script.bat",
341 content: []byte("@echo off\necho Hello, World!\n"),
346 filename: "script.cmd",
347 content: []byte("@echo off\necho Hello, World!\n"),
352 filename: "binary.exe",
353 content: []byte{0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00},
358 filename: "image.png",
359 content: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A},
364 filename: "image.jpg",
365 content: []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46},
370 filename: "archive.zip",
371 content: []byte{0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00},
376 filename: "document.pdf",
377 content: []byte("%PDF-1.4\n%âãÏÓ\n"),
382 for _, tt := range tests {
383 t.Run(tt.name, func(t *testing.T) {
385 filePath := filepath.Join(tempDir, tt.filename)
386 require.NoError(t, os.WriteFile(filePath, tt.content, 0o644))
388 got := isTextFile(filePath)
389 require.Equal(t, tt.wantText, got, "isTextFile(%s) = %v, want %v", tt.filename, got, tt.wantText)
394func TestColumnMatch(t *testing.T) {
397 // Test both implementations
398 for name, fn := range map[string]func(pattern, path, include string) ([]grepMatch, error){
399 "regex": searchFilesWithRegex,
400 "rg": func(pattern, path, include string) ([]grepMatch, error) {
401 return searchWithRipgrep(t.Context(), pattern, path, include)
404 t.Run(name, func(t *testing.T) {
407 if name == "rg" && getRg() == "" {
408 t.Skip("rg is not in $PATH")
411 matches, err := fn("THIS", "./testdata/", "")
412 require.NoError(t, err)
413 require.Len(t, matches, 1)
415 require.Equal(t, 2, match.lineNum)
416 require.Equal(t, 14, match.charNum)
417 require.Equal(t, "I wanna grep THIS particular word", match.lineText)
418 require.Equal(t, "testdata/grep.txt", filepath.ToSlash(filepath.Clean(match.path)))
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list