internal/scanner/scanner.go
Ref: Size: 2.9 KiB
package scanner
import (
"bufio"
"bytes"
"os"
"path/filepath"
"regexp"
"strings"
)
type Marker struct {
Line int
Text string
Tagged bool
UUID string
}
// lineCommentPrefixes: extensions whose markers must be a line-comment.
var lineCommentPrefixes = map[string]string{
".go": "//", ".js": "//", ".jsx": "//", ".ts": "//", ".tsx": "//",
".c": "//", ".cpp": "//", ".h": "//", ".hpp": "//",
".rs": "//", ".java": "//", ".kt": "//", ".swift": "//",
".scala": "//", ".cs": "//",
".py": "#", ".rb": "#", ".sh": "#", ".bash": "#", ".zsh": "#",
".yaml": "#", ".yml": "#", ".toml": "#",
".sql": "--", ".lua": "--", ".hs": "--", ".elm": "--",
}
// blockCommentDelims: extensions whose single-line markers may use a block comment.
type blockDelim struct{ open, close string }
var blockCommentDelims = map[string]blockDelim{
".css": {"/*", "*/"},
".html": {"<!--", "-->"},
".xml": {"<!--", "-->"},
".md": {"<!--", "-->"},
}
// nameOverrides: filename → line-comment prefix when no useful extension exists.
var nameOverrides = map[string]string{
"Makefile": "#", "makefile": "#",
"Dockerfile": "#", "dockerfile": "#",
}
var (
untaggedRE = regexp.MustCompile(`^@claude\s+\S`)
taggedRE = regexp.MustCompile(`^@claude\[(fw-[0-9a-f]{8})\]\s+\S`)
)
func Scan(path string) ([]Marker, error) {
ext := strings.ToLower(filepath.Ext(path))
linePrefix, hasLine := lineCommentPrefixes[ext]
if !hasLine {
linePrefix, hasLine = nameOverrides[filepath.Base(path)]
}
blockD, hasBlock := blockCommentDelims[ext]
if !hasLine && !hasBlock {
return nil, nil
}
const maxBytes = 1 << 20 // 1 MB
st, err := os.Stat(path)
if err != nil {
return nil, err
}
if st.Size() > maxBytes {
return nil, nil
}
// Binary sniff: read first 8 KB, look for null byte.
sniffN := min(int64(8192), st.Size())
sniff := make([]byte, sniffN)
sf, err := os.Open(path)
if err != nil {
return nil, err
}
n, _ := sf.Read(sniff)
sf.Close()
if bytes.IndexByte(sniff[:n], 0) >= 0 {
return nil, nil
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var markers []Marker
sc := bufio.NewScanner(f)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
line := 0
for sc.Scan() {
line++
raw := strings.TrimSpace(sc.Text())
var body string
switch {
case hasLine && strings.HasPrefix(raw, linePrefix):
body = strings.TrimSpace(strings.TrimPrefix(raw, linePrefix))
case hasBlock && strings.HasPrefix(raw, blockD.open) && strings.HasSuffix(raw, blockD.close):
body = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(raw, blockD.open), blockD.close))
default:
continue
}
if m := taggedRE.FindStringSubmatch(body); m != nil {
markers = append(markers, Marker{Line: line, Text: body, Tagged: true, UUID: m[1]})
continue
}
if untaggedRE.MatchString(body) {
markers = append(markers, Marker{Line: line, Text: body})
}
}
return markers, sc.Err()
}