Skip to content

Source Edit: fix the Markdown in place (v1.12.0) - #35

Merged
b451c merged 31 commits into
mainfrom
feature/source-edit
Oct 5, 2026
Merged

b451c merged 31 commits into
mainfrom
feature/source-edit

Conversation

@b451c

@b451c b451c commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Source Edit: fix the Markdown you are reading without leaving the window (answers #31 and #34).

What it adds

  • Option+Cmd+E, the pencil button or File > Edit Source switch the window to the raw Markdown at the reading line (a selected word is already selected in the source). Cmd+S saves, Esc returns to the rendered view at the edited place.
  • Byte-faithful, in-place saves: encoding, BOM, line endings, permissions, extended attributes and symlinks are kept. Read-only files and files over 2 MB are not edited.
  • Unsaved text is never lost silently: close button, tab close, Cmd+W and quit all ask (Save / Don't Save / Cancel).
  • External changes while editing: silent adopt when the buffer is clean, a banner with both choices when it is not, and a conflict prompt at save time.
  • Editor: own undo stack, system find and replace, indentation helpers, block-level tint from the app's own parser.

How it is built

  • The app stays a DocumentGroup(viewing:); the session writes the file itself and an unsaved-changes guard sits in front of the window delegate.
  • 331 new unit tests (688 in total). Debug, Release and Release+APPSTORE build locally; this PR is here so CI checks the older SDK before the merge.

Generated with Claude Code

b451c and others added 30 commits October 5, 2026 13:59
DocumentFileFormat detects encoding, BOM and line-ending style from the raw
bytes and re-encodes LF text so an unchanged document saves byte for byte;
DocumentFileWriter overwrites an existing regular file in place (keeps inode,
permissions, xattrs, creation date, links; refuses read-only and non-regular
files) with read-back verification and a best-effort restore.

MarkdownDocument.decode now strips the BOM on the bytes, so the text never
carries U+FEFF regardless of the Foundation version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…missing file returns

SourceEditSupport: line/offset mapping on UTF-16, block lookup for a source
line, literal selection lookup, and newline / indent / outdent edits returned
as single undoable replacements (NSString core, one pass on large documents).

FileWatcher: after the watched file disappears, poll for the path once a
second and re-arm when it is back (git checkout, stash pop); a permission
error stops the poll instead of retrying forever.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ce edits

EditCloseGuard is a forwarding proxy in front of SwiftUI's window delegate:
a dirty session turns every close path (red button, tab close, Cmd-W, quit)
into a Save / Don't Save / Cancel sheet, with a synchronous last-resort prompt
if a window closes without consulting the delegate. It keeps the underlying
NSDocument clean on purpose, so it disables automatic termination itself
while text is unsaved. No session installs a guard yet, so behaviour is
unchanged.

The app's Close command and the missing-file banner now go through
EditCloseGuard.requestClose (plain close() for clean windows, as before).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n geometry)

SourceEditorController builds a TextKit 1 plain-text editor in the rendered
document's column, with its own UndoManager (never the window's, which
belongs to SwiftUI's autosaving NSDocument), CR-free buffer, indentation
edits from SourceEditSupport as single undo steps, the system find bar, and
caret / scroll-to-line API for the edit session. Not mounted yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er on the view, bounded relayout restore

Paste normalization is its own undo step; SourceTextView returns the
editor's UndoManager even without its delegate; restored text takes the
current style; the exact top-line restore after a relayout is skipped past
400k UTF-16 units (keeps the estimate instead of a multi-hundred-ms layout);
focus requests wait for a window; Shift/Option-Return insert a plain newline.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e confirmation

SourceEditSession owns one window's edit flows: fresh read with entry checks
(writable, size, byte-exact round trip), in-place save in the file's own
encoding and line endings with a conflict check, external-change handling
(silent adopt when clean, banner when dirty), leave / discard, and the
EditCloseGuard handlers. Prompts are injectable; the rendered text always
equals what is on disk. SourceEditorController gains reload(_:).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Session: Save Anyway keeps the base until the write succeeded and re-checks
the disk after the prompt; Keep My Version adopts the disk's format; one
prompt chain at a time; literal (UTF-16) text comparison; lossless-decode
entry check that also covers mixed line endings; banner-aware save and
discard; delayed adopt of a momentarily empty file.

Editor: base attributes are added, not set (marked text keeps its
attributes); CR paste is named Paste; multi-range replacements are
normalized; exact restore also bounded by lines; reload uses the relayout
primitives.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… with the same bytes

The session records the last text it committed to the rendered view and
re-commits the saved text whenever the disk is back at the base and the two
differ. Busy banner/menu actions beep; the empty-file recheck delay is
injectable for tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Edit Source / Done Editing (Option-Cmd-E), Save (Cmd-S) and Discard Changes in
the File menu via focused values; the editor mounted over the rendered list;
Save / Done pills while editing, the pencil pill starts Source Edit and the
external editor gets its own pill; changed-on-disk banner; Cmd-F opens the
editor's find bar while editing; leaving lands the rendered view on the block
that holds the caret, resolved against the parse of the saved text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the editor

Escape leaves edit mode (or closes the find bar) whatever has focus; Don't
Save no longer scrolls the rendered view to a line of the discarded buffer;
Cmd-G is matched exactly; the session follows a changed document URL; the
covered list leaves the accessibility tree (AppKit level); a selection in the
rendered view is selected in the source when it occurs there literally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… lazy, bounded hint

The hint carries the rendered text around the selection and the source match
with the best-agreeing context wins; the hint is built only after the entry
checks and only for small selections. The file watcher follows a changed
document URL. Landing tests no longer import a module the test target does
not have (clean builds failed).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he parser

Headings, fenced / verbatim blocks and blockquotes are coloured with
temporary layout-manager attributes computed from MarkdownBlockParser's
blocks (no second grammar, no storage or undo changes). Parsing runs off the
main thread on a snapshot, debounced 200 ms, stale results dropped; buffers
above 100k UTF-16 units stay untinted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…wait while typing

The re-tint is scheduled where the generation is bumped (storage delegate),
so undo / redo and any other edit path re-colour; continuous typing is
re-tinted at most 1 s after the first unparsed edit. Tests exercise the
stale-result check, the size-limit boundary and typing next to tinted runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The external editor stays on Cmd-E and in the File menu; two edit buttons
side by side were one too many.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e map

The completed-roadmap checklist (a second changelog) becomes a pointer, the
65-line file tree a short map, Tech Stack / Key Components / Project
Structure merge into Under the Hood, and the duplicate build instructions go.
A stale AsyncImage claim is dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@b451c
b451c merged commit a1ef423 into main Oct 5, 2026
3 checks passed
@b451c
b451c deleted the feature/source-edit branch October 5, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant