- Add ChunkedBuffer for 64KB chunked file access with dirty-chunk eviction protection - Add LineIndex for precise byte-offset-to-line-number mapping - Refactor IO task system with context cancellation, typed priorities, and new task types (ReadChunk, BuildLineIndex, StatFile) - Add ReadFileAt to FileSystem interface (mock + real implementations) - Integrate virtual scrolling into editor layout - Add comprehensive tests for chunked buffer eviction, dirty-chunk safety, and full edit lifecycle
165 lines
4.4 KiB
Go
165 lines
4.4 KiB
Go
package browser
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"pad/internal/io/pool"
|
|
"pad/internal/io/pool/types"
|
|
)
|
|
|
|
var ErrDirNotFound = fmt.Errorf("directory not found")
|
|
|
|
type BrowserManager struct {
|
|
state *BrowserState
|
|
workerPool *pool.WorkerPool
|
|
fs pool.FileSystem
|
|
}
|
|
|
|
func NewBrowserManager(state *BrowserState, wp *pool.WorkerPool, fs pool.FileSystem) (*BrowserManager, error) {
|
|
return &BrowserManager{
|
|
state: state,
|
|
workerPool: wp,
|
|
fs: fs,
|
|
}, nil
|
|
}
|
|
|
|
func (bm *BrowserManager) NavigateTo(dirPath string) {
|
|
fmt.Printf("NavigateTo: %s\n", dirPath)
|
|
bm.state.CurrentPath = dirPath
|
|
navigateToDirectory(bm.state, dirPath)
|
|
bm.state.Loading = true
|
|
|
|
// Dispatch BuildIndexTask to the worker pool.
|
|
// In a real app, this would be a task that reads and sorts the directory.
|
|
task := pool.NewBuildIndexTask(dirPath, bm.fs)
|
|
bm.workerPool.Dispatch(task)
|
|
}
|
|
|
|
func (bm *BrowserManager) OnScroll() {
|
|
if bm.state.TotalEntries == 0 {
|
|
return
|
|
}
|
|
|
|
// Identify unloaded pages in the visible + prefetch range
|
|
minPage, maxPage := computeVisiblePageRange(bm.state)
|
|
var pagesToLoad []int
|
|
for i := minPage; i <= maxPage; i++ {
|
|
if page, ok := bm.state.Pages[i]; !ok || !page.Loaded {
|
|
pagesToLoad = append(pagesToLoad, i)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("OnScroll: minPage=%d, maxPage=%d, pagesToLoad=%v\n", minPage, maxPage, pagesToLoad)
|
|
|
|
// Dispatch LoadPagesTask for unloaded pages
|
|
if len(pagesToLoad) > 0 {
|
|
fmt.Printf("OnScroll: Dispatching LoadPagesTask for pages %v\n", pagesToLoad)
|
|
task := pool.NewLoadPagesTask(bm.state.CurrentPath, pagesToLoad, bm.fs)
|
|
bm.workerPool.Dispatch(task)
|
|
}
|
|
|
|
// Evict distant pages to save memory
|
|
bm.state.EvictPages()
|
|
}
|
|
|
|
func (bm *BrowserManager) HandleResult(result pool.Result) {
|
|
if !result.IsBrowserResult() {
|
|
return
|
|
}
|
|
|
|
switch result.TaskType {
|
|
case pool.TypeBuildIndex:
|
|
if result.Success {
|
|
bm.handleBuildIndexSuccess(result)
|
|
} else {
|
|
bm.handleError(result)
|
|
}
|
|
case pool.TypeLoadPages:
|
|
if result.Success {
|
|
bm.handleLoadPagesSuccess(result)
|
|
} else {
|
|
bm.handleError(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (bm *BrowserManager) handleBuildIndexSuccess(result pool.Result) {
|
|
fmt.Printf("handleBuildIndexSuccess: TotalEntries=%d\n", len(result.Data.([]types.DirEntry)))
|
|
bm.state.Loading = false
|
|
|
|
// In the mock implementation, BuildIndexTask returns []types.DirEntry.
|
|
// We need to convert these to browser.Entry and build the SortIndex.
|
|
entries := result.Data.([]types.DirEntry)
|
|
|
|
var browserEntries []Entry
|
|
// Add ".." entry if not at root
|
|
if bm.state.CurrentPath != "/" && bm.state.CurrentPath != "." {
|
|
browserEntries = append(browserEntries, Entry{
|
|
Path: filepath.Dir(bm.state.CurrentPath),
|
|
Name: "..",
|
|
Size: 0,
|
|
ModTime: time.Now(),
|
|
IsDir: true,
|
|
})
|
|
}
|
|
|
|
for _, e := range entries {
|
|
info, _ := e.Info()
|
|
browserEntries = append(browserEntries, Entry{
|
|
Path: filepath.Join(bm.state.CurrentPath, e.Name()),
|
|
Name: e.Name(),
|
|
Size: info.Size(),
|
|
ModTime: info.ModTime(),
|
|
IsDir: e.IsDir(),
|
|
})
|
|
}
|
|
|
|
// Build the sorted index with position maps
|
|
bm.state.SortIndex = &DirectoryIndex{
|
|
Path: bm.state.CurrentPath,
|
|
EntryCount: len(browserEntries),
|
|
Entries: browserEntries,
|
|
SortOrders: make(map[string][]int),
|
|
}
|
|
|
|
// Compute position maps for all sort modes
|
|
for mode := SortMode(0); mode < SortMode(modeCount()); mode++ {
|
|
key := sortModeKey(mode)
|
|
if key != "" {
|
|
bm.state.SortIndex.SortOrders[key] = buildPositionMap(browserEntries, mode)
|
|
}
|
|
}
|
|
|
|
bm.state.TotalEntries = len(browserEntries)
|
|
fmt.Printf("handleBuildIndexSuccess: TotalEntries set to %d\n", bm.state.TotalEntries)
|
|
|
|
// Trigger initial page load
|
|
bm.OnScroll()
|
|
}
|
|
|
|
func (bm *BrowserManager) handleLoadPagesSuccess(result pool.Result) {
|
|
// In a real implementation, LoadPagesTask would return the actual entry data.
|
|
// In our current mock/placeholder, it just returns the page indices.
|
|
// We'll load the data from the SortIndex.
|
|
pageIndices := result.Data.([]int)
|
|
|
|
for _, idx := range pageIndices {
|
|
page := loadPageFromIndex(bm.state, idx)
|
|
if page != nil {
|
|
bm.state.Pages[idx] = page
|
|
}
|
|
}
|
|
}
|
|
|
|
func (bm *BrowserManager) handleError(result pool.Result) {
|
|
fmt.Printf("handleError: TaskType=%d, Error=%v\n", result.TaskType, result.Error)
|
|
bm.state.Loading = false
|
|
// For now, just reset the state or log the error.
|
|
// In production, we'd show a toast or error message.
|
|
if bm.state.CurrentPath == bm.state.CurrentPath {
|
|
bm.state.Reset()
|
|
}
|
|
}
|