- Deleted (drift-prone point-in-time plans / dead specs): browser_implementation_plan.md, editor_implementation_plan.md, virtual_scroll_render_optimization.md, conflict_resolution.md, touch.md, element_model.md, layout_rendering.md, bugs.txt - Rewrote architecture.md: single-owner/no-lock model, channel topology, Frame handoff contract, ownership rules, editor/browser/render internals, active vs dead task types, testing hooks. - Rewrote spec.md: actual behavior (browser, editor, IME, autosave, 50 MB limit with measured numbers), actual code layout, invariants to preserve, explicit deferred-features table (undo, restoration, sync awareness). - Added doc/README.md: doc index, documentation policy, build/install recipe, on-device observation loop. - Updated development_plan.md: Phase 5 mostly done; §7 spec deltas written with two corrections (no undo at all; no external-change detection).
158 lines
7.8 KiB
Markdown
158 lines
7.8 KiB
Markdown
# Pad — Text Editor Specification
|
||
|
||
This is the specification of what Pad **actually is** (v1, current code).
|
||
Behavior that was once spec'd but never built is listed as deferred in §7, not
|
||
described as a requirement. If code and this document disagree, the code wins
|
||
— and this document should be fixed.
|
||
|
||
## 1. Overview
|
||
|
||
Pad is a minimal plain-text editor for Android, written in Go with the Gio UI
|
||
framework. It presents a single root directory of text files (a browser) and a
|
||
full-screen editor with the Android soft keyboard. The design goal is a
|
||
fast, honest editor for a directory that is synced across devices — instant
|
||
open, autosave on every edit, and large files that stay smooth.
|
||
|
||
**Design philosophy:**
|
||
|
||
- **Plain text only** — no formatting, no syntax highlighting.
|
||
- **Automatic everything** — autosave with a 1 s debounce; there is no save
|
||
button.
|
||
- **Minimal surface area** — a browser and an editor, nothing else.
|
||
- **Bounded memory** — a measured file-size limit with an explicit
|
||
"too large" state, instead of silently degrading or OOMing.
|
||
|
||
**Platform:** Android-first. Window `390×844` dp. Default root directory is
|
||
`/storage/emulated/0/Notes` on Android (overridable with `-root`); `.`
|
||
elsewhere.
|
||
|
||
## 2. Implemented behavior
|
||
|
||
### 2.1 File browser
|
||
|
||
- Lists files and directories under the current directory.
|
||
- **Sort:** four modes — name asc/desc, date asc/desc — cycled by the sort
|
||
button. Default: date descending (newest first).
|
||
- **Search:** incremental, case-insensitive filter over entry names via the
|
||
search bar (a Gio `widget.Editor`).
|
||
- **Pagination:** directory entries are loaded asynchronously in pages through
|
||
the worker pool; the first paint does not wait for a full directory read.
|
||
- **Navigation:** tap a directory to enter it, back to return. Single tap
|
||
opens a file — no long-press menus, no file management actions.
|
||
|
||
### 2.2 Editor
|
||
|
||
- **Input via the Android IME:** soft-keyboard typing, composition,
|
||
backspace, and autocorrect replacements all arrive through one IME
|
||
replace-range path and are converted from rune indices to byte offsets.
|
||
(Swipe-typing support follows from the same IME path; final sign-off on a
|
||
physical device is the one open validation item.)
|
||
- **Word wrap:** on by default; wrapped lines are virtual — the buffer stores
|
||
only real newlines.
|
||
- **Virtualized viewport:** only the visible byte range is shaped and drawn
|
||
each frame (typically ~4 KB of a large file), keeping frame cost and shaper
|
||
memory constant regardless of file size.
|
||
- **Cursor + scroll:** tap to place the cursor, drag/scroll to pan, Home/End
|
||
and Page Up/Down on hardware keyboards, arrow keys.
|
||
- **Autosave:** every edit restarts a 1 s debounce; on expiry the full content
|
||
is written to disk by a worker. Failed writes are retried. This is the only
|
||
persistence mechanism.
|
||
|
||
### 2.3 Large files (measured)
|
||
|
||
- Editable limit: **50 MB** (`MaxEditableFileSize`), measured on-device:
|
||
a 10 MB file (130,954 lines) opens in ~120 ms (stat ~76 ms, read ~27 ms,
|
||
line-index ~18 ms) and idles at **~150 MB PSS / ~230 MB RSS, flat under
|
||
scroll**. 50 MB extrapolates to a few hundred MB — acceptable on a modern
|
||
phone.
|
||
- Files above the limit open into a **"too large to edit"** state: a notice is
|
||
shown, edit operations are no-ops, and the browser still lists the file.
|
||
- The buffer is chunked (64 KB chunks, prefix-sum offsets, full load for
|
||
in-range files); edits splice only affected chunks. Details:
|
||
`architecture.md` §6.
|
||
|
||
## 3. Code organization (actual)
|
||
|
||
```
|
||
cmd/pad/ # Gio entry point, main loop, frameReceiver,
|
||
# Android defaults (impl_android.go)
|
||
internal/
|
||
browser/ # BrowserState, BrowserManager, sort, search,
|
||
# pagination, layout, handlers
|
||
editor/ # Logic goroutine, State, ChunkedBuffer, LineIndex,
|
||
# IME handling, autosave, Frame handoff (frame.go)
|
||
io/pool/ # Worker pool (priority lanes), task types,
|
||
# real/ — real filesystem (rooted at /)
|
||
# mock/ — in-memory FS for tests
|
||
# types/ — shared types (LineIndex, ...)
|
||
ui/ # Element model, Renderer, units (Dp/Px), theme
|
||
ui/icons/ # Vector icons
|
||
test/e2e/ # Logic-level e2e harness + tests
|
||
doc/ # spec.md (this file), architecture.md,
|
||
# development_plan.md
|
||
```
|
||
|
||
## 4. Architecture (summary)
|
||
|
||
The logic goroutine is the **sole owner** of mutable state; the main (Gio)
|
||
goroutine reads only a `Frame` snapshot and writes only via channels; a
|
||
frame-receiver goroutine stores frames and invalidates the window; a
|
||
priority-aware worker pool does all file I/O. No mutex guards application
|
||
state. The IME snippet, search box, and gesture state live on the
|
||
main-goroutine side because Gio mutates them during draw.
|
||
|
||
Full details, channel topology, and ownership rules: [`architecture.md`](./architecture.md).
|
||
|
||
## 5. Invariants to preserve (future development must not break these)
|
||
|
||
1. **Single owner, no locks on state** (architecture.md §1). Any new feature
|
||
adds a channel or an owner-executed callback — never a direct state touch
|
||
from another goroutine.
|
||
2. **Main never reads logic `State`** — only the `Frame` snapshot.
|
||
3. **Never shape the whole file.** The text shaper's internal line storage
|
||
retains the largest layout ever performed; one whole-file layout permanently
|
||
inflates memory (this caused a 1 GB leak, fixed in Phase 3).
|
||
4. **The IME snippet is the visible window**, and rune↔byte conversion happens
|
||
in one place (`HandleReplaceRange` / `RuneIndexToByte`).
|
||
5. **Autosave is the only persistence.** If state persistence (last file,
|
||
cursor, scroll) is added later, it must go through the same
|
||
owner-dispatches-a-task pattern.
|
||
6. **Logic work stays < 16 ms.** Anything that can block or scan more than the
|
||
viewport goes to the worker pool.
|
||
|
||
## 6. Performance expectations (validated on-device)
|
||
|
||
| Operation | Target | Measured (10 MB file, Android 35 emulator) |
|
||
|---|---|---|
|
||
| Open file | instant feel | ~120 ms (stat + read + line index) |
|
||
| Scroll | 60 fps | flat ~240 MB RSS, no growth, no OOM |
|
||
| Type | responsive | single IME path; rapid commits land cleanly |
|
||
| Memory | bounded | ~150 MB PSS / ~230 MB RSS at 10 MB file, flat |
|
||
|
||
## 7. Deferred / not implemented (explicit non-goals for v1)
|
||
|
||
These were in the original v1 spec but are **not in the code**. They are
|
||
recorded here so future rounds don't mistake doc text for behavior:
|
||
|
||
| Feature | Status | Notes |
|
||
|---|---|---|
|
||
| Undo (any) | **not implemented** | No undo stack exists; the old `SaveUndoTask` is dead code. |
|
||
| State restoration on relaunch | **not implemented** | Last file / cursor / scroll are not persisted. |
|
||
| External change detection | **not implemented** | No mtime compare on open/resume, no watcher, no Keep/Reload prompt. |
|
||
| Syncthing conflict handling | **not implemented** | No `.sync-conflict-*` file detection or merging. |
|
||
| File-system watcher | **not implemented** | Browser does not live-refresh; it re-scans on navigation. |
|
||
| Alphabetical index sidebar | **not implemented** | `AlphaIndex` element exists but is unused. |
|
||
| In-file search, tabs, split view | **not implemented** | — |
|
||
| Files > 50 MB | **not supported** | `TooLarge` state instead. |
|
||
| Desktop / other platforms | **not supported** | Android-first. |
|
||
|
||
## 8. Product requirements (standing)
|
||
|
||
| Requirement | Detail |
|
||
|---|---|
|
||
| Instant open | Files open without visible lag; content and line index load async. |
|
||
| Auto-save | Every edit persisted with a 1 s debounce; no save button. |
|
||
| External change awareness | Deferred (§7) — the sync-awareness story is explicitly out of v1. |
|
||
| Plain text only | No formatting, no highlighting, no file management UI. |
|
||
| Bounded memory | 50 MB editable limit with an explicit "too large" state. |
|