From 06b14442079037ab06be1e18ae020e6ceb0bd8fa Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Sun, 23 Aug 2026 10:03:27 -0400 Subject: [PATCH] gofmt: format all remaining files with the go1.27 toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/browser/browser.go | 3 +- internal/browser/browser_test.go | 10 +- internal/browser/index.go | 10 +- internal/browser/index_test.go | 2 - internal/browser/layout.go | 4 +- internal/browser/layout_test.go | 2 +- internal/browser/lazy_loading_test.go | 2 +- internal/browser/pixel_scroll_test.go | 4 +- internal/browser/scroll_test.go | 6 +- internal/browser/search_test.go | 2 +- internal/browser/sort.go | 1 - internal/browser/types.go | 30 ++-- internal/browser/types_test.go | 12 +- internal/editor/auto_save_test.go | 14 +- internal/editor/buffer_test.go | 2 +- internal/editor/e2e_test.go | 6 +- internal/editor/filename_test.go | 18 +-- internal/editor/ime_range_test.go | 4 +- internal/editor/scroll_fix_test.go | 78 +++++----- internal/editor/wrap_apply_test.go | 1 - internal/io/pool/context_test.go | 24 ++-- internal/io/pool/mock/filesystem.go | 8 +- internal/io/pool/task.go | 199 +++++++++++++++----------- internal/io/pool/types/types.go | 26 ++-- internal/io/pool/worker_pool.go | 18 +-- internal/io/pool/worker_pool_test.go | 18 +-- internal/test/e2e/harness_test.go | 2 +- internal/test/e2e/helpers.go | 2 +- internal/test/e2e/restore_test.go | 2 +- internal/test/e2e/search_test.go | 2 +- internal/ui/layout.go | 14 +- 31 files changed, 279 insertions(+), 247 deletions(-) diff --git a/internal/browser/browser.go b/internal/browser/browser.go index 4bf760c..4505e26 100644 --- a/internal/browser/browser.go +++ b/internal/browser/browser.go @@ -66,7 +66,7 @@ func computeVisiblePageRange(s *BrowserState) (minPage, maxPage int) { if minPage < 0 { minPage = 0 } - maxPage = (s.GetScrollIndex() + s.VisibleCount)/PageSize + PrefetchDist + maxPage = (s.GetScrollIndex()+s.VisibleCount)/PageSize + PrefetchDist return minPage, maxPage } @@ -164,4 +164,3 @@ func filterEntriesByQuery(entries []Entry, query string) []int { return results } - diff --git a/internal/browser/browser_test.go b/internal/browser/browser_test.go index 42c5810..12731b2 100644 --- a/internal/browser/browser_test.go +++ b/internal/browser/browser_test.go @@ -28,7 +28,7 @@ func TestLoadPage_FromIndexAtOffset(t *testing.T) { // Build position maps for sort modes s.SortIndex = &DirectoryIndex{ - Entries: allEntries, + Entries: allEntries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(allEntries, SortModeNameAsc), "date_desc": buildPositionMap(allEntries, SortModeDateDesc), @@ -76,7 +76,7 @@ func TestLoadPage_LastPagePartialFromIndex(t *testing.T) { // Build position maps for sort modes s.SortIndex = &DirectoryIndex{ - Entries: allEntries, + Entries: allEntries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(allEntries, SortModeNameAsc), "date_desc": buildPositionMap(allEntries, SortModeDateDesc), @@ -116,7 +116,7 @@ func TestLoadPage_OutOfBoundsFromIndex(t *testing.T) { // Build position maps for sort modes s.SortIndex = &DirectoryIndex{ - Entries: allEntries, + Entries: allEntries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(allEntries, SortModeNameAsc), "date_desc": buildPositionMap(allEntries, SortModeDateDesc), @@ -195,7 +195,7 @@ func TestGetEntryByIndex(t *testing.T) { // Build position maps for sort modes s.SortIndex = &DirectoryIndex{ - Entries: allEntries, + Entries: allEntries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(allEntries, SortModeNameAsc), "date_desc": buildPositionMap(allEntries, SortModeDateDesc), @@ -236,7 +236,7 @@ func TestGetEntryByIndex_UnloadedPage(t *testing.T) { // Build position maps for sort modes s.SortIndex = &DirectoryIndex{ - Entries: allEntries, + Entries: allEntries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(allEntries, SortModeNameAsc), "date_desc": buildPositionMap(allEntries, SortModeDateDesc), diff --git a/internal/browser/index.go b/internal/browser/index.go index 9986b75..0ac2a34 100644 --- a/internal/browser/index.go +++ b/internal/browser/index.go @@ -13,11 +13,11 @@ import ( // DirectoryIndex holds a cached index of a directory's contents. // Per design: index stores raw metadata; sorting is pre-computed via position maps. type DirectoryIndex struct { - Path string `json:"path"` - Mtime time.Time `json:"mtime"` - EntryCount int `json:"entry_count"` - Entries []Entry `json:"entries"` - SortOrders map[string][]int `json:"sort_orders,omitempty"` // key: "name_asc", "name_desc", etc. + Path string `json:"path"` + Mtime time.Time `json:"mtime"` + EntryCount int `json:"entry_count"` + Entries []Entry `json:"entries"` + SortOrders map[string][]int `json:"sort_orders,omitempty"` // key: "name_asc", "name_desc", etc. } // getCachePath returns the path to the cached index file for a directory. diff --git a/internal/browser/index_test.go b/internal/browser/index_test.go index f9962e2..31112cc 100644 --- a/internal/browser/index_test.go +++ b/internal/browser/index_test.go @@ -224,5 +224,3 @@ func TestBuildIndex_LargeDirectory(t *testing.T) { t.Errorf("buildIndex took %v, expected < 5s", elapsed) } } - - diff --git a/internal/browser/layout.go b/internal/browser/layout.go index 9634322..2f1f862 100644 --- a/internal/browser/layout.go +++ b/internal/browser/layout.go @@ -160,8 +160,6 @@ func recomputeSearchResults(s *BrowserState) { } } - - // ComputeVisibleEntriesForTest is exported for testing purposes. // It builds the list of ui.ListItem entries that should be rendered. func ComputeVisibleEntriesForTest(state *BrowserState) []ui.ListItem { @@ -224,7 +222,7 @@ func computeSearchResults(state *BrowserState) []ui.ListItem { var items []ui.ListItem for idx := startIndex; idx < endIndex; idx++ { rawIdx := state.SearchResults[idx] - + entry, ok := getEntryByIndex(state, rawIdx) if !ok { // Page not loaded yet; add placeholder diff --git a/internal/browser/layout_test.go b/internal/browser/layout_test.go index 5bea770..cc859a6 100644 --- a/internal/browser/layout_test.go +++ b/internal/browser/layout_test.go @@ -32,7 +32,7 @@ func makeTestState(t *testing.T, entryCount int, scrollIndex int, visibleCount i // Build position maps for sort modes s.SortIndex = &DirectoryIndex{ - Entries: entries, + Entries: entries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(entries, SortModeNameAsc), "date_desc": buildPositionMap(entries, SortModeDateDesc), diff --git a/internal/browser/lazy_loading_test.go b/internal/browser/lazy_loading_test.go index b39c2f4..62500ad 100644 --- a/internal/browser/lazy_loading_test.go +++ b/internal/browser/lazy_loading_test.go @@ -67,7 +67,7 @@ func TestLazyLoadingLargeDirectory(t *testing.T) { // 3. Scroll down to page 50 state.ScrollOffset = 50 * 100 * state.EntryHeight // Scroll to middle bm.OnScroll() - + // Wait for load pages result for scroll res, ok = getResult(15 * time.Second) if !ok { diff --git a/internal/browser/pixel_scroll_test.go b/internal/browser/pixel_scroll_test.go index b476bb3..a8cab28 100644 --- a/internal/browser/pixel_scroll_test.go +++ b/internal/browser/pixel_scroll_test.go @@ -145,7 +145,7 @@ func TestPixelScroll_EvictionTriggered(t *testing.T) { entries[i] = NewEntry("/test/file.txt", "file.txt", 100, s.TapTimestamp, false) } s.SortIndex = &DirectoryIndex{ - Entries: entries, + Entries: entries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(entries, SortModeNameAsc), "date_desc": buildPositionMap(entries, SortModeDateDesc), @@ -184,7 +184,7 @@ func TestPixelScroll_PrefetchTriggered(t *testing.T) { entries[i] = NewEntry("/test/file.txt", "file.txt", 100, s.TapTimestamp, false) } s.SortIndex = &DirectoryIndex{ - Entries: entries, + Entries: entries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(entries, SortModeNameAsc), "date_desc": buildPositionMap(entries, SortModeDateDesc), diff --git a/internal/browser/scroll_test.go b/internal/browser/scroll_test.go index 2419476..45b6f39 100644 --- a/internal/browser/scroll_test.go +++ b/internal/browser/scroll_test.go @@ -83,7 +83,7 @@ func TestScroll_PrefetchTriggered(t *testing.T) { } // Build position maps for sort modes s.SortIndex = &DirectoryIndex{ - Entries: entries, + Entries: entries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(entries, SortModeNameAsc), "date_desc": buildPositionMap(entries, SortModeDateDesc), @@ -131,7 +131,7 @@ func TestScroll_EvictionTriggered(t *testing.T) { } // Build position maps for sort modes s.SortIndex = &DirectoryIndex{ - Entries: entries, + Entries: entries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(entries, SortModeNameAsc), "date_desc": buildPositionMap(entries, SortModeDateDesc), @@ -246,7 +246,7 @@ func TestScroll_PrefetchTriggeredOnPageBoundary(t *testing.T) { } // Build position maps for sort modes s.SortIndex = &DirectoryIndex{ - Entries: entries, + Entries: entries, SortOrders: map[string][]int{ "name_asc": buildPositionMap(entries, SortModeNameAsc), "date_desc": buildPositionMap(entries, SortModeDateDesc), diff --git a/internal/browser/search_test.go b/internal/browser/search_test.go index bd0ce90..6c7053f 100644 --- a/internal/browser/search_test.go +++ b/internal/browser/search_test.go @@ -171,7 +171,7 @@ func TestSearch_JumpToFirst(t *testing.T) { if len(s.SearchResults) == 0 { t.Fatalf("expected results for 'file_3', got 0") } - + // The first result should be index 3 (file_3). // With 12 results, it fits in 20 visible. ScrollIndex should be 0. if s.GetScrollIndex() != 0 { diff --git a/internal/browser/sort.go b/internal/browser/sort.go index ef9e72a..b39f3d1 100644 --- a/internal/browser/sort.go +++ b/internal/browser/sort.go @@ -69,4 +69,3 @@ func comparator(mode SortMode) func(a, b Entry) int { return func(a, b Entry) int { return 0 } } } - diff --git a/internal/browser/types.go b/internal/browser/types.go index bc3ede4..901120e 100644 --- a/internal/browser/types.go +++ b/internal/browser/types.go @@ -19,20 +19,20 @@ type Entry struct { // Page represents a chunk of directory entries loaded from disk. type Page struct { - Index int // Page number (0-based) - Entries []Entry // Entries in this page - Loaded bool // True if page data is in memory - Dirty bool // True if page needs refresh (external change detected) + Index int // Page number (0-based) + Entries []Entry // Entries in this page + Loaded bool // True if page data is in memory + Dirty bool // True if page needs refresh (external change detected) } // BrowserState holds all mutable browser state owned by the logic goroutine. type BrowserState struct { // Navigation - CurrentPath string // Currently browsed directory (relative to root) - History []string // Path history for navigation - ScrollOffset float64 // Vertical scroll offset in pixels (per-pixel scrolling) - EntryHeight float64 // Height of a single entry in pixels - VisibleCount int // Number of entries currently visible + CurrentPath string // Currently browsed directory (relative to root) + History []string // Path history for navigation + ScrollOffset float64 // Vertical scroll offset in pixels (per-pixel scrolling) + EntryHeight float64 // Height of a single entry in pixels + VisibleCount int // Number of entries currently visible // Lazy loading Pages map[int]*Page // Loaded pages by page index @@ -44,8 +44,8 @@ type BrowserState struct { SortMode SortMode // Current sort mode // Search - Query string // Current search query (forwarded from the main-owned search widget) - SearchResults []int // Indices of matching entries (empty = no filter) + Query string // Current search query (forwarded from the main-owned search widget) + SearchResults []int // Indices of matching entries (empty = no filter) // NOTE: the search bar's widget.Editor is intentionally NOT part of this // state: Gio mutates widget state on the main goroutine during draw, and // this state is owned by the logic goroutine (architecture.md §1). The @@ -62,8 +62,8 @@ type BrowserState struct { } const ( - PageSize = 100 // Entries per page (tunable; ~5KB per page in memory) - PrefetchDist = 2 // Pages to prefetch beyond visible region + PageSize = 100 // Entries per page (tunable; ~5KB per page in memory) + PrefetchDist = 2 // Pages to prefetch beyond visible region ) // NewEntry creates a new Entry from the given parameters. @@ -127,10 +127,10 @@ func formatSize(bytes int64) string { // EvictPages removes pages that are far from the current scroll position. func (s *BrowserState) EvictPages() { minPage := s.GetScrollIndex()/PageSize - PrefetchDist - maxPage := (s.GetScrollIndex() + s.VisibleCount)/PageSize + PrefetchDist + maxPage := (s.GetScrollIndex()+s.VisibleCount)/PageSize + PrefetchDist for idx, page := range s.Pages { if idx < minPage || idx > maxPage { - page.Entries = nil // Release memory + page.Entries = nil // Release memory page.Loaded = false } } diff --git a/internal/browser/types_test.go b/internal/browser/types_test.go index e83dd33..85f6d0e 100644 --- a/internal/browser/types_test.go +++ b/internal/browser/types_test.go @@ -103,7 +103,7 @@ func TestFormatSize(t *testing.T) { {0, "0 B"}, {500, "500 B"}, {1024, "1.0 KB"}, - {1024*1024, "1.0 MB"}, + {1024 * 1024, "1.0 MB"}, {1024 * 1024 * 1024, "1.0 GB"}, } @@ -118,7 +118,7 @@ func TestFormatSize(t *testing.T) { func TestBrowserStateEvictPages(t *testing.T) { s := NewBrowserState() s.EntryHeight = 48.0 - s.ScrollOffset = 500 * 48 // Page 5 + s.ScrollOffset = 500 * 48 // Page 5 s.VisibleCount = 10 // With ScrollIndex=500, PageSize=100: @@ -127,10 +127,10 @@ func TestBrowserStateEvictPages(t *testing.T) { // Pages 3-7 should be kept, others evicted // Add pages at various positions - s.Pages[0] = NewPage(0, nil) // Should be evicted (far) - s.Pages[3] = NewPage(3, nil) // Should be kept (at min boundary) - s.Pages[5] = NewPage(5, nil) // Should be kept (visible) - s.Pages[7] = NewPage(7, nil) // Should be kept (at max boundary) + s.Pages[0] = NewPage(0, nil) // Should be evicted (far) + s.Pages[3] = NewPage(3, nil) // Should be kept (at min boundary) + s.Pages[5] = NewPage(5, nil) // Should be kept (visible) + s.Pages[7] = NewPage(7, nil) // Should be kept (at max boundary) s.Pages[10] = NewPage(10, nil) // Should be evicted (far) s.EvictPages() diff --git a/internal/editor/auto_save_test.go b/internal/editor/auto_save_test.go index 3b8aa9b..132d0a4 100644 --- a/internal/editor/auto_save_test.go +++ b/internal/editor/auto_save_test.go @@ -14,10 +14,10 @@ func TestWriteFailureTracking(t *testing.T) { state := NewState() filename := "test.txt" state.Editor.Filename = filename - + // Simulate a write failure state.Editor.SetWriteFailed(filename, true) - + if !state.Editor.WriteFailed() { t.Errorf("Expected WriteFailed() to be true") } @@ -34,7 +34,7 @@ func TestWriteFailureTracking(t *testing.T) { func TestAutoSave_RetryFails(t *testing.T) { // Setup: New Logic, set mock FS to fail l := NewLogic(nil, "/", func(string) {}) - + // Start a goroutine to drain frameChan to prevent deadlocks go func() { for range l.frameChan { @@ -50,16 +50,16 @@ func TestAutoSave_RetryFails(t *testing.T) { if !ok { t.Fatal("mockFS is not a *mock.FileSystem") } - mfs.SetWriteError(true) - + mfs.SetWriteError(true) + // 1. Manually dispatch a write task task := pool.NewWriteFileTask(filename, []byte("hello"), l.mockFS) l.workerPool.Dispatch(task) - + // 2. Manually process the result to trigger the failure state res := <-l.workerPool.ResultChan() l.handleWorkerResult(res) - + // 3. Assert failure if !l.state.Editor.WriteFailed() { t.Errorf("Expected WriteFailed() to be true") diff --git a/internal/editor/buffer_test.go b/internal/editor/buffer_test.go index 5ad0579..91fe659 100644 --- a/internal/editor/buffer_test.go +++ b/internal/editor/buffer_test.go @@ -25,7 +25,7 @@ func TestBackspace(t *testing.T) { state := NewState() TheState = state state.Editor.Buffer = "Hello" - state.Editor.CursorPosition = 5 + state.Editor.CursorPosition = 5 HandleBackspace() // Assuming we create this function diff --git a/internal/editor/e2e_test.go b/internal/editor/e2e_test.go index b1fb5fe..6512575 100644 --- a/internal/editor/e2e_test.go +++ b/internal/editor/e2e_test.go @@ -189,9 +189,9 @@ func TestLargeFileChunkBoundary(t *testing.T) { // (The old fixed-slot model treated each chunk as an independent buffer and // did NOT shift later chunks; that was the bug this design replaces.) expectedStr := string(initialContent) - expectedStr = expectedStr[:65000] + "CHUNK1" + expectedStr[65000:] // insert 1 @65000 - expectedStr = expectedStr[:65535] + "BOUNDARY" + expectedStr[65535:] // insert 2 @65535 - expectedStr = expectedStr[:262000] + "CHUNK3" + expectedStr[262000:] // insert 3 @262000 + expectedStr = expectedStr[:65000] + "CHUNK1" + expectedStr[65000:] // insert 1 @65000 + expectedStr = expectedStr[:65535] + "BOUNDARY" + expectedStr[65535:] // insert 2 @65535 + expectedStr = expectedStr[:262000] + "CHUNK3" + expectedStr[262000:] // insert 3 @262000 expected := []byte(expectedStr) if len(savedContent) != len(expected) { diff --git a/internal/editor/filename_test.go b/internal/editor/filename_test.go index 5bbc67e..78102a9 100644 --- a/internal/editor/filename_test.go +++ b/internal/editor/filename_test.go @@ -1,26 +1,26 @@ package editor import ( - "testing" "pad/internal/ui" + "testing" ) func TestEditorLayout_Filename(t *testing.T) { // Setup: Reset state TheState = NewState() - + // Set a filename expectedFilename := "test.txt" TheState.Editor.Filename = expectedFilename - + // Run layout screenWidth := ui.Dp(400) screenHeight := ui.Dp(800) elements := EditorLayout(screenWidth, screenHeight, false) - + // Find top bar (assuming it's the first container element) topBar := elements[0].(ui.Container) - + // Find the filename label in the top bar (the back icon precedes it). var filenameLabel ui.Label for _, c := range topBar.Children { @@ -37,18 +37,18 @@ func TestEditorLayout_Filename(t *testing.T) { func TestEditorLayout_DefaultFilename(t *testing.T) { // Setup: Reset state TheState = NewState() - + // Filename is empty TheState.Editor.Filename = "" - + // Run layout screenWidth := ui.Dp(400) screenHeight := ui.Dp(800) elements := EditorLayout(screenWidth, screenHeight, false) - + // Find top bar (assuming it's the first container element) topBar := elements[0].(ui.Container) - + // Find the filename label in the top bar (the back icon precedes it). var filenameLabel ui.Label for _, c := range topBar.Children { diff --git a/internal/editor/ime_range_test.go b/internal/editor/ime_range_test.go index a75144a..49fb886 100644 --- a/internal/editor/ime_range_test.go +++ b/internal/editor/ime_range_test.go @@ -40,8 +40,8 @@ func newChunkedState(t *testing.T, content string, chunkSize int) *State { func TestRuneIndexToByteStr(t *testing.T) { cases := []struct { - s string - n int + s string + n int want int }{ {"", 0, 0}, diff --git a/internal/editor/scroll_fix_test.go b/internal/editor/scroll_fix_test.go index 6972fdc..c7427a5 100644 --- a/internal/editor/scroll_fix_test.go +++ b/internal/editor/scroll_fix_test.go @@ -20,45 +20,45 @@ func absFloat(x ui.Dp) float64 { func TestFragmentStartYCalculation(t *testing.T) { // Setup test scenario lineHeight := ui.Dp(16.8) // 14 * 1.2 - + // Test case 1: Scroll to line 0 (top) scrollOffset := ui.Dp(0) expectedVisualLine := 0 expectedFragmentStartY := ui.Dp(0) - + visualLine := int(scrollOffset / lineHeight) fragmentStartY := ui.Dp(visualLine) * lineHeight - + if visualLine != expectedVisualLine { t.Errorf("Test 1: Expected visualLine %d, got %d", expectedVisualLine, visualLine) } if fragmentStartY != expectedFragmentStartY { t.Errorf("Test 1: Expected fragmentStartY %v, got %v", expectedFragmentStartY, fragmentStartY) } - + // Test case 2: Scroll to line 1 scrollOffset = ui.Dp(16.8) expectedVisualLine = 1 expectedFragmentStartY = ui.Dp(16.8) - + visualLine = int(scrollOffset / lineHeight) fragmentStartY = ui.Dp(visualLine) * lineHeight - + if visualLine != expectedVisualLine { t.Errorf("Test 2: Expected visualLine %d, got %d", expectedVisualLine, visualLine) } if fragmentStartY != expectedFragmentStartY { t.Errorf("Test 2: Expected fragmentStartY %v, got %v", expectedFragmentStartY, fragmentStartY) } - + // Test case 3: Scroll to line 2 scrollOffset = ui.Dp(33.6) expectedVisualLine = 2 expectedFragmentStartY = ui.Dp(33.6) - + visualLine = int(scrollOffset / lineHeight) fragmentStartY = ui.Dp(visualLine) * lineHeight - + if visualLine != expectedVisualLine { t.Errorf("Test 3: Expected visualLine %d, got %d", expectedVisualLine, visualLine) } @@ -77,24 +77,24 @@ func TestVisualLineIndexCreation(t *testing.T) { Advance: []ui.Dp{10, 10, 10, 10}, LineHeight: ui.Dp(16.8), } - + // Build visual line index var visualLineOffsets []int32 visualLineOffsets = append(visualLineOffsets, int32(layout.ByteOffsets[0])) - + for i := 1; i < len(layout.Y); i++ { if layout.Y[i] != layout.Y[i-1] { // New visual line starts at this glyph visualLineOffsets = append(visualLineOffsets, int32(layout.ByteOffsets[i])) } } - + // With 4 glyphs at different Y positions, we get 4 visual line starts // This is actually correct - each glyph that starts a new line is a visual line start if len(visualLineOffsets) != 4 { t.Errorf("Expected 4 visual line starts, got %d", len(visualLineOffsets)) } - + // Check byte offsets - should match all the byte offsets where Y changes expectedOffsets := []int32{0, 7, 14, 21} for i := 0; i < len(visualLineOffsets) && i < len(expectedOffsets); i++ { @@ -107,14 +107,14 @@ func TestVisualLineIndexCreation(t *testing.T) { // TestWordWrapFragmentStartY tests fragmentStartY calculation with word wrap enabled func TestWordWrapFragmentStartY(t *testing.T) { lineHeight := ui.Dp(16.8) // 14 * 1.2 - + // Test case: Word wrap enabled, scroll to visual line 5 // With word wrap, visual lines don't correspond 1:1 with logical lines wordWrap := true - _ = wordWrap // Mark as used for this test + _ = wordWrap // Mark as used for this test scrollOffset := ui.Dp(5 * float64(lineHeight)) // Scroll to visual line 5 visualLine := int(scrollOffset / lineHeight) - + // With word wrap, we should use a different estimation // because one logical line can span multiple visual lines if wordWrap { @@ -122,7 +122,7 @@ func TestWordWrapFragmentStartY(t *testing.T) { // to avoid skipping wrapped lines estimatedBytesPerVisualLine := 10 // Very small step expectedStartByteOffset := visualLine * estimatedBytesPerVisualLine - + if expectedStartByteOffset != 50 { // 5 * 10 t.Errorf("Word wrap case: expected start byte offset %d, got %d", 50, expectedStartByteOffset) } @@ -130,12 +130,12 @@ func TestWordWrapFragmentStartY(t *testing.T) { // Without word wrap, use bytes per logical line estimate estimatedBytesPerLogicalLine := 50 expectedStartByteOffset := visualLine * estimatedBytesPerLogicalLine - + if expectedStartByteOffset != 250 { // 5 * 50 t.Errorf("No word wrap case: expected start byte offset %d, got %d", 250, expectedStartByteOffset) } } - + // fragmentStartY should always be visualLine * lineHeight expectedFragmentStartY := ui.Dp(visualLine) * lineHeight if expectedFragmentStartY != ui.Dp(5*16.8) { @@ -150,31 +150,31 @@ func TestByteOffsetAndYFromScroll(t *testing.T) { visualLineIndex := &types.VisualLineIndex{ Offsets: []int32{0, 7, 14, 21}, // 4 lines } - + layout := ui.GlyphLayout{ - ByteOffsets: []int{0, 7, 14, 21}, - X: []ui.Dp{0, 0, 0, 0}, - Y: []ui.Dp{0, 16.8, 33.6, 50.4}, - Advance: []ui.Dp{10, 10, 10, 10}, - LineHeight: lineHeight, + ByteOffsets: []int{0, 7, 14, 21}, + X: []ui.Dp{0, 0, 0, 0}, + Y: []ui.Dp{0, 16.8, 33.6, 50.4}, + Advance: []ui.Dp{10, 10, 10, 10}, + LineHeight: lineHeight, VisualLineIndex: visualLineIndex, } - + // Test scrolling to different positions testCases := []struct { - scrollOffset ui.Dp - expectedByte int - expectedY ui.Dp + scrollOffset ui.Dp + expectedByte int + expectedY ui.Dp }{ - {ui.Dp(0), 0, ui.Dp(0)}, // Top of document - {ui.Dp(16.8), 7, ui.Dp(16.8)}, // Start of line 1 - {ui.Dp(33.6), 14, ui.Dp(33.6)}, // Start of line 2 - {ui.Dp(50.4), 21, ui.Dp(50.4)}, // Start of line 3 + {ui.Dp(0), 0, ui.Dp(0)}, // Top of document + {ui.Dp(16.8), 7, ui.Dp(16.8)}, // Start of line 1 + {ui.Dp(33.6), 14, ui.Dp(33.6)}, // Start of line 2 + {ui.Dp(50.4), 21, ui.Dp(50.4)}, // Start of line 3 } - + for _, tc := range testCases { byteOffset, y := ByteOffsetAndYFromScrollWithLayout(tc.scrollOffset, layout) - + if byteOffset != tc.expectedByte { t.Errorf("Scroll %v: expected byte offset %d, got %d", tc.scrollOffset, tc.expectedByte, byteOffset) } @@ -191,17 +191,17 @@ func ByteOffsetAndYFromScrollWithLayout(scrollOffset ui.Dp, layout ui.GlyphLayou if lineHeight == 0 { lineHeight = editor.EditorLineHeight() } - + // Calculate which visual line should be at the given scroll offset visualLine := int(scrollOffset / lineHeight) - + // If we have a visual line index, use it for accurate byte offset if layout.VisualLineIndex != nil && visualLine < len(layout.VisualLineIndex.Offsets) { byteOffset := int(layout.VisualLineIndex.Offsets[visualLine]) lineTop := ui.Dp(visualLine) * lineHeight return byteOffset, lineTop } - + // Fallback to the original logic using layout.Y values // 1. Find the index of the line whose top is <= scrollOffset idx := sort.Search(len(layout.Y), func(i int) bool { @@ -226,4 +226,4 @@ func ByteOffsetAndYFromScrollWithLayout(scrollOffset ui.Dp, layout ui.GlyphLayou lineTop := layout.Y[lineStartIdx] - lineHeight return layout.ByteOffsets[lineStartIdx], lineTop -} \ No newline at end of file +} diff --git a/internal/editor/wrap_apply_test.go b/internal/editor/wrap_apply_test.go index 1f164f3..7980718 100644 --- a/internal/editor/wrap_apply_test.go +++ b/internal/editor/wrap_apply_test.go @@ -202,4 +202,3 @@ func byteOffsetOfLine(content string, i int) int { } return o } - diff --git a/internal/io/pool/context_test.go b/internal/io/pool/context_test.go index db7702d..b068c33 100644 --- a/internal/io/pool/context_test.go +++ b/internal/io/pool/context_test.go @@ -53,13 +53,13 @@ func (t *cancelTask) Execute() Result { } } -func (t *cancelTask) Priority() Priority { return t.priority } -func (t *cancelTask) TaskID() string { return t.id } -func (t *cancelTask) TaskType() TaskType { return t.taskType } -func (t *cancelTask) DirPath() string { return "/test" } +func (t *cancelTask) Priority() Priority { return t.priority } +func (t *cancelTask) TaskID() string { return t.id } +func (t *cancelTask) TaskType() TaskType { return t.taskType } +func (t *cancelTask) DirPath() string { return "/test" } func (t *cancelTask) Context() context.Context { return t.ctx } -func (t *cancelTask) Cancel() { t.cancel() } -func (t *cancelTask) Timeout() time.Duration { return 0 } +func (t *cancelTask) Cancel() { t.cancel() } +func (t *cancelTask) Timeout() time.Duration { return 0 } // timeoutTask is a task that respects timeout. type timeoutTask struct { @@ -79,13 +79,13 @@ func (t *timeoutTask) Execute() Result { } } -func (t *timeoutTask) Priority() Priority { return t.priority } -func (t *timeoutTask) TaskID() string { return t.id } -func (t *timeoutTask) TaskType() TaskType { return t.taskType } -func (t *timeoutTask) DirPath() string { return "/test" } +func (t *timeoutTask) Priority() Priority { return t.priority } +func (t *timeoutTask) TaskID() string { return t.id } +func (t *timeoutTask) TaskType() TaskType { return t.taskType } +func (t *timeoutTask) DirPath() string { return "/test" } func (t *timeoutTask) Context() context.Context { return context.Background() } -func (t *timeoutTask) Cancel() {} -func (t *timeoutTask) Timeout() time.Duration { return 50 * time.Millisecond } +func (t *timeoutTask) Cancel() {} +func (t *timeoutTask) Timeout() time.Duration { return 50 * time.Millisecond } func TestTaskContextCancellation(t *testing.T) { task := newCancelTask("test-cancel", 1*time.Second) diff --git a/internal/io/pool/mock/filesystem.go b/internal/io/pool/mock/filesystem.go index 43311c0..42d71b0 100644 --- a/internal/io/pool/mock/filesystem.go +++ b/internal/io/pool/mock/filesystem.go @@ -68,10 +68,10 @@ type notifyEvent struct { // FileSystem is a thread-safe in-memory filesystem for testing. type FileSystem struct { mu sync.RWMutex - files map[string]*File // path -> file - delay time.Duration // uniform delay applied to all operations - changes chan FileChangeEvent // async change notifications (nil = no notifications) - writeError bool // Added to simulate write errors + files map[string]*File // path -> file + delay time.Duration // uniform delay applied to all operations + changes chan FileChangeEvent // async change notifications (nil = no notifications) + writeError bool // Added to simulate write errors } // NewFileSystem creates an empty mock filesystem. diff --git a/internal/io/pool/task.go b/internal/io/pool/task.go index 6e61a0c..9108dc7 100644 --- a/internal/io/pool/task.go +++ b/internal/io/pool/task.go @@ -115,8 +115,6 @@ func (t TaskType) String() string { } } - - // --- Specific Task Implementations --- // ReadChunkTask reads a specific chunk of a file. @@ -163,13 +161,17 @@ func (t *ReadChunkTask) Execute() Result { } } -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) 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() } } +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. @@ -210,13 +212,17 @@ func (t *StatFileTask) Execute() Result { } } -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) 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() } } +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 { @@ -267,13 +273,17 @@ func (t *BuildLineIndexTask) Execute() Result { } } -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) 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() } } +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 { @@ -304,13 +314,17 @@ func (t *BuildIndexTask) Execute() Result { 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) 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() } } +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 { @@ -343,13 +357,17 @@ func (t *LoadPagesTask) Execute() Result { 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) 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() } } +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 { @@ -381,13 +399,17 @@ func (t *ReadDirTask) Execute() Result { 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) 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() } } +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 { @@ -421,13 +443,17 @@ func (t *SaveStateTask) Execute() Result { 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) 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() } } +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 { @@ -461,13 +487,17 @@ func (t *SaveUndoTask) Execute() Result { 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) 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() } } +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. @@ -499,13 +529,17 @@ func (t *ReadFileTask) Execute() Result { 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) 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() } } +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 @@ -587,13 +621,17 @@ func toLowerFold(s string) string { // 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() } } +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 { @@ -626,13 +664,17 @@ func (t *WriteFileTask) Execute() Result { 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) 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() } } +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. @@ -709,12 +751,12 @@ type mockFileInfo struct { 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 } +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 { @@ -722,8 +764,8 @@ type mockDirEntry struct { isDir bool } -func (e *mockDirEntry) Name() string { return e.name } -func (e *mockDirEntry) IsDir() bool { return e.isDir } +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 } @@ -739,6 +781,3 @@ func (m *MockFS) ReadDir(path string) ([]types.DirEntry, error) { } var _ FileSystem = (*MockFS)(nil) // Compile-time interface check - - - diff --git a/internal/io/pool/types/types.go b/internal/io/pool/types/types.go index a722db3..0da3705 100644 --- a/internal/io/pool/types/types.go +++ b/internal/io/pool/types/types.go @@ -68,25 +68,25 @@ func (li *LineIndex) FindLogicalLineForByteOffset(byteOffset int) int { if len(li.Offsets) == 0 { return -1 } - + // Binary search to find the line that contains this byte offset low, high := 0, len(li.Offsets)-1 for low <= high { mid := (low + high) / 2 - midOffset := int(li.Offsets[mid]) - - if byteOffset >= midOffset { - // This line starts at or before our byte offset - // Check if the next line starts after our byte offset - if mid == len(li.Offsets)-1 || int(li.Offsets[mid+1]) > byteOffset { - return mid // Found the line - } - low = mid + 1 - } else { - high = mid - 1 + midOffset := int(li.Offsets[mid]) + + if byteOffset >= midOffset { + // This line starts at or before our byte offset + // Check if the next line starts after our byte offset + if mid == len(li.Offsets)-1 || int(li.Offsets[mid+1]) > byteOffset { + return mid // Found the line } + low = mid + 1 + } else { + high = mid - 1 + } } - + return -1 // Not found } diff --git a/internal/io/pool/worker_pool.go b/internal/io/pool/worker_pool.go index c280ea6..b5ba98b 100644 --- a/internal/io/pool/worker_pool.go +++ b/internal/io/pool/worker_pool.go @@ -13,15 +13,15 @@ import ( // - Workers execute tasks and post results on resultChan // - Workers never access state directly type WorkerPool struct { - highWorkChan chan Task - lowWorkChan chan Task - resultChan chan Result - workerCount int - stopOnce sync.Once - stopChan chan struct{} - wg sync.WaitGroup - taskCounter atomic.Int64 - running atomic.Bool + highWorkChan chan Task + lowWorkChan chan Task + resultChan chan Result + workerCount int + stopOnce sync.Once + stopChan chan struct{} + wg sync.WaitGroup + taskCounter atomic.Int64 + running atomic.Bool // pendingTasks tracks in-flight tasks for cancellation by directory. pendingTasks map[string][]Task diff --git a/internal/io/pool/worker_pool_test.go b/internal/io/pool/worker_pool_test.go index 14a7d56..4b821d0 100644 --- a/internal/io/pool/worker_pool_test.go +++ b/internal/io/pool/worker_pool_test.go @@ -42,13 +42,13 @@ func (t *stubTask) Execute() Result { } } -func (t *stubTask) Priority() Priority { return t.priority } -func (t *stubTask) TaskID() string { return t.id } -func (t *stubTask) TaskType() TaskType { return t.taskType } -func (t *stubTask) DirPath() string { return "" } +func (t *stubTask) Priority() Priority { return t.priority } +func (t *stubTask) TaskID() string { return t.id } +func (t *stubTask) TaskType() TaskType { return t.taskType } +func (t *stubTask) DirPath() string { return "" } func (t *stubTask) Context() context.Context { return context.Background() } -func (t *stubTask) Cancel() {} -func (t *stubTask) Timeout() time.Duration { return 5 * time.Second } +func (t *stubTask) Cancel() {} +func (t *stubTask) Timeout() time.Duration { return 5 * time.Second } func TestWorkerPool_StartStop(t *testing.T) { pool := NewWorkerPool(4) @@ -672,9 +672,9 @@ func TestWorkerPool_ResultIsSuccess(t *testing.T) { } result2 := Result{ - TaskID: "test", + TaskID: "test", Success: false, - Error: fmt.Errorf("error"), + Error: fmt.Errorf("error"), } if result2.IsSuccess() { t.Error("Result should not be successful") @@ -683,7 +683,7 @@ func TestWorkerPool_ResultIsSuccess(t *testing.T) { func TestWorkerPool_ResultIsError(t *testing.T) { result := Result{ - TaskID: "test", + TaskID: "test", Success: false, Error: fmt.Errorf("error"), } diff --git a/internal/test/e2e/harness_test.go b/internal/test/e2e/harness_test.go index f6a6b73..7c11ea0 100644 --- a/internal/test/e2e/harness_test.go +++ b/internal/test/e2e/harness_test.go @@ -20,7 +20,7 @@ func TestEditorInitialLayout(t *testing.T) { t.Fatalf("GoToEditor: %v", err) } h.SendConfig(780, 1688) - + // Wait for a new frame after switching to editor page _, err := e2e.WaitForNewFrame(h, h.FrameCount(), 5*time.Second) if err != nil { diff --git a/internal/test/e2e/helpers.go b/internal/test/e2e/helpers.go index f14d45a..26c955d 100644 --- a/internal/test/e2e/helpers.go +++ b/internal/test/e2e/helpers.go @@ -14,7 +14,7 @@ type TestScenario struct { Setup func(*Harness) Actions []HarnessAction Assertions []FrameAssertion - FrameIndex int // which frame to assert on (-1 for last) + FrameIndex int // which frame to assert on (-1 for last) Timeout time.Duration } diff --git a/internal/test/e2e/restore_test.go b/internal/test/e2e/restore_test.go index 5aaff8d..3b0b7f6 100644 --- a/internal/test/e2e/restore_test.go +++ b/internal/test/e2e/restore_test.go @@ -646,7 +646,7 @@ func TestRestore_LinePinHoldsAcrossLateWrapFeedback(t *testing.T) { File: "/wrap.txt", Cursor: 0, Scroll: float64(ui.Dp(500 * lh)), // the saving session's visual-space offset - ScrollLine: 200, // the logical line at the viewport top + ScrollLine: 200, // the logical line at the viewport top ScrollSub: 0, } diff --git a/internal/test/e2e/search_test.go b/internal/test/e2e/search_test.go index c365043..a0ebe86 100644 --- a/internal/test/e2e/search_test.go +++ b/internal/test/e2e/search_test.go @@ -248,7 +248,7 @@ func TestSortModeChangePreservesSearchFilter(t *testing.T) { // When the sort mode changes, the position map changes, // so the *index* of the entry "Documents" (which matches "doc") // will change! - + // Let's print the entries to see if they are still correct, // ignoring the index values themselves. // (Owner-side snapshot: all reads happen on the logic goroutine.) diff --git a/internal/ui/layout.go b/internal/ui/layout.go index d141381..43dc854 100644 --- a/internal/ui/layout.go +++ b/internal/ui/layout.go @@ -2,14 +2,14 @@ package ui const ( // Standard dimensions in Dp - StatusBarLineHeight = Dp(24) + StatusBarLineHeight = Dp(24) StatusBarFilenameLine = Dp(24) - StatusBarIconsLine = Dp(24) - BottomBarHeight = Dp(24) - IconSize = Dp(24) - IconGap = Dp(36) - Padding = Dp(8) - ButtonPadding = Dp(8) + StatusBarIconsLine = Dp(24) + BottomBarHeight = Dp(24) + IconSize = Dp(24) + IconGap = Dp(36) + Padding = Dp(8) + ButtonPadding = Dp(8) ) // LineHeightScale is the baseline-to-baseline spacing multiplier for editor text.