Skip to content

feat(core): add offset-based sync-scroll preview primitives#148

Open
Caldalis wants to merge 1 commit into
floatboatai:mainfrom
Caldalis:feat/core-sync-scroll-preview
Open

feat(core): add offset-based sync-scroll preview primitives#148
Caldalis wants to merge 1 commit into
floatboatai:mainfrom
Caldalis:feat/core-sync-scroll-preview

Conversation

@Caldalis

@Caldalis Caldalis commented Jul 10, 2026

Copy link
Copy Markdown

Summary / 摘要

Offset-based (not percentage) two-way scroll sync between the editor and a new read-only rendered preview pane: three
small core primitives (scroll event, scrollToOffset, renderPreviewHtml) plus a reference implementation in the
electron demo behind an opt-in toggle (default off).

Motivation / 背景与动机

This re-attempts roadmap #13 with the concerns that closed PR #118 designed out from the start:

  1. Single concern — scroll sync only. No keybinding UI. Wiki-link rendering in the preview is an explicit
    non-goal: it surfaced a pre-existing lezer-mdast-adapter.ts quirk, documented in design.md as a standalone follow-up
    instead of bundled here. (Two other narrow adapter fixes — table align and task-item text — are included because
    the preview functionally depends on them; the scope judgment distinguishing the two cases is in design.md.)

  2. Offset-anchored, not percentage — the editor emits the document offset at the top of the viewport
    (lineBlockAtHeight, CM6 content-space); the preview aligns the nearest data-offset block top. Sub-block ratio
    interpolation was deliberately dropped after self-review found the two sides measure "ratio" in incompatible units
    (source line vs. AST block) — design.md has the full analysis.

  3. Opt-in — default off; zero default-behavior change.

Changes / 变更内容

  • packages/core:
    • New rAF-throttled scroll event { offset, ratio } — ViewPlugin on scrollDOM, content-space lineBlockAtHeight, no screen-coordinate
      math; the CM6 EditorView is still never exposed to hosts
    • New EditorAPI.scrollToOffset(offset, ratio?) — write-side counterpart, same line-based unit
    • New renderPreviewHtml(ast) — mdast → HTML via the already-present remark-rehype + rehype-stringify (transform-only, no re-parse),
      top-level blocks tagged with data-offset; exported ScrollPayload; README updated
    • ⚠️ Two narrow fixes to the shared lezer-mdast-adapter.ts, called out explicitly: adaptTable hardcoded align: [] (and
      mdast-util-to-hast emits exactly align.length cells per row, so preview tables rendered as cell-less <tr> shells) and the Task branch looked
      for child nodes where Lezer keeps item text implicit in gaps (task items rendered as bare checkboxes). Fixed at the source rather than papered
      over in the preview — the task text is unrecoverable downstream, and other getAst() consumers (e.g. plugin-wordcount silently skipping task-item
      text) were affected too. Full scope judgment vs. the deliberately-deferred wiki-link adapter bug is in design.md; exportHTML() is unaffected
      (it re-parses via remark-parse). Happy to split these into their own PR if preferred.
  • apps/electron-demo:
    • New preview-panel.ts: read-only rendered pane, bidirectional block-top sync, two-frame echo guard
    • Link-click interception: plain click swallowed (electron/main.ts has no will-navigate guard, so an unhandled
      click would navigate the whole BrowserWindow away); Ctrl/Cmd-click opens externally — same convention as the inline
      live-preview link renderer; non-absolute hrefs are a no-op
    • Toolbar toggle + splitPreview setting (default off)
    • ⚠️ One shared-helper fix, called out explicitly: togglePanel() restored panels with style.display = "", which
      drops the inline display:flex these panels rely on. It now takes an explicit show-display value — this also fixes
      the same latent bug for the outline/vault/backlinks panels. Rationale in design.md; happy to split this out if you
      prefer.
  • openspec/: add-sync-scroll-preview — proposal, design (decisions + self-review findings), spec deltas for
    editor-core and sync-scroll-preview, tasks
  • New runtime dependencies: none

