Commit Graph

12 Commits

Author SHA1 Message Date
d8b5bc704b Handle drags: 1:1 finger tracking with cross-flip; fix caret/taps on empty lines
Selection handles now track the finger 1:1 (anchor grab point + displacement)
instead of snapping by whole lines, and crossing the opposite handle flips
the selection (native behaviour) instead of clearing it.

Caret and tap/handle line resolution use VisualLineStarts instead of the
min-Y baseline: the window's first visual line may be an empty line with no
recorded glyphs, which used to draw boundary carets one line too low per
leading empty line and land taps/dragged handles one line below the finger.
New exported ui.CaretPoint centralises byte->insertion-point mapping.

The off-screen caret no longer clamps to the window edge: EditorLayout ships
the true (possibly negative / past-end) window-relative cursor and the
renderer skips the caret when the cursor is outside the shaped window, so
scrolling past the caret no longer makes it jump onto the top/bottom line.

IME/router replay fixes: key.FocusCmd is issued only on a focus transition
(a per-frame no-op still takes the immediate-command path and re-queues all
pointer events), and the key.SelectionCmd IME sync is deferred while a
handle drag is in progress (each push re-injected the drag into every
gesture). Handle drags forward only Grabbed events; a tap inside a handle
grab box is a no-op.

Also: key.FocusEvent no longer logs as unexpected in main; dead code removed
(worker taskWrapper, browser applyXxxResult stubs, scrollIndex, mock_setup
sortModeKey/lineSpan helpers); mock FileSystem.ListPaths prefix match uses
strings.HasPrefix; build scripts run the new scripts/check.sh static gate
(go vet + staticcheck). Tests: caret_point_test, touch_selection updates
(flip/empty-line cases), off-window caret e2e, selection drag e2e grab step.
2026-08-19 22:34:12 -04:00
6c6a0c1a27 Fix write-concurrency race: per-file write protocol + unique temps
The review-identified race: saves are async (owner snapshots content,
worker pool writes), nothing serialized per file, and every write of a
file used the SAME deterministic temp ('.<name>.tmp'). Two overlapping
writes (autosave x autosave, retry x autosave, or the synchronous
FlushAll on GoToBrowser/Shutdown x a worker write) interleaved on the
shared temp and could rename a byte-mixture into place; even without
interleaving, last-rename-wins could promote a STALE snapshot.

Owner-side protocol (logic.go, requestSave + result handler):
- at most one write in flight per file (writeInFlight maps filename ->
  the file version whose content the in-flight write carries);
- a save requested while one is in flight is deferred (savePending) and
  re-issued by the write's result handler with a FRESH snapshot, so
  'last rename wins' coincides with 'newest snapshot wins';
- on success the SNAPSHOT version (not the current one) is recorded as
  written, so an edit that arrived during the write leaves the file
  dirty and triggers the re-issue;
- FlushAll (GoToBrowser, Shutdown) defers via the same protocol instead
  of writing concurrently on the shared temp;
- shutdown drain: on done the owner waits (bounded 5 s) for in-flight
  writes and armed retries to settle before exiting, so the post-exit
  synchronous FlushAll and workerPool.Stop cannot race a straggling
  worker write;
- retry timer now sends a non-blocking token (no timer-goroutine stall
  on a full channel); emitFrame no longer blocks on a slow/gone main
  (frames are snapshots; the next emission wins) - also required so the
  drain can never deadlock on frame delivery.

Mechanism (real/filesystem.go):
- WriteFileAtomic uses a unique per-call temp ('.<name>.tmp.<pid>.<seq>'),
  making same-file staging-file interleaving structurally impossible even
  if the serialization regressed (defense in depth);
- each successful write best-effort removes stale temps of the same file
  (crash leftovers, plus the legacy deterministic name for upgraded
  installs); a failed write removes its own temp.

Tests:
- write_serialization_test.go (e2e): a counting FS wrapper proves the
  peak concurrent same-file saves is 1 across two deliberately
  overlapping autosaves (2 s saves; the second edit lands inside the
  first save's window and its token is deferred, then re-issued with the
  newer content), and that a Flush during an in-flight save adds no
  concurrent writer and the newest snapshot still wins. Mutation-verified:
  disabling the deferral fails it with peak = 2. (The pool's WriteFileTask
  calls FS.WriteFile, not WriteFileAtomic - the real FS is atomic only
  because WriteFile delegates to WriteFileAtomic; the wrapper mirrors that
  delegation or the overlap window does not exist.)
- filesystem_test.go: stale-temp test updated to the new pattern, also
  covering the legacy name and asserting a different file's temp is
  untouched.
- real_file_fuzz_test.go: stray-temp check matches both patterns.

Docs: architecture.md 6.5 rewritten (protocol invariants), spec.md
autosave line, development_plan.md v11 + Phase 12.

On-device smoke: open, type, autosave lands exact content on disk, no
temp files left, clean relaunch. Full suite green under -race.

Residuals (documented): no fsync before rename (power-loss window only);
external-change detection absent; a drain-deadline exit with a straggling
write can only lose freshness (unique temps keep every rename a complete
snapshot).
2026-08-17 16:29:47 -04:00
5205906de4 Data-corruption test suite: differential fuzz + atomicity contract
Adds the corruption-proofing layer for the edit/persist path:

