Repository navigation
Release 1.15.0 - #58
Merged
Merged
Conversation
Source Edit can show the editor and the rendered document side by side. The rendered side follows the buffer as you type; only Save writes the file. Toggle with the Split pill, Option-click on Edit, or ⌥⇧⌘E.
QuickMD forces every document into the frontmost window as a tab (WindowTabbing.preferTabsAlways + a manual merge), so two documents can never be read side by side. Settings ▸ General ▸ Windows ▸ "Open each document in its own window" turns that off: each document then gets its own window. How it works: - At launch, the tabbing preference is written to the argument domain as "manual" instead of "always", and NSWindow.allowsAutomaticWindowTabbing is set to false. The preference is what AppKit reads when it shows a window, and AppKit caches it for the whole process, so it can only be chosen at launch. - While the app runs, the two levers that do work at runtime are used: NSWindow.allowsAutomaticWindowTabbing (global) and NSWindow.tabbingMode (per window). The manual merge that joins a new window to an existing tab group is skipped in separate-windows mode. Documents already open keep the tab group they are in — AppKit gives an app no way to take a tab group apart — but every document opened from then on honours the new mode.
feat(windows): option to open each document in its own window
feat(edit): split view with live preview (⌥⇧⌘E)
The code highlighter is language-blind: every fenced block got the generic number / string / keyword / comment / type colours, so a log or prose pasted as ```text lit up like source code. GitHub draws plain-text fences without any colour (Linguist "Text", tm_scope none). The highlight moves from CodeBlockView into BlockTextConverter, next to plainCode (the string it only recolours), so it is compiled into the tests. highlightedCode returns plainCode unchanged when the info string's first word is text, plaintext, fundamental or parindent (case-insensitive). Characters and fonts are untouched: measured heights, search, selection and print are unaffected (print never coloured code). No info string, unknown words, txt and console keep the generic colouring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ⌘G jump glides 0.25 s to the match through an animator scroll of the clip view. AppKit sets the clip origin on every frame of that animation, so a wheel, trackpad, scroller or key scroll during it was undone and the view landed on the match anyway (measured headlessly: a scroll 80 ms into the glide ended at the target). ScrollGlide (VerticalRhythm.swift, next to PendingMatchAim) replaces the coordinator's isJumping / jumpGeneration pair: each glide has a token, the reader's scroll stops a running glide and invalidates its token, so the completion AppKit still delivers for the stopped animation neither aims at the match nor ends a later glide. ScrollGlide.stop replaces the running animation with a zero-length one at the current origin; the reader's own scroll then continues from there. Every reader-scroll hook (scrollWheel, the live-scroll notification, scrollVertically) runs before the event's scroll. Non-animated placements and new tables keep the old semantics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) Separate windows now behave like macOS's own "Never" setting: the launch writes "manual" and automatic tabbing stays allowed, so the Window menu keeps Merge All Windows / Move Tab to New Window / Show Tab Bar. The global allowsAutomaticWindowTabbing flag is turned off only when the option is switched on in a process that was launched with tabs ("always"): AppKit reads the tabbing preference once per process (measured), and a per-window tabbingMode set from WindowConfigurator lands after the window is ordered in. - joinExistingTabs checks "already in a tab group" before the option, so a restored or user-merged group is never resized to the remembered size. - applyWindowPreference also rewrites the argument domain; no per-window tabbingMode writes any more (off is byte-for-byte the #36 behaviour). - Settings: "Open documents in separate windows", one caption saying what a running app cannot change (tabs in full screen need a relaunch). - Tests use an isolated defaults suite, inject the cached AppKit preference, restore the size keys by removing absent ones, and test behaviour instead of mirroring the ternary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…initions Footnotes were numbered in definition order and every definition was listed, so a document citing [^b] before [^a] showed ² before ¹, unused notes appeared in the list, and a heading with a footnote got an id that disagreed with GitHub's. Numbers now follow the first reference in the document (a definition's own text counts where it stands), unreferenced definitions are dropped, and heading ids follow automatically. A definition only ever kept its first line: a lazy continuation line or an indented paragraph after a blank line fell out into the document as a body paragraph, and a [x]: url line right under [^n]: defined a link where GitHub shows it as footnote text. Definitions now take their lazy lines and 4-column-indented paragraphs (FootnoteDefinitionBody), and the footnote list draws one line per paragraph or hard break. Expected values come from gh api markdown (cases in FootnoteTests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s are rows A paragraph made only of images (plain and/or linked) is one block now, as GitHub renders it: one picture keeps the image block (and its link), two or more become an image row that wraps like words. Badges one per line, ` ` and consecutive image lines were placeholders or a column of separate image blocks before. - Parser: `imageParagraph` decides the whole paragraph (soft break = space, two trailing spaces / backslash = hard break, setext underline or text = not images); lines inside a longer paragraph keep the pre-1.15 output. Linear: a failed paragraph is not rescanned per line. - `InlineSyntaxScanner.imageOnlyLine` replaces `linkedImage` and keeps the link destination; `HTMLImageSyntax` keeps the href of an `<a>` around an `<img>` on the same line. - A linked picture's click goes through `handleLinkActivation` (the text links' handler), with a pointing-hand cursor rect, the destination as help, and the diagrams' hover zoom button for the preview on pictures of at least 64 pt. - `ImageFlowLayout` and `BlockLayout.ImageRow` share one line breaking, so a row with known picture sizes is measured exactly; rows are `.reported` like images. Print uses the same flow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inline `$…$` was typeset only in paragraphs; quotes, alerts, table cells and headings showed the LaTeX source. Now they go through one converter (`BlockTextConverter.makeMathString`, the paragraphs' segmenter applied line by line) so the view, the measurer, selection/copy and search share one string per block: - quote and alert bodies: inline formulas, and a paragraph made only of `$$…$$` formulas is display math (as GitHub decides it), centred, copied as `$$⏎latex⏎$$`; - table cells (grid tables; front matter stays text): inline formulas, pipes as GitHub splits them, copied as `$…$`; a table with a formula is built on the main actor (SwiftMath is main-thread only) and re-used on a width change; - headings: formulas at the heading's size; ids and the Contents sidebar keep reading the source, as GitHub's ids do; - print/PDF typesets the same places; untypesettable formulas show their source; a `$` inside a code span is never a delimiter (also fixes paragraphs, where "`$x$`" was typeset). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#57) - WindowTabbing doc comments separate what was measured on macOS 27.0.1 (standalone probe, now including a tabbingMode set right after orderFront: no effect either way) from what is inferred (WindowConfigurator timing, Window-menu items under the global flag). The WindowConfigurator comment no longer says every window joins as a tab: that depends on the option. - testPreferTabsAlwaysIsWhatAppKitReads read AppKit's per-process cached preference, so its result depended on which test touched AppKit first. It now asserts the stored argument-domain value (renamed accordingly). - WindowTabbingTests use the shared TemporaryDefaults helper: per-test UUID suites left an empty plist per test in ~/Library/Preferences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t a time (follow-up to #55) The preview is now decided by one pure rule (SideBySidePreview) fed on every buffer change, every load/reload (new editor onReplace -> session bufferReplaced), every commit of the rendered text, on toggling and on leaving. Before, Save followed by a clean external change, or Don't Save on a cancelled quit, left the preview showing text that was neither the buffer nor the disk. The list ignores a leftover preview once side by side is gone. Section copy slices the text the headings were parsed from (installedText): side by side those are the buffer's lines, and slicing the disk at them copied the wrong section. PreviewRefreshScheduler: 0.3 s debounce, 1 s maximum wait, nothing published while any parse of the window runs (the detached parse cannot be cancelled; superseded parses used to pile up on big files), waits widened to the last parse's duration above 256K UTF-16 units. Per keystroke it only re-arms a timer. Layout: banners sit below the Side by Side / Save / Done row while editing; "Rendering..." shows on the preview half while it is re-measured; entering side by side grows a window narrower than 700 pt of document (plus sidebars) within the screen, keeps that minimum while it shows, and refuses with a toast when the screen cannot hold it. Toggling beeps while a prompt is up; closing the graphic preview while editing focuses the editor; ToC clicks and in-page anchors scroll the preview too. Naming: "Edit Side by Side" / "Editor Only" (menu), "Side by Side" (pills); never "Split View", the macOS window feature. Print / Export / Copy Markdown stay off while editing (SourceEditMenuState.allowsSavedTextCommands). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in 1.14.4 (follow-up to #55) Only side by side slices `installedText` (the buffer the preview's headings came from). Reading and the editor alone keep 1.14.4's `currentText` exactly, including the moment between a commit and its parse landing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An image block keeps its 8 pt corners; a picture in a row gets at most 15 % of its shorter side, so a 20 pt badge keeps its own 3 pt shape instead of turning into a capsule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A list item owns the blocks indented under it (CommonMark). Until now an indented code block under an item was drawn as plain text, a fenced one sat at the margin, and a second paragraph of an item started at the margin with its source spaces. The block list stays flat. ListNesting follows the open items and their content columns line by line; the parser records which item owns a code block or a paragraph, and renderPendingRows (which already knows each ordered list's widest marker) turns that into the item's text x: - indented code (4 columns past the content column, not after paragraph text) becomes a code block; fenced and indented code carry MarkdownBlock.listIndent, the view and print pad by it and the measurer narrows the code by it; - a paragraph of the item hangs at the item's text, with one indent unit in its characters for print/copy, like a nested item. Numbering and marker alignment continue past the code (unchanged mechanism). Gaps are the existing text/box pair values. joinSoftBreaks: a line after a hard break inside an item no longer turns the item's paragraph into a plain one, so the next ordered item stays an item (GitHub: <li>item 1<br>line two</li><li>item 2</li>). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS 27.2 tables showed the header band, the outline and the column dividers but no horizontal rules. All rules were NSTextTableBlock borders painted by AppKit; the bottom ones stopped showing there. 27.2 is not available to reproduce, so the rules no longer depend on AppKit at all. The blocks keep their border widths (layout: cell rects, row heights and the .exact table height are unchanged) but the border colour is clear; the view paints the dividers and rules itself, last, inside the rounded clip, from the laid-out cell bounds (tableRuleRects), snapped to whole device pixels. The rects are the ones AppKit filled on 27.0 (logged through fillBackgroundRectArray), so the result is pixel-identical there: 1904 headless renders (light/dark/GitHub/Dracula, every zoom rung 0.7-3.0, grid, headerless, header-only, one-row, wrapped, wide, front matter; plain, select all, partial selection, search; 1x and 2x) match the previous build exactly at whole-point widths. TableTextView.draw now runs drawTable (shared with the tests). The rule colour travels with the TableText, built with the string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of 39a2234: every momentum event stopped the glide, so a flick followed by ⌘G while the inertia still ran stopped the jump after one frame and the match never showed. ScrollGlide.scrollWheel now decides per event: a mouse wheel tick (no phases) stops it; a trackpad gesture or its momentum only if the gesture began during this glide; a mostly horizontal event (|dx| > |dy|) or one without vertical delta never. The live-scroll notification stops it only for the scroller (a gesture posts it from inside scrollWheel, which has already decided). The match-aim rule is unchanged. The mid-flight stop is now proven in a test: a 2 s glide stopped once the origin has left 0 rests between start and target, its completion is ignored, a later scroll is kept, the next glide lands. No rounding was added to stop(): NSClipView.setBoundsOrigin already snaps to device pixels (1234.3 -> 1234.0 at 1x), which the test pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, a reader scroll stops the Find Next glide Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Review follow-up. Which rules exist and how thick they are now comes from the table layout (a divider after every column but the last; a rule under every band but the last and always under the header) and the BlockLayout constants, never from the blocks' border widths: a 27.2 AppKit that drops or reports 0 for the maxY border must not make the rules vanish again. The position still comes from the laid-out cells; a band's rule sits on the band's lowest cell bottom, so cells of unequal height cannot step it. Snapped rects are tested against the dirty rect. Tests: a wrapped cell next to one-line cells gives one straight rule at the top of the next band; a simulation with every maxY border width stripped to 0 still paints every rule (light/dark, 1x/2x, grid, ragged, headerless, front matter). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inline renderer matched `*`, `_`, `~` by "find the next marker" and links by bracket depth, so rare cases differed from GitHub: `5__6__78` was bold, `*foo**bar**baz*` and `**a *b* c**` lost a trait, `*[foo*](u)` was italic, `[a [b](x) c](y)` linked the outer text, `***`/`~~` content was not parsed, 3+ backtick code spans were text, and a title with a paren (`[a](u "x (y")`) broke the link. renderInlineFormatting is now one linear pass: nodes (text, atoms, delimiter runs), a bracket stack resolving links at `]` (inner link wins, earlier openers deactivated) and CommonMark's delimiter stack (InlineEmphasis.swift: flanking, intraword `_`, rule of 3, GFM `~`/`~~` of equal length). No recursion, so adversarial lines stay on the 512 KB stack. Code spans follow CommonMark (any run length, space stripping) in one shared scanner; inline link tails are parsed CommonMark-style first (pointy destinations with parens, titles with parens/quotes) and leniently second (destinations with spaces keep working). `]` resolution is shared with ReferenceLinks so quotes/alerts/cells/headings resolve brackets exactly like paragraphs. Also: `<scheme:…>`/`<a@b>` autolinks, GFM bare-URL edges (trailing punctuation, balanced parens, www.) and e-mail autolinks. Emphasis over an atom keeps the pre-1.15 look (body font + text colour); an ordinary document renders run-for-run as before (pinned by a dump from 53ec549). Parse time: 11.7K-line fixture 1425 -> 1307 ms, 2K-line 196 -> 77 ms, 10K synthetic 1643 -> 1193 ms (-O, median of 7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…paragraphs - A fence, indented code, heading or rule inside a footnote closes its paragraph: the next unindented line is the document's again (it was swallowed as lazy footnote text). Code lines are drawn literally. - A definition inside a list item counts its 4 continuation columns from the item's content column (FootnoteContainerColumns); the item's next paragraph no longer disappears into the footnote. - Numbering reads a paragraph with its soft breaks joined, so a code span across a line break hides a reference as the renderer does; references inside a duplicate definition count where it stands (GitHub). - A lazy line after a link definition at the footnote's start is footnote text. - A definition between paragraph text and `---` / `1984. y` / text after an empty `[^n]:` ends the paragraph (a blank marker line): no more Setext heading out of "Para\n[^1]: x\n---" (pre-existing). - The reference scan is linear (cached next `]`, backtick runs paired with forward-only pointers; 50k `[^` took 75 s before) and lives in Footnotes.swift, independent of ReferenceLinks' private helpers. Expected values from gh api markdown; one test per review input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce, multi-line definitions Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…ld preview back (follow-up to #55) Second review of the follow-up: - The width side by side grew is given back on leaving (toggle off, Done, Esc, any leave) unless the user resized or moved the window meanwhile (SideBySideGrowth); WindowSizeMemory remembers the pre-growth size while the growth is in effect, for every window of the tab group. - Banners are lowered under the pills only while side by side shows; reading and the editor alone keep 1.14.4's position. - Leaving side by side clears the preview at once (scheduler.hide), never after a running parse; the window's parse task waits for a running parse (waitUntilIdle), so a toggle or a leave mid-parse never starts a second detached parse and a superseded one never lands. - A sidebar shown, hidden or dragged side by side re-runs the fit; the minimum width never exceeds what the window has (narrower halves instead). - The Option relabel of the Edit pill only while the pointer is over it, reset when the app or a window resigns; "Rendering..." on the preview half only after 0.3 s pending and not during a live resize. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for formulas
Review fixes for the math-in-every-kind change:
- InlineMathSegmenter follows GitHub's delimiter rules (gh api, quoted in
the tests): an opener only at the start or after a space, tab, line
break or "(", the closer is the NEXT `$` and closes nothing when an ASCII
letter, digit or `_` follows, no formula across a line break, digits
are a formula, and `$$…$$` inside text is an inline formula. So
`$HOME/$USER`, `$5-$10`, `$a$1` stay text everywhere, paragraphs included.
- The loose regex is gone: `containsInlineMath` / `needsMath` ask the
segmenter, so a price or shell-variable table, quote or heading is built
off-main exactly as in 1.14.4.
- A display formula in a quote or alert gets the `$$` block's air
(`BlockLayout.Math.verticalPadding`) as paragraph spacing — measured from
the same string; print uses the same padding.
- A formula wider than its line or cell is scaled down to fit
(`FittingMathAttachment`) instead of drawing into the neighbour.
- The untypesettable display-formula fallback limits are documented.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decided for 1.15 (option B): an emphasis span no longer re-stamps everything inside it with the body font and the text colour. It adds bold/italic to each piece's own font (DocumentFonts, both scopes): a code span stays monospaced on its chip, a footnote number keeps its size, an image placeholder its colour, a link the link colour. GitHub draws them the same way. One switch, `emphasisKeepsContentLook`, brings the pre-1.15 look back if the before/after board is rejected. Test: a footnote reference inside `~~…~~` / `***…***` is a reference in a paragraph and in a quote alike (their content is parsed now). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llow-up to #55) The idle gate made every parse wait for a running one, so a viewer that never edits saw a zoom / theme / reload / external-change parse start only after the superseded one finished. Now a parse task waits only while Source Edit is active, or while a parse of PREVIEW text is still running (left from a session that just ended: Esc -> Don't Save while it parses would otherwise start a second detached parse next to it). Outside that, nothing waits: no suspension point is added and superseding parses start at once, as on main; superseded results are dropped by the existing cancellation guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons in code - The definitions pre-pass uses ListNesting instead of the footnotes branch's FootnoteContainerColumns (one model of list nesting): it gives a footnote definition its item's content column, and it marks an item's indented code as verbatim, so `[x]: url` / `[^1]: …` lines in it stay code (GitHub: they are in <pre><code>). Only a fence's opening line starts a block for it (fence content no longer opens list items). - A link or footnote definition the pre-pass took out starts a block for the block loop's nesting too: at the margin it ends the list. - An image or <img> line is paragraph text for nesting: the item's paragraph goes on (GitHub: `- item /  / more` is one <li>). - Only `.codeBlock` rows get listIndent (not mermaid/svg fences). - Code indent is clamped to half the column (BlockLayout.Code.leadingInset), used by both the measurer and the view. - Print/PDF pads code by listIndent minus the list gutter: SwiftUI Text draws item text without the gutter there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…L rows Review follow-ups for image rows: - A linked picture is its link while loading, failed or behind a denied folder too (same click path, help tag, pointing hand, link role) — 1.14.4's badge placeholders were clickable, an offline README must not lose that. - A row picture that is not showing is a badge-height chip of its alt text instead of the 40 pt error card; `ImageRow.chipSize` frames the view and sizes the measurer's row, so an offline badge row is exact. Single images keep their cards (pixel-identical to main). - Rows wrap at the measured content width, not the live cell width. - Search no longer counts row alts (rows draw no captions). - VoiceOver: link role instead of button on linked pictures. - HTML image lines take part in picture paragraphs: several `<a href><img></a>` on a line, such lines one under another, and an `<img>` line under Markdown image lines are one row, as on GitHub. The HTML whitelist accepts exactly what it did. - The linearity test is structural (one attempt covers a failed run), no wall clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d or cache hits Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…and headings Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…elines - Pins dump ROLES, not values: colours as the theme role they equal (text/link/secondary/code, resolved at test time), fonts as size + bold/italic/mono, plus underline/strike/link/baseline/superscript/tags. CI's older SDK resolves the system blue and font names differently. The ordinary pin was regenerated from a detached 53ec549 worktree; the only difference from now is the documented H1 code run. - AutolinkScan memo is exact again: when only the second-to-last label has the `_`, it covers starts before `lastDot - 3`, so `www._www.example` links `www.example` (GitHub); still linear (work counters). - E-mail after `/` is refused only inside `ftp://` (GitHub); `sales/support@x`, `path/to/me@host`, `ssh://git@github.com` link again. - `InlineSyntaxScanner.LinkBrackets`: ONE bracket stack deciding which `]` closes a link, used by the renderer, ReferenceLinks and FootnoteReferenceScan — the footnote scan now skips link destinations (`[t](x[^a]) then[^b]` numbers b only, GitHub); an image's `[` does not deactivate an outer link. - Reference labels capped at 999 BYTES, as cmark. - Copy-section button: first baseline from the fonts the title is set in (`titleFirstBaseline(of:headerFont:)`), view and measurer alike; plain titles unchanged, code-only titles match TextKit's baseline. - Definition terms use the same bold-context helper as headings (`boldLook`): code stays monospaced on its chip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ootnote-scan fixes after review Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…eline The math branch made the title lazy and the inline branch reads its fonts for the copy button's baseline; after both merged the row did not compile. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…undaries, print floor
Final review of the math change:
- The `$$` look-ahead stops at a fence line (``` or ~~~): an unclosed `$$`
above a fence whose code holds `$$` stays text and the fence stays code
(GitHub). The pre-pass shares the same look-ahead. The existing
"fence inside a `$$` block is the formula's" expectation was not what
GitHub does and is updated with its output.
- Scientific notation glued to words keeps working (owner's call, as in
1.14.4): a formula that can only be LaTeX — a `\command`, or one `_`/`^`
sub/superscript (a letter, digit or `{…}`) — closes even before a letter
or digit: `H$_2$O`, `x$^2$y`, `10$^{-3}$m`, `2$\times$3`, `$\alpha$1`.
`$_FOO$BAR`, `$^ $@`, `echo $_ and $x` and the shell/price cases stay text.
- A heading formula keeps its own run through the heading look, so
`# T $x$**b**` typesets like the paragraph.
- Print: two display formulas in a row inside a quote are one padding
apart, and the 50 % floor → source rule applies (display and inline).
- The source fallback is capped at 8 lines, the last ending with "…"; a
copy still writes the whole source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s folder E2E in the App Store build: every local picture the sandbox would not read ran its own app-modal NSOpenPanel. Badge rows made that one panel per picture; Cancel brought it back for the next one and on every re-layout (nine in a row), and granting the document's folder did not cover `badges/` (exact-path match). `FolderAccessBroker` (pure, injected effects, unit-tested) decides: - a grant or a saved bookmark covers the folder and everything below it (paths standardized, symlinks resolved); - the panel asks for the document's folder when the picture is in its subtree, else for the picture's folder; - a declined folder is not asked for again this session (pictures show the denied card/chip at once); the explicit Grant button / menu item always asks and a grant clears the declines it covers; - requests while a panel is up wait for its answer (one panel); - no panel while the app is inactive (waits for it to become active); - not sandboxed: never asks. A grant posts a notification; denied pictures under it reload in every window. Print restores ancestor bookmarks silently, as before never asks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E2E saw a kb-posted wheel (pixel units, no phases) during a ⌘G glide leave the view exactly on the next match. Headlessly, the same CGEvent converted by AppKit (pixel -12: scrollingDeltaY -12, deltaY -1.2, precise; line -1) is classified as the reader's scroll and, delivered to the scroll view directly or through a hosting view / text view responder chain during a running animator glide, stops it and then scrolls by itself. So the rule and the delivery hold; the test pins the field combination. The order of wheel vs glide in the app is now visible in DEBUG builds: `wheel: … gliding=… → stop|ignore` and `glide landed`, next to the existing `scrollRequest` / `place … (jump)` lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… DEBUG scroll logging Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…nels while editing Follow-ups to the folder-access fix (E2E + whole-release seam review S1): - A click on a local link is an explicit request: in the sandboxed build a target the app cannot read asks for folder access (document folder when it is in its subtree, even after a decline) and opens on success. It used to fail silently. A declined panel, a missing target (both builds) and a failed `NSWorkspace.open` (completion handler) each show a toast. The program check runs after access, on the readable type. - `ImageLoader`: a folder (also `![x]()`, the document's folder) is `.notFound`, readable bytes that do not decode are `.undecodable`; only an existing unreadable file is `.unreadable`, the one case that asks. - No automatic panel while the document is in Source Edit (the preview re-renders half-typed paths): the picture shows its denied placeholder, nothing is marked declined, Grant still asks. Remote pictures in the preview are fetched once their URL has stood for a second. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The footnote list's separator and the thematic break are a row of 32 "─" characters. In a column narrower than that (the half-width side-by-side preview, a narrow window) they wrapped into a second short dash line. Both now carry one shared clipped paragraph style: a single line that shrinks with the column. The characters, and therefore search, selection and copy, are unchanged. The look at normal widths is pixel-identical (900 pt). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… access, rules never wrap Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The same idea and colours (blue tile, light page with a folded corner, the Markdown mark in a cyan-to-violet gradient), drawn as vectors on the macOS icon grid: no glow, no scroll, a larger mark. 16 and 32 px use a simplified tile with a white mark. SVG sources live in QuickMD/AppIcon. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Emphasis, strikethrough, brackets and autolinks ran before formulas were
found in the rendered text, so `$f^*$ and $g^*$`, `$a*b$ ... $c*d$`,
`$(a)_n + x_{i}$` and `$[c,d](u)$` broke - in paragraphs and, since 1.15,
in headings, cells, quotes and alerts.
- InlineMathSegmenter.formulas(in:) applies the existing delimiter rules to
the SOURCE line (escapes, code spans, `$` respected; one byte search
for lines without `$`/`&`); the inline pass draws each formula as an
opaque atom tagged InlineFormulaAttribute whose characters are its source.
pieces(of:)/hasMath read only that attribute. Code beats a formula; a
formula beats emphasis, links and autolinks; it never cuts a link in two.
- Removed the rendered-text heuristics: EscapedCharacterAttribute,
FormulaSourceAttribute, formulaSourceWithEscapes, run-boundary/link-edge
scans, tex().
- Heading ids keep GitHub's text (inline pass without formula atoms).
- N1: the heading baseline includes a taller formula's top (view and
measurer through titleFirstBaseline(of:headerFont:shown:)); headingRow
loses its @autoclosure.
- N2: a formula in link text keeps the link (URL + underline on the
attachment, link colour on the source fallback).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inks, heading baseline, formulas in links Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…ck fences
- InlineMathSegmenter.Cursor: the formulas of a source line as a
left-to-right pass meets them, shared by renderInlineFormatting,
FootnoteReferenceScan.labels and ReferenceLinks.inlinedLine. A `[^a]` or
`[a][r]` inside a formula is no longer numbered or rewritten (the private
number marker no longer lands in the LaTeX of a quote or heading formula).
- An `_` after a closer counts as markup only when an earlier `_` run may
open emphasis (snake_case $x$_ end is text, as on GitHub).
- fenceOpening: a backtick fence's info string holds no backtick
("```js``` is code" is a paragraph, as on GitHub); this also keeps the
`$$` look-ahead going.
- Stale escapableChars comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… one panel per click - Unsandboxed, a POSIX-unreadable picture shows the plain error: the folder-access card and menu only appear where a panel can help (`ImageLoadFailure.folderAccessFile(isSandboxed:)`). - A link to a missing file says "Couldn't find" before any panel: `stat` is metadata, which the App Sandbox allows everywhere; only existence it cannot tell (EACCES/EPERM) goes on to ask, and a decline never claims the file exists. - An explicit request that finds an automatic panel up for a folder covering it takes that panel's answer: one click never shows two panels. - Paths are compared as the disk spells them: the deepest existing ancestor is canonicalized (symlinks, on-disk case via realpath), the missing rest appended, so case-insensitive volumes and missing files under /var match their folders. - The Source Edit registry is ended by the window token and keyed by the path at activation (`SourceEditRegistry`), so a rename while editing leaves no stale mark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The selection registry kept ONE text view per block id. With a visible window more than one hosting view can be attached to a row at once (a queued cell still holding the row, a cell prepared ahead of the scroll); the last to attach won, and when it was dismantled, freed or not in the table, the row's entry was gone while the view on screen, whose attachSelection short-circuits on an unchanged id, never registered again. Its link clicks went nowhere and selection fell back to the whole row (any text row; seen first on a paragraph with a formula after scrolling away and back). Present since 1.11. AttachedViewRegistry (SelectionRows.swift, unit-tested) keeps every attached view per id; a view leaves only by its own remove, and the controller picks the one on display in its table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view (dead links after scrolling) Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The eighth screenshot shows Edit Side by Side. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The system green differs between macOS versions, so the raw value failed on the macos-15 runner. The test job now keeps the result bundle and prints the failure summary when a test fails. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
b451c
marked this pull request as ready for review
November 11, 2026 22:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
QuickMD 1.15.0 (build 32). Details:
QuickMD/CHANGELOG.md.textcode blocks.Draft until the release is cut.