The tree was formatted with an older gofmt; go1.27's gofmt additionally wants: EOF exactly one newline (no trailing blank lines), imports sorted alphabetically within a block, mixed-precedence binary expressions re-spaced for grouping ((a+b)/c), single-field composite literals un-aligned, adjacent one-line method signatures aligned, and one-line bodies containing a compound statement expanded. Applied repo-wide (31 files under internal/); pure formatting, no semantic changes — build and the full test suite pass.
784 lines
22 KiB
Go
784 lines
22 KiB
Go
package pool
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"pad/internal/io/pool/types"
|
|
)
|
|
|
|
// taskIDCounter provides unique IDs for tasks.
|
|
var taskIDCounter atomic.Int64
|
|
|
|
// Task defines the interface for a worker task.
|
|
type Task interface {
|
|
Execute() Result
|
|
Priority() Priority
|
|
TaskType() TaskType
|
|
TaskID() string
|
|
DirPath() string
|
|
Context() context.Context
|
|
Timeout() time.Duration
|
|
Cancel()
|
|
}
|
|
|
|
// Priority defines task priority.
|
|
type Priority int
|
|
|
|
const (
|
|
LowPriority Priority = iota
|
|
MediumPriority
|
|
HighPriority
|
|
)
|
|
|
|
func (p Priority) String() string {
|
|
switch p {
|
|
case LowPriority:
|
|
return "low"
|
|
case MediumPriority:
|
|
return "medium"
|
|
case HighPriority:
|
|
return "high"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// TaskType defines the type of task.
|
|
type TaskType int
|
|
|
|
const (
|
|
TypeUnknown TaskType = iota
|
|
TypeReadFile
|
|
TypeReadChunk
|
|
TypeStatFile
|
|
TypeBuildLineIndex
|
|
TypeWriteFile
|
|
TypeBuildIndex
|
|
// Browser task types
|
|
TypeReadDir
|
|
TypeLoadIndex
|
|
TypeLoadPages
|
|
TypeStatDir
|
|
// Cache task types
|
|
TypeWriteCache
|
|
TypeReadCache
|
|
TypeInvalidate
|
|
// State persistence types
|
|
TypeSaveState
|
|
TypeSaveUndo
|
|
// In-file search (scans an in-memory content snapshot on a worker)
|
|
TypeSearch
|
|
)
|
|
|
|
func (t TaskType) String() string {
|
|
switch t {
|
|
case TypeReadFile:
|
|
return "read_file"
|
|
case TypeReadChunk:
|
|
return "read_chunk"
|
|
case TypeStatFile:
|
|
return "stat_file"
|
|
case TypeBuildLineIndex:
|
|
return "build_line_index"
|
|
case TypeWriteFile:
|
|
return "write_file"
|
|
case TypeBuildIndex:
|
|
return "build_index"
|
|
case TypeReadDir:
|
|
return "read_dir"
|
|
case TypeLoadIndex:
|
|
return "load_index"
|
|
case TypeLoadPages:
|
|
return "load_pages"
|
|
case TypeStatDir:
|
|
return "stat_dir"
|
|
case TypeWriteCache:
|
|
return "write_cache"
|
|
case TypeReadCache:
|
|
return "read_cache"
|
|
case TypeInvalidate:
|
|
return "invalidate_cache"
|
|
case TypeSaveState:
|
|
return "save_state"
|
|
case TypeSaveUndo:
|
|
return "save_undo"
|
|
case TypeSearch:
|
|
return "search"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// --- Specific Task Implementations ---
|
|
|
|
// ReadChunkTask reads a specific chunk of a file.
|
|
type ReadChunkTask struct {
|
|
taskID string
|
|
Path string
|
|
ChunkIdx int
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewReadChunkTask creates a new ReadChunkTask.
|
|
func NewReadChunkTask(path string, chunkIdx int, fs FileSystem) *ReadChunkTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &ReadChunkTask{
|
|
taskID: fmt.Sprintf("readchunk-%d-%s", chunkIdx, filepath.Base(path)),
|
|
Path: path,
|
|
ChunkIdx: chunkIdx,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *ReadChunkTask) Execute() Result {
|
|
// Use ReadFileAt to read only the specific chunk range (not the entire file)
|
|
chunkSize := 64 * 1024 // Must match the chunk size used in ChunkedBuffer
|
|
start := t.ChunkIdx * chunkSize
|
|
|
|
chunk, err := t.FS.ReadFileAt(t.Path, start, chunkSize)
|
|
if err != nil {
|
|
return Result{TaskID: t.taskID, TaskType: TypeReadChunk, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to read chunk %d: %w", t.ChunkIdx, err)}
|
|
}
|
|
|
|
return Result{
|
|
TaskID: t.taskID,
|
|
TaskType: TypeReadChunk,
|
|
FilePath: t.Path,
|
|
Success: true,
|
|
Data: chunk,
|
|
ChunkIdx: t.ChunkIdx, // Explicitly pass the chunk index
|
|
Timestamp: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (t *ReadChunkTask) Priority() Priority { return HighPriority }
|
|
func (t *ReadChunkTask) TaskType() TaskType { return TypeReadChunk }
|
|
func (t *ReadChunkTask) TaskID() string { return t.taskID }
|
|
func (t *ReadChunkTask) DirPath() string { return filepath.Dir(t.Path) }
|
|
func (t *ReadChunkTask) Context() context.Context { return t.ctx }
|
|
func (t *ReadChunkTask) Timeout() time.Duration { return 5 * time.Second }
|
|
func (t *ReadChunkTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// StatFileTask retrieves file metadata (like size and modification time).
|
|
// The plan indicates this replaces the full-file ReadFileTask for opening.
|
|
type StatFileTask struct {
|
|
taskID string
|
|
Path string
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewStatFileTask creates a new StatFileTask.
|
|
func NewStatFileTask(path string, fs FileSystem) *StatFileTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &StatFileTask{
|
|
taskID: fmt.Sprintf("statfile-%s", filepath.Base(path)),
|
|
Path: path,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *StatFileTask) Execute() Result {
|
|
// In the current plan, this reads the full file content to get size.
|
|
// A more optimized version for large files would use os.Stat or similar,
|
|
// which returns size directly without reading content.
|
|
content, err := t.FS.ReadFile(t.Path)
|
|
if err != nil {
|
|
return Result{TaskID: t.taskID, TaskType: TypeStatFile, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to read file for stat: %w", err)}
|
|
}
|
|
|
|
// We also need mtime for invalidation, but FileInfo might not be directly returned as Data.
|
|
// For now, just returning size.
|
|
return Result{
|
|
TaskID: t.taskID, TaskType: TypeStatFile, FilePath: t.Path, Success: true,
|
|
Data: &FileStat{Path: t.Path, Size: int64(len(content))},
|
|
}
|
|
}
|
|
|
|
func (t *StatFileTask) Priority() Priority { return MediumPriority }
|
|
func (t *StatFileTask) TaskType() TaskType { return TypeStatFile }
|
|
func (t *StatFileTask) TaskID() string { return t.taskID }
|
|
func (t *StatFileTask) DirPath() string { return filepath.Dir(t.Path) }
|
|
func (t *StatFileTask) Context() context.Context { return t.ctx }
|
|
func (t *StatFileTask) Timeout() time.Duration { return 5 * time.Second }
|
|
func (t *StatFileTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// FileStat holds file metadata.
|
|
type FileStat struct {
|
|
Path string
|
|
Size int64
|
|
// MTime time.Time // Add this if FileSystem.Stat returns it and we need it
|
|
}
|
|
|
|
// BuildLineIndexTask builds the line index for a file.
|
|
type BuildLineIndexTask struct {
|
|
taskID string
|
|
Path string
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewBuildLineIndexTask creates a new BuildLineIndexTask.
|
|
func NewBuildLineIndexTask(path string, fs FileSystem) *BuildLineIndexTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &BuildLineIndexTask{
|
|
taskID: fmt.Sprintf("buildindex-%s", filepath.Base(path)),
|
|
Path: path,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *BuildLineIndexTask) Execute() Result {
|
|
content, err := t.FS.ReadFile(t.Path)
|
|
if err != nil {
|
|
return Result{TaskID: t.taskID, TaskType: TypeBuildLineIndex, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to read file for index build: %w", err)}
|
|
}
|
|
|
|
offsets := []int32{0} // Line 0 starts at byte offset 0
|
|
for i := 0; i < len(content); i++ {
|
|
if content[i] == '\n' {
|
|
offsets = append(offsets, int32(i+1))
|
|
}
|
|
}
|
|
|
|
lineIndex := types.NewLineIndex(offsets, 0, int64(len(content)))
|
|
|
|
return Result{
|
|
TaskID: t.taskID, TaskType: TypeBuildLineIndex, FilePath: t.Path, Success: true,
|
|
Data: lineIndex,
|
|
}
|
|
}
|
|
|
|
func (t *BuildLineIndexTask) Priority() Priority { return LowPriority }
|
|
func (t *BuildLineIndexTask) TaskType() TaskType { return TypeBuildLineIndex }
|
|
func (t *BuildLineIndexTask) TaskID() string { return t.taskID }
|
|
func (t *BuildLineIndexTask) DirPath() string { return filepath.Dir(t.Path) }
|
|
func (t *BuildLineIndexTask) Context() context.Context { return t.ctx }
|
|
func (t *BuildLineIndexTask) Timeout() time.Duration { return 30 * time.Second }
|
|
func (t *BuildLineIndexTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// BuildIndexTask builds the directory index for the browser.
|
|
type BuildIndexTask struct {
|
|
taskID string
|
|
Dir string
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewBuildIndexTask creates a new BuildIndexTask.
|
|
func NewBuildIndexTask(dir string, fs FileSystem) *BuildIndexTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &BuildIndexTask{
|
|
taskID: fmt.Sprintf("buildindex-%s", filepath.Base(dir)),
|
|
Dir: dir,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *BuildIndexTask) Execute() Result {
|
|
entries, err := t.FS.ReadDir(t.Dir)
|
|
if err != nil {
|
|
return Result{TaskID: t.taskID, TaskType: TypeBuildIndex, Success: false, Error: fmt.Errorf("failed to read directory: %w", err)}
|
|
}
|
|
return Result{TaskID: t.taskID, TaskType: TypeBuildIndex, Success: true, Data: entries}
|
|
}
|
|
|
|
func (t *BuildIndexTask) Priority() Priority { return HighPriority }
|
|
func (t *BuildIndexTask) TaskType() TaskType { return TypeBuildIndex }
|
|
func (t *BuildIndexTask) TaskID() string { return t.taskID }
|
|
func (t *BuildIndexTask) DirPath() string { return t.Dir }
|
|
func (t *BuildIndexTask) Context() context.Context { return t.ctx }
|
|
func (t *BuildIndexTask) Timeout() time.Duration { return 5 * time.Second }
|
|
func (t *BuildIndexTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// LoadPagesTask loads specific pages from a directory index.
|
|
type LoadPagesTask struct {
|
|
taskID string
|
|
Dir string
|
|
PageIdxs []int
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewLoadPagesTask creates a new LoadPagesTask.
|
|
func NewLoadPagesTask(dir string, pageIdxs []int, fs FileSystem) *LoadPagesTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
id := taskIDCounter.Add(1)
|
|
return &LoadPagesTask{
|
|
taskID: fmt.Sprintf("loadpages-%s-%d", filepath.Base(dir), id),
|
|
Dir: dir,
|
|
PageIdxs: pageIdxs,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *LoadPagesTask) Execute() Result {
|
|
// In a real implementation, this would read the actual entry data for the pages.
|
|
// For now, just return the page indices as success — the browser manager
|
|
// will load the actual data from the SortIndex.
|
|
return Result{TaskID: t.taskID, TaskType: TypeLoadPages, Success: true, Data: t.PageIdxs}
|
|
}
|
|
|
|
func (t *LoadPagesTask) Priority() Priority { return HighPriority }
|
|
func (t *LoadPagesTask) TaskType() TaskType { return TypeLoadPages }
|
|
func (t *LoadPagesTask) TaskID() string { return t.taskID }
|
|
func (t *LoadPagesTask) DirPath() string { return t.Dir }
|
|
func (t *LoadPagesTask) Context() context.Context { return t.ctx }
|
|
func (t *LoadPagesTask) Timeout() time.Duration { return 5 * time.Second }
|
|
func (t *LoadPagesTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// ReadDirTask reads directory entries.
|
|
type ReadDirTask struct {
|
|
taskID string
|
|
Dir string
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewReadDirTask creates a new ReadDirTask.
|
|
func NewReadDirTask(dir string, fs FileSystem) *ReadDirTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
id := taskIDCounter.Add(1)
|
|
return &ReadDirTask{
|
|
taskID: fmt.Sprintf("readdir-%s-%d", filepath.Base(dir), id),
|
|
Dir: dir,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *ReadDirTask) Execute() Result {
|
|
entries, err := t.FS.ReadDir(t.Dir)
|
|
if err != nil {
|
|
return Result{TaskID: t.taskID, TaskType: TypeReadDir, Success: false, Error: fmt.Errorf("failed to read directory: %w", err)}
|
|
}
|
|
return Result{TaskID: t.taskID, TaskType: TypeReadDir, Success: true, Data: entries}
|
|
}
|
|
|
|
func (t *ReadDirTask) Priority() Priority { return HighPriority }
|
|
func (t *ReadDirTask) TaskType() TaskType { return TypeReadDir }
|
|
func (t *ReadDirTask) TaskID() string { return t.taskID }
|
|
func (t *ReadDirTask) DirPath() string { return t.Dir }
|
|
func (t *ReadDirTask) Context() context.Context { return t.ctx }
|
|
func (t *ReadDirTask) Timeout() time.Duration { return 5 * time.Second }
|
|
func (t *ReadDirTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// SaveStateTask persists application state.
|
|
type SaveStateTask struct {
|
|
taskID string
|
|
Path string
|
|
Content []byte
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewSaveStateTask creates a new SaveStateTask.
|
|
func NewSaveStateTask(path string, content []byte, fs FileSystem) *SaveStateTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
id := taskIDCounter.Add(1)
|
|
return &SaveStateTask{
|
|
taskID: fmt.Sprintf("savestate-%s-%d", filepath.Base(path), id),
|
|
Path: path,
|
|
Content: content,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *SaveStateTask) Execute() Result {
|
|
err := t.FS.WriteFileAtomic(t.Path, t.Content)
|
|
if err != nil {
|
|
return Result{TaskID: t.taskID, TaskType: TypeSaveState, Success: false, Error: fmt.Errorf("failed to save state: %w", err)}
|
|
}
|
|
return Result{TaskID: t.taskID, TaskType: TypeSaveState, Success: true}
|
|
}
|
|
|
|
func (t *SaveStateTask) Priority() Priority { return LowPriority }
|
|
func (t *SaveStateTask) TaskType() TaskType { return TypeSaveState }
|
|
func (t *SaveStateTask) TaskID() string { return t.taskID }
|
|
func (t *SaveStateTask) DirPath() string { return filepath.Dir(t.Path) }
|
|
func (t *SaveStateTask) Context() context.Context { return t.ctx }
|
|
func (t *SaveStateTask) Timeout() time.Duration { return 5 * time.Second }
|
|
func (t *SaveStateTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// SaveUndoTask persists undo stack.
|
|
type SaveUndoTask struct {
|
|
taskID string
|
|
Path string
|
|
Content []byte
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewSaveUndoTask creates a new SaveUndoTask.
|
|
func NewSaveUndoTask(path string, content []byte, fs FileSystem) *SaveUndoTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
id := taskIDCounter.Add(1)
|
|
return &SaveUndoTask{
|
|
taskID: fmt.Sprintf("saveundo-%s-%d", filepath.Base(path), id),
|
|
Path: path,
|
|
Content: content,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *SaveUndoTask) Execute() Result {
|
|
err := t.FS.WriteFileAtomic(t.Path, t.Content)
|
|
if err != nil {
|
|
return Result{TaskID: t.taskID, TaskType: TypeSaveUndo, Success: false, Error: fmt.Errorf("failed to save undo: %w", err)}
|
|
}
|
|
return Result{TaskID: t.taskID, TaskType: TypeSaveUndo, Success: true}
|
|
}
|
|
|
|
func (t *SaveUndoTask) Priority() Priority { return LowPriority }
|
|
func (t *SaveUndoTask) TaskType() TaskType { return TypeSaveUndo }
|
|
func (t *SaveUndoTask) TaskID() string { return t.taskID }
|
|
func (t *SaveUndoTask) DirPath() string { return filepath.Dir(t.Path) }
|
|
func (t *SaveUndoTask) Context() context.Context { return t.ctx }
|
|
func (t *SaveUndoTask) Timeout() time.Duration { return 5 * time.Second }
|
|
func (t *SaveUndoTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// ReadFileTask reads the full content of a file.
|
|
// Used as a fallback for small files or initial load before chunking is set up.
|
|
type ReadFileTask struct {
|
|
taskID string
|
|
Path string
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewReadFileTask creates a new ReadFileTask.
|
|
func NewReadFileTask(path string, fs FileSystem) *ReadFileTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &ReadFileTask{
|
|
taskID: fmt.Sprintf("readfile-%s", filepath.Base(path)),
|
|
Path: path,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *ReadFileTask) Execute() Result {
|
|
content, err := t.FS.ReadFile(t.Path)
|
|
if err != nil {
|
|
return Result{TaskID: t.taskID, TaskType: TypeReadFile, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to read file: %w", err)}
|
|
}
|
|
return Result{TaskID: t.taskID, TaskType: TypeReadFile, FilePath: t.Path, Success: true, Data: content}
|
|
}
|
|
|
|
func (t *ReadFileTask) Priority() Priority { return HighPriority }
|
|
func (t *ReadFileTask) TaskType() TaskType { return TypeReadFile }
|
|
func (t *ReadFileTask) TaskID() string { return t.taskID }
|
|
func (t *ReadFileTask) DirPath() string { return filepath.Dir(t.Path) }
|
|
func (t *ReadFileTask) Context() context.Context { return t.ctx }
|
|
func (t *ReadFileTask) Timeout() time.Duration { return 5 * time.Second }
|
|
func (t *ReadFileTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// SearchData is the payload of a completed SearchTask: the byte ranges
|
|
// [start, end) of every match of Query in the scanned Text, ascending and
|
|
// non-overlapping. Gen is the query generation the scan was dispatched for,
|
|
// so the caller can drop stale results.
|
|
type SearchData struct {
|
|
Gen uint64
|
|
Matches [][2]int
|
|
}
|
|
|
|
// SearchTask scans an in-memory content snapshot for a plain, case-
|
|
// insensitive substring. The caller snapshots the content (the editor's
|
|
// in-range files are fully resident) so the scan never races edits or chunk
|
|
// state; a superseding query gets a higher Gen and its result is dropped by
|
|
// the caller. Matching is plain substring (no regex), case-insensitive via
|
|
// unicode lower-casing of both sides, matching the browser's name filter.
|
|
type SearchTask struct {
|
|
taskID string
|
|
Text string
|
|
Query string
|
|
Gen uint64
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewSearchTask creates a new SearchTask.
|
|
func NewSearchTask(text, query string, gen uint64) *SearchTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &SearchTask{
|
|
taskID: fmt.Sprintf("search-%d", taskIDCounter.Add(1)),
|
|
Text: text,
|
|
Query: query,
|
|
Gen: gen,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *SearchTask) Execute() Result {
|
|
matches := FindSubstring(t.Text, t.Query)
|
|
return Result{TaskID: t.taskID, TaskType: TypeSearch, Success: true, Data: SearchData{Gen: t.Gen, Matches: matches}}
|
|
}
|
|
|
|
// FindSubstring returns the byte ranges [start, end) of every occurrence of
|
|
// query in text, case-insensitively. Occurrences are ascending and non-
|
|
// overlapping (a match consumes its range, so "aa" finds one match in
|
|
// "aaa"). An empty query matches nothing.
|
|
func FindSubstring(text, query string) [][2]int {
|
|
if query == "" {
|
|
return nil
|
|
}
|
|
q := toLowerFold(query)
|
|
t := toLowerFold(text)
|
|
var matches [][2]int
|
|
from := 0
|
|
for {
|
|
i := indexOf(t[from:], q)
|
|
if i < 0 {
|
|
return matches
|
|
}
|
|
abs := from + i
|
|
matches = append(matches, [2]int{abs, abs + len(q)})
|
|
from = abs + len(q)
|
|
}
|
|
}
|
|
|
|
// toLowerFold lower-cases s without allocating when s is already
|
|
// lower-case ASCII (the common case for both query and content).
|
|
func toLowerFold(s string) string {
|
|
for i := 0; i < len(s); i++ {
|
|
if b := s[i]; b >= 'A' && b <= 'Z' {
|
|
return strings.ToLower(s)
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// indexOf is strings.Index but kept local so the scan above reads as one
|
|
// unit; it is the standard library's two-way search.
|
|
func indexOf(s, sub string) int { return strings.Index(s, sub) }
|
|
|
|
func (t *SearchTask) Priority() Priority { return MediumPriority }
|
|
func (t *SearchTask) TaskType() TaskType { return TypeSearch }
|
|
func (t *SearchTask) TaskID() string { return t.taskID }
|
|
func (t *SearchTask) DirPath() string { return "" }
|
|
func (t *SearchTask) Context() context.Context { return t.ctx }
|
|
func (t *SearchTask) Timeout() time.Duration { return 10 * time.Second }
|
|
func (t *SearchTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// WriteFileTask writes content to a file.
|
|
type WriteFileTask struct {
|
|
taskID string
|
|
Path string
|
|
Content []byte
|
|
FS FileSystem
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewWriteFileTask creates a new WriteFileTask.
|
|
func NewWriteFileTask(path string, content []byte, fs FileSystem) *WriteFileTask {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &WriteFileTask{
|
|
taskID: fmt.Sprintf("writefile-%s", filepath.Base(path)),
|
|
Path: path,
|
|
Content: content,
|
|
FS: fs,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
func (t *WriteFileTask) Execute() Result {
|
|
err := t.FS.WriteFile(t.Path, t.Content)
|
|
if err != nil {
|
|
return Result{TaskID: t.taskID, TaskType: TypeWriteFile, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to write file: %w", err)}
|
|
}
|
|
return Result{TaskID: t.taskID, TaskType: TypeWriteFile, FilePath: t.Path, Success: true}
|
|
}
|
|
|
|
func (t *WriteFileTask) Priority() Priority { return LowPriority }
|
|
func (t *WriteFileTask) TaskType() TaskType { return TypeWriteFile }
|
|
func (t *WriteFileTask) TaskID() string { return t.taskID }
|
|
func (t *WriteFileTask) DirPath() string { return filepath.Dir(t.Path) }
|
|
func (t *WriteFileTask) Context() context.Context { return t.ctx }
|
|
func (t *WriteFileTask) Timeout() time.Duration { return 5 * time.Second }
|
|
func (t *WriteFileTask) Cancel() {
|
|
if t.cancel != nil {
|
|
t.cancel()
|
|
}
|
|
}
|
|
|
|
// --- Mock File System (for testing/development) ---
|
|
// MockFS implements the pool.FileSystem interface for testing.
|
|
|
|
type MockFS struct {
|
|
files map[string][]byte
|
|
}
|
|
|
|
func NewMockFS() *MockFS {
|
|
return &MockFS{
|
|
files: make(map[string][]byte),
|
|
}
|
|
}
|
|
|
|
func (m *MockFS) ReadFile(path string) ([]byte, error) {
|
|
content, ok := m.files[path]
|
|
if !ok {
|
|
return nil, fmt.Errorf("file not found: %s", path)
|
|
}
|
|
return content, nil
|
|
}
|
|
|
|
func (m *MockFS) ReadFileAt(path string, offset, size int) ([]byte, error) {
|
|
content, ok := m.files[path]
|
|
if !ok {
|
|
return nil, fmt.Errorf("file not found: %s", path)
|
|
}
|
|
if offset >= len(content) {
|
|
return []byte{}, nil
|
|
}
|
|
end := offset + size
|
|
if end > len(content) {
|
|
end = len(content)
|
|
}
|
|
result := make([]byte, len(content[offset:end]))
|
|
copy(result, content[offset:end])
|
|
return result, nil
|
|
}
|
|
|
|
func (m *MockFS) WriteFile(path string, content []byte) error {
|
|
m.files[path] = content
|
|
return nil
|
|
}
|
|
|
|
func (m *MockFS) WriteFileAtomic(path string, content []byte) error {
|
|
m.files[path] = content
|
|
return nil
|
|
}
|
|
|
|
func (m *MockFS) DeleteFile(path string) error {
|
|
delete(m.files, path)
|
|
return nil
|
|
}
|
|
|
|
func (m *MockFS) CreateDir(path string) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockFS) DirExists(path string) bool {
|
|
_, ok := m.files[path]
|
|
return ok
|
|
}
|
|
|
|
func (m *MockFS) FileExists(path string) bool {
|
|
_, ok := m.files[path]
|
|
return ok
|
|
}
|
|
|
|
// mockFileInfo implements io.FileInfo for MockFS.
|
|
type mockFileInfo struct {
|
|
name string
|
|
size int64
|
|
mode uint32
|
|
modTime time.Time
|
|
}
|
|
|
|
func (f *mockFileInfo) Name() string { return f.name }
|
|
func (f *mockFileInfo) Size() int64 { return f.size }
|
|
func (f *mockFileInfo) Mode() os.FileMode { return os.FileMode(f.mode) }
|
|
func (f *mockFileInfo) ModTime() time.Time { return f.modTime }
|
|
func (f *mockFileInfo) IsDir() bool { return false }
|
|
func (f *mockFileInfo) Sys() any { return nil }
|
|
|
|
// mockDirEntry implements types.DirEntry for MockFS ReadDir.
|
|
type mockDirEntry struct {
|
|
name string
|
|
isDir bool
|
|
}
|
|
|
|
func (e *mockDirEntry) Name() string { return e.name }
|
|
func (e *mockDirEntry) IsDir() bool { return e.isDir }
|
|
func (e *mockDirEntry) Info() (os.FileInfo, error) {
|
|
return &mockFileInfo{name: e.name, size: 0}, nil
|
|
}
|
|
|
|
func (m *MockFS) ReadDir(path string) ([]types.DirEntry, error) {
|
|
var entries []types.DirEntry
|
|
for p := range m.files {
|
|
if filepath.Dir(p) == path {
|
|
entries = append(entries, &mockDirEntry{name: filepath.Base(p), isDir: false})
|
|
}
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
var _ FileSystem = (*MockFS)(nil) // Compile-time interface check
|