Pad/internal/editor/mock_setup.go
Greg Pomerantz d73cb0be2c Integrate real filesystem with interface abstraction
- Define pool.FileSystem interface in internal/io/pool/filesystem.go
- Move DirEntry interface to internal/io/pool/types/types.go to break
  the pool <-> mock import cycle
- Implement real.FileSystem with atomic writes (temp file + os.Rename)
- Update mock.FileSystem to satisfy pool.FileSystem interface
- Update worker pool tasks to accept pool.FileSystem interface
- Update main.go to use RealFileSystem by default, with -root flag
- Update all browser/editor/test references to types.DirEntry
2026-06-04 16:19:38 -04:00

253 lines
9.1 KiB
Go

package editor
import (
"sort"
"strings"
"time"
"pad/internal/browser"
"pad/internal/io/pool"
"pad/internal/io/pool/mock"
"pad/internal/io/pool/types"
)
// populateMockFileSystem seeds the mock filesystem with sample data for testing.
// Creates a realistic directory structure with subdirectories, various file types,
// and varied sizes/dates to exercise the browser functionality.
func populateMockFileSystem(fs pool.FileSystem) {
mfs, ok := fs.(*mock.FileSystem)
if !ok {
panic("populateMockFileSystem requires a *mock.FileSystem")
}
baseTime := time.Date(2026, 5, 15, 10, 0, 0, 0, time.UTC)
// Create root directory - required for ReadDir("/") to succeed
mfs.AddDir("/", baseTime)
// --- Root directories ---
dirs := []string{
"/Documents",
"/Pictures",
"/Music",
"/Downloads",
"/Projects",
"/Projects/pad",
"/Projects/goplus",
"/Documents/Work",
"/Documents/Personal",
"/Pictures/Vacation",
}
for i, d := range dirs {
mfs.AddDir(d, baseTime.AddDate(0, 0, -i))
}
// --- Root files ---
rootFiles := []struct {
name string
content string
days int
}{
{"README.md", "# Pad Text Editor\n\nWelcome to Pad, a high-performance text editor built in Go and Gio.\nThis is a mock file loaded from the in-memory mock filesystem.\n\n## Features\n- Virtualized scroll list\n- Fast case-insensitive search\n- Pre-sorted directory indexing\n", -30},
{"config.yaml", "editor:\n font_size: 14\n line_height: 1.2\n word_wrap: true\ntheme:\n background: \"#ffffff\"\n foreground: \"#000000\"\n", -60},
{"notes.txt", "Pad Browser Implementation Todo:\n1. Implement directory navigation (Done)\n2. Implement file tapping to open in editor (Done)\n3. Implement external change detection\n4. Add alphabetical index sidebar\n", -15},
{"setup.log", "2026-06-01 08:00:00 [INFO] Initializing Pad system...\n2026-06-01 08:00:01 [INFO] Mock filesystem populated successfully.\n2026-06-01 08:00:02 [INFO] Worker pool started with 4 goroutines.\n2026-06-01 08:00:03 [INFO] Startup complete, showing browser page.\n", -7},
{".gitignore", "*.log\n.pad/\n.git/\n*.tmp\n", -90},
}
for _, f := range rootFiles {
mfs.AddFile("/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
}
// --- Documents/Work files ---
workFiles := []struct {
name string
content string
days int
}{
{"Q1_Report.docx", "Pad Word Document Mock\n\nQuarter 1 Performance Report:\nOverall efficiency increased by 15% following implementation of channel-based thread communication.\n", -45},
{"Q2_Report.docx", "Pad Word Document Mock\n\nQuarter 2 Planning:\nTargeting zero-alloc layout computations and sub-16ms frame times across all devices.\n", -14},
{"Budget_2026.xlsx", "Pad Spreadsheet Mock\n\nQ1 Expenses: $12,450.00\nQ2 Budgeted: $15,000.00\nTotal Project Allocation: $50,000.00\n", -30},
{"Meeting_Notes.txt", "Team Sync Notes - June 1, 2026\n\nAttendees: Greg, Agent\n- Solved scroll clamping with search query active.\n- Wired ListView taps to HandleBrowserTap.\n- Dispatched ReadFileTask to render file contents dynamically.\n", -1},
{"Presentation.pptx", "Pad Presentation Mock\n\nSlide 1: Title - No Practical Limits\nSlide 2: Virtualization is the key\nSlide 3: Single-Owner thread design\n", -21},
}
for _, f := range workFiles {
mfs.AddFile("/Documents/Work/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
}
// --- Documents/Personal files ---
personalFiles := []struct {
name string
content string
days int
}{
{"Resume.pdf", "Greg Pomerantz\nGo & Graphics Software Engineer\n\nSkills: Go, Gio, OpenGL, Concurrency, Virtualization\n", -60},
{"Tax_Return_2025.pdf", "Form 1040 Mock\n\nAdjusted Gross Income: $120,000.00\nTotal Tax Due: $18,450.00\nRefund Owed: $1,200.00\n", -90},
{"Recipe_Collection.txt", "Classic Pancakes:\n- 1 cup flour\n- 2 tbsp sugar\n- 1 tbsp baking powder\n- 1/2 tsp salt\n- 1 cup milk\n- 1 egg\n- 2 tbsp melted butter\n", -120},
{"Letter_to_Friend.txt", "Hey friend,\n\nI've been working on a really cool Go text editor called Pad. It uses Gio for rendering and handles massive files without blocking the GUI thread. Talk soon!\n", -5},
}
for _, f := range personalFiles {
mfs.AddFile("/Documents/Personal/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
}
// --- Projects/pad source files ---
padFiles := []struct {
name string
content string
days int
}{
{"main.go", "package main\n\nfunc main() {\n\t// Startup logic\n}\n", -2},
{"state.go", "package editor\n\n// Embedded state engine\n", -2},
{"logic.go", "package editor\n\n// Concurrency logic loop\n", -1},
{"browser.go", "package browser\n\n// Core directory browser\n", -7},
{"types.go", "package browser\n\n// Struct definitions\n", -7},
{"index.go", "package browser\n\n// Cached indices\n", -7},
{"layout.go", "package browser\n\n// Layout tree creation\n", -5},
{"handlers.go", "package browser\n\n// Event handling\n", -3},
{"go.mod", "module pad\n\ngo 1.24.2\n", -30},
{"go.sum", "gioui.org v0.9.0 h1:...\n", -30},
}
for _, f := range padFiles {
mfs.AddFile("/Projects/pad/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
}
// --- Pictures/Vacation files ---
vacationFiles := []struct {
name string
content string
days int
}{
{"IMG_0001.jpg", "[Binary JPEG Image Data Mock]", -180},
{"IMG_0002.jpg", "[Binary JPEG Image Data Mock]", -180},
{"IMG_0003.jpg", "[Binary JPEG Image Data Mock]", -179},
{"IMG_0004.jpg", "[Binary JPEG Image Data Mock]", -179},
{"IMG_0005.jpg", "[Binary JPEG Image Data Mock]", -178},
}
for _, f := range vacationFiles {
mfs.AddFile("/Pictures/Vacation/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
}
// --- Downloads files ---
downloads := []struct {
name string
content string
days int
}{
{"installer.dmg", "[DMG Disk Image Mock - 100MB]", -10},
{"archive.tar.gz", "[Compressed TAR GZ Archive Mock - 50MB]", -20},
{"patch.diff", "--- a/internal/browser/handlers.go\n+++ b/internal/browser/handlers.go\n@@ -65,3 +65,3 @@\n-bm.NavigateTo(newPath)\n+bm.NavigateTo(entry.Path)\n", -3},
}
for _, f := range downloads {
mfs.AddFile("/Downloads/"+f.name, []byte(f.content), baseTime.AddDate(0, 0, f.days))
}
}
// sortModeKey converts a browser.SortMode to its JSON key string.
func sortModeKey(mode browser.SortMode) string {
switch mode {
case 0: // SortModeNameAsc
return "name_asc"
case 1: // SortModeNameDesc
return "name_desc"
case 2: // SortModeDateAsc
return "date_asc"
case 3: // SortModeDateDesc
return "date_desc"
default:
return ""
}
}
// modeCount returns the total number of sort modes.
func modeCount() int {
return 4
}
// buildBrowserIndex converts types.DirEntry results into a browser.DirectoryIndex
// with pre-computed position maps for all sort modes.
func buildBrowserIndex(entries []types.DirEntry) *browser.DirectoryIndex {
var browserEntries []browser.Entry
for _, e := range entries {
info, err := e.Info()
if err != nil {
continue
}
browserEntries = append(browserEntries, browser.Entry{
Path: e.Name(),
Name: e.Name(),
Size: info.Size(),
ModTime: info.ModTime(),
IsDir: info.IsDir(),
})
}
// Build position maps for all sort modes
sortOrders := make(map[string][]int)
for mode := browser.SortMode(0); mode < browser.SortMode(modeCount()); mode++ {
key := sortModeKey(mode)
if key != "" {
sortOrders[key] = buildPositionMap(browserEntries, mode)
}
}
return &browser.DirectoryIndex{
Path: "/",
EntryCount: len(browserEntries),
Entries: browserEntries,
SortOrders: sortOrders,
}
}
// buildPositionMap creates a sorted index → raw index mapping.
func buildPositionMap(entries []browser.Entry, mode browser.SortMode) []int {
n := len(entries)
indices := make([]int, n)
for i := range indices {
indices[i] = i
}
// Sort indices based on entry comparison
cmp := comparator(mode)
sort.SliceStable(indices, func(i, j int) bool {
a, b := entries[indices[i]], entries[indices[j]]
return cmp(a, b) < 0
})
return indices
}
// comparator returns a comparison function for the given SortMode.
// Returns negative if a < b, zero if equal, positive if a > b.
func comparator(mode browser.SortMode) func(a, b browser.Entry) int {
switch mode {
case 0: // SortModeNameAsc
return func(a, b browser.Entry) int {
if c := strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)); c != 0 {
return c
}
return strings.Compare(a.Name, b.Name)
}
case 1: // SortModeNameDesc
return func(a, b browser.Entry) int {
if c := strings.Compare(strings.ToLower(b.Name), strings.ToLower(a.Name)); c != 0 {
return c
}
return strings.Compare(b.Name, a.Name)
}
case 2: // SortModeDateAsc
return func(a, b browser.Entry) int {
if c := a.ModTime.Compare(b.ModTime); c != 0 {
return c
}
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
}
case 3: // SortModeDateDesc
return func(a, b browser.Entry) int {
if c := b.ModTime.Compare(a.ModTime); c != 0 {
return c
}
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
}
default:
return func(a, b browser.Entry) int { return 0 }
}
}