- chunked_buffer_fuzz_test.go: differential fuzz of ChunkedBuffer
  Insert/Delete against a plain []byte shadow model (arbitrary byte
  positions/text, chunk sizes 1..64KB, 500-2000 ops each) verifying
  FileLen, FullContent, Content probes and the chunk-size invariant
  after every op; rune-aligned variant adds UTF-8 validity and an
  independent RuneIndexToByte oracle.
- line_index_fuzz_test.go: differential fuzz of the incremental
  LineIndex updates against a full-recomputation oracle (mixed,
  no-newline, single-line, CRLF, trailing-newline shapes), plus a
  trailing-empty-line structural invariant test.
- state_api_fuzz_test.go: differential fuzz of the production edit
  entry points (HandleInsert/Backspace/Delete/ReplaceRange, incl.
  selection variants) checking content, UTF-8 validity, chunk
  invariant, line index and exact cursor after every op.
- real_file_fuzz_test.go (e2e): random edit sequences (incl.
  window-relative IME replaces) against the REAL filesystem with
  per-batch IME-window-consistency checks, forced-flush disk
  byte-comparison, then a second logic instance (restart simulation)
  must reload byte-identical content with a fresh line index;
  asserts no stray temp files.
- filesystem_test.go (real): atomicity contract - exact round trip
  at edge sizes, concurrent reader never sees a torn file across 150
  alternating 2MB writes, failed write (read-only dir) leaves the
  original byte-identical, stale temp file is consumed.

Fixes a real invariant violation the fuzzing exposed: Insert halved
an oversized spliced result once, so a large paste into a non-empty
buffer left chunks up to ~P/2 (8x target at 1MB/64KB), breaking the
documented 'no chunk > 2x target' invariant. Insert now re-chunks the
oversized result into pieces of at most chunkSize, making the
invariant hold after every edit. Mutation-tested: dropping one byte
in Insert and one entry in UpdateLineIndexAfterInsert are both
caught by the fuzz suite.
2026-08-17 12:43:17 -04:00
58725a5e6c Make state single-owner and stabilize race detector
Frame now carries view state (scale/focus/query) to the main goroutine,
which reads only the frame-receiver-stored snapshot. Renderer owns Gio
widget editors (registered by ID) and the draw scale. Autosave timer
sends a token to the logic goroutine instead of touching state;
Shutdown waits for the owner to exit before FlushAll.

Tests access state only through owner-side Inspect/WithState helpers
(harness + in-package). Fixed double-Harness.Run race in two e2e tests,
made TestWorkerPool_PriorityPreemption deterministic, and raised the
lazy-loading test timeout that was too short under -race.

go test -race ./... is now green.
2026-08-16 01:32:27 -04:00
7240b62a61 WIP baseline: Termux open-file bridge + word-wrap-aware viewport/scroll
- JNI: open_file_in_termux via ACTION_SEND intent (text/plain + file:// uri),
  global context ref kept from registerFragment
- impl_android.go: OpenFile(path) attaches current thread if needed
- NewLogic takes openfunc; State.open + ui.OpenFile now func(string)
- ChunkedBuffer.VisibleByteRange: word-wrap path using
  GlyphLayout.VisualLineStarts (+byteOffset, lineHeight, visual index param)
- GlyphLayout gains LineHeight; drawWrappedText records VisualLineStarts
- WordWrap default true; State.ByteOffset tracks first visible line
- types: VisualLineIndex
- scroll_fix_test.go (new)
- debug prints left in place (WIP; cleanup in later phase)
2026-08-16 00:31:26 -04:00
4052769813 Fix to chunk loading policy. 2026-06-05 18:06:17 -04:00
1324772215 Refactor RealFileSystem to use WorkingDir, and ensure clean shutdown with FlushAll 2026-06-05 11:42:39 -04:00
615957196e feat: virtual scrolling with chunked buffer for large files
- 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
2026-06-05 09:11:32 -04:00
93a5f879f5 feat: implement atomic writes for filesystem backend
- Add WriteFile method to mock filesystem (non-atomic path, creates files)
- Implement WriteFileAtomic in mock with temp file + rename pattern
  using .tmp/ directory, matching real filesystem semantics
- Update WriteFileTask.Execute() to use WriteFileAtomic
- Update FlushAll() to use WriteFileAtomic
- Fix mock to create files on write (matching os.WriteFile behavior)
- Update tests to match new semantics (create-if-not-exists)
2026-06-04 18:01:51 -04:00
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
ca828569f7 Fix deadlock when opening files and update E2E tests 2026-06-04 13:06:30 -04:00
6a43e3c0db Add e2e test harness for testing editor logic without Gio display
- internal/test/e2e/: FrameCapture, Harness, ElementAssertions, helpers
- internal/editor/logic.go: Add done channel and Done() method for graceful shutdown
- internal/editor/mock_setup.go: Mock filesystem for tests
- internal/browser/: Browser layout and search logic
- internal/io/: Worker pool for async tasks
- Update architecture docs and spec
2026-05-31 08:56:30 -04:00