Testing / 测试

  • pnpm test passes — 535 tests, 20 new across scroll-event.test.ts, live-preview-to-html.test.ts (incl. 3 adapter regression tests:
    table cells + alignment, task text + checkbox + inline formatting, nested blocks under a task), preview-panel.test.ts
  • Affected packages build — pnpm build, pnpm build:electron-demo, pnpm typecheck all clean
  • Manual UI check in electron-demo across three rounds: toggled split preview on/off; verified top→bottom scroll alignment in both directions
    across headings, paragraphs, code fences, and tables; no feedback-loop jitter; panel stays off on a fresh profile. These passes surfaced and drove
    the fixes for three real-browser-only layout bugs, the window.open relative-href issue, and the empty-table / textless-task adapter bugs
    (tasks.md 4.4, 6.9, 6.10).

Compliance / 合规自检

  • CLA signed

  • AI disclosure:
    AI辅助笔记:

    工具:claude code(主要编码助手);AI辅助也用于英文文档编写和代码审查。
    AI的使用方式:

    1. 代码库理解——在设计工作之前,探索现有的实时预览/
      lezer-mdast-adapter/演示面板架构。

    2. 代码草拟——我设定了设计
      AI根据该设计草拟了代码;
      我审查并根据情况修改了代码。

    3. 测试草拟——AI帮助编写了我指定行为的测试用例

    4. 英文写作——我的工作语言是中文; AI 帮助我规范化了我的设计笔记和想法,并将其整理成英文 README 部分、OpenSpec 提案/设计/规范/任务以及代码注释,此外还处理了一些繁琐和机械性工作

    5. 搜索查询,整理资料

    由我完成:上述设计决策及其权衡(记录在 design.md 中);所有功能代码的代码审查;所有手动验证
    我可以解释并捍卫此 PR 中的每个设计决策。
    (节流、销毁清理、数据偏移完整性、链接点击安全性包括 wiki 链接垃圾 href 的情况)

  • New dependencies: none

  • No build artifacts committed

  • No secrets committed

Checklist / 自检清单

  • Title follows Conventional Commits
  • Public API changes update package README / types
  • live-preview-table.ts not touched (n/a)
  • New capability → OpenSpec change linked (add-sync-scroll-preview)
  • Change aligns with project scope (GOVERNANCE.md §4)

Screenshots / Recordings

test3

Implements roadmap floatboatai#13 (markdown live preview sync scroll): three core
primitives plus a reference split-preview pane in the electron demo,
behind an opt-in toggle (default off). Offset-anchored, not
percentage-based - markdown blocks render at wildly different heights,
so proportional sync cannot keep two panes aligned.

packages/core:
- New rAF-throttled scroll event ({ offset, ratio }) computed via
  lineBlockAtHeight in CM6 content-space; no screen-coordinate math.
- New EditorAPI.scrollToOffset(offset, ratio?) as the write-side
  counterpart, same line-based unit.
- New renderPreviewHtml(ast): mdast -> HTML via the existing
  remark-rehype/rehype-stringify deps (transform-only, no re-parse),
  tagging top-level blocks with data-offset scroll anchors.
- lezer-mdast-adapter: emit spec-shaped mdast for GFM tables (align
  parsed from the delimiter row instead of hardcoded []) and task-list
  items (gap text recovered into a paragraph child). Without these the
  preview rendered cell-less tables and textless task items; other
  getAst() consumers (e.g. plugin-wordcount silently skipping task-item
  text) were affected too. Scope rationale in design.md.
- Export ScrollPayload type.

apps/electron-demo:
- New preview-panel.ts: read-only rendered pane, bidirectional
  block-top scroll sync with a two-frame echo guard; link clicks
  intercepted (plain click swallowed, Ctrl/Cmd-click opens externally,
  absolute http(s) URLs only).
- Toolbar toggle + splitPreview setting (default off).
- togglePanel now takes an explicit show-display value, fixing a latent
  display="" restore bug also affecting outline/vault/backlinks panels.

openspec: add-sync-scroll-preview (proposal, design, specs, tasks).
No new runtime dependencies.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

2 participants