init commit
This commit is contained in:
168
internal/search/text_search.go
Normal file
168
internal/search/text_search.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/yeho/doks/internal/registry"
|
||||
)
|
||||
|
||||
type TextSearcher struct {
|
||||
filesDir string
|
||||
registry *registry.Registry
|
||||
hasRipgrep bool
|
||||
contextLines int
|
||||
}
|
||||
|
||||
func NewTextSearcher(filesDir string, reg *registry.Registry) *TextSearcher {
|
||||
_, err := exec.LookPath("rg")
|
||||
return &TextSearcher{
|
||||
filesDir: filesDir,
|
||||
registry: reg,
|
||||
hasRipgrep: err == nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TextSearcher) Search(query string, opts Options) ([]Result, error) {
|
||||
if t.hasRipgrep {
|
||||
return t.searchWithRipgrep(query, opts)
|
||||
}
|
||||
return t.searchPureGo(query, opts)
|
||||
}
|
||||
|
||||
func (t *TextSearcher) searchWithRipgrep(query string, opts Options) ([]Result, error) {
|
||||
contextArg := strconv.Itoa(opts.ContextLines)
|
||||
cmd := exec.Command("rg",
|
||||
"--line-number",
|
||||
"--with-filename",
|
||||
"--context", contextArg,
|
||||
"--max-count", strconv.Itoa(opts.MaxResults*2), // Get more than needed, will dedupe
|
||||
"-i", // Case insensitive
|
||||
query,
|
||||
t.filesDir,
|
||||
)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
// Exit code 1 means no matches, which is fine
|
||||
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
||||
return []Result{}, nil
|
||||
}
|
||||
// Other errors might be okay too (e.g., some files couldn't be read)
|
||||
if stdout.Len() == 0 {
|
||||
return []Result{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return t.parseRipgrepOutput(stdout.String(), opts)
|
||||
}
|
||||
|
||||
func (t *TextSearcher) parseRipgrepOutput(output string, opts Options) ([]Result, error) {
|
||||
results := []Result{}
|
||||
seen := make(map[string]bool) // Track unique file:line combinations
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" || line == "--" {
|
||||
continue
|
||||
}
|
||||
|
||||
// ripgrep output format: filename:linenum:text or filename-linenum-text (context)
|
||||
// We want the matching lines (with :)
|
||||
parts := strings.SplitN(line, ":", 3)
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
filename := filepath.Base(parts[0])
|
||||
lineNum, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lineText := parts[2]
|
||||
|
||||
// Find the key for this file
|
||||
key := t.findKeyByFilename(filename)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
dedupKey := filename + ":" + strconv.Itoa(lineNum)
|
||||
if seen[dedupKey] {
|
||||
continue
|
||||
}
|
||||
seen[dedupKey] = true
|
||||
|
||||
results = append(results, Result{
|
||||
Key: key,
|
||||
Filename: filename,
|
||||
FilePath: parts[0],
|
||||
LineNumber: lineNum,
|
||||
LineText: strings.TrimSpace(lineText),
|
||||
})
|
||||
|
||||
if len(results) >= opts.MaxResults {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (t *TextSearcher) searchPureGo(query string, opts Options) ([]Result, error) {
|
||||
results := []Result{}
|
||||
queryLower := strings.ToLower(query)
|
||||
|
||||
entries := t.registry.List()
|
||||
for _, entry := range entries {
|
||||
filePath := filepath.Join(t.filesDir, entry.Filename)
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
lineNum := 0
|
||||
for scanner.Scan() {
|
||||
lineNum++
|
||||
line := scanner.Text()
|
||||
if strings.Contains(strings.ToLower(line), queryLower) {
|
||||
results = append(results, Result{
|
||||
Key: entry.Key,
|
||||
Filename: entry.Filename,
|
||||
FilePath: filePath,
|
||||
LineNumber: lineNum,
|
||||
LineText: strings.TrimSpace(line),
|
||||
})
|
||||
|
||||
if len(results) >= opts.MaxResults {
|
||||
file.Close()
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (t *TextSearcher) findKeyByFilename(filename string) string {
|
||||
entries := t.registry.List()
|
||||
for _, entry := range entries {
|
||||
if entry.Filename == filename {
|
||||
return entry.Key
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user