Skip to content

feat(outline): track reading position and flash jump landings (#64) - #68

Merged
orange2ai merged 5 commits into
marswaveai:mainfrom
moyu12-ae:fix/outline-scrollspy-feedback
Sep 5, 2026
Merged

orange2ai merged 5 commits into
marswaveai:mainfrom
moyu12-ae:fix/outline-scrollspy-feedback

Conversation

@moyu12-ae

@moyu12-ae moyu12-ae commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

解决的 Issue

Closes #64

改动内容

1. 阅读进度同步(进度视图)

  • 滚动正文时,大纲中当前章节实时高亮(所见即所得与源码模式均支持),即 issue 中提到的「随着进度不断调整」。
  • 高亮颜色从各主题的 --link-color 通过 color-mix 派生(复用搜索高亮的既有模式),12 个内置/独立主题零适配自动跟随,深色主题同样适用。
  • 长文档中当前条目自动保持在可视范围内;滚动触底时激活最后一个标题。

2. 跳转反馈(对标飞书的闪烁提醒)

  • 点击大纲条目、或点击文内 [text](#anchor) 锚点链接跳转后,落点标题整行闪过一次主题色高亮带(约 1.6s 渐隐)。
  • 闪烁在平滑滚动到位(scrollend)后才开始播放,保证用户看到落点时反馈刚好出现。
  • 实现上使用 ProseMirror Decoration 而非直接改 DOM class:实测外部 class 变更会触发 ProseMirror 的意外变更重绘,重绘会立即重建标题节点并吞掉闪烁。

3. 顺带修复:大纲点击跳转静默失效

  • 大纲此前缓存标题的 DOM 元素引用,而 Milkdown 在内容重设时会重建全部标题节点,缓存引用集体失效后 scrollIntoView 变成对游离节点调用,点击跳转就「丢了」。点击时实时重新解析 DOM;scrollspy 热路径复用 renderOutline 缓存的引用、仅在节点失联时回退实时查询,避免每帧 querySelectorAll。
  • 侧栏露出当前条目改为直接滚动面板自身:scrollIntoView 的祖先链滚动会连带取消正文窗格进行中的平滑滚动。

4. 长标题显示

  • 截断的长标题悬停显示完整 tooltip(侧栏可拖拽调宽已按 design.md 另行推进,不在本 PR 范围)。

Review 修正(2026-09-03)

  • 锚点跳转接入跳转锁:编辑器内跳转新增 start/settle phase 信号,锚点链接与大纲点击走同一把锁——长距离锚点滚动途中大纲高亮不再被沿途章节抢走(CDP 实测:滚动途中高亮保持原章节,落点后切换到目标章节)。
  • 兜底计时器位移门控:跳转锁与闪烁的兜底定时器在滚动位移持续时重新武装,长距离平滑滚动不再提前释放/提前播完闪烁。
  • scrollspy 热路径开销:复用缓存引用 + 失联回退,稳态滚动每帧仅读取被激活条目的 rect。
  • Rebase 到最新 main:移除 feature-requests.md 中对可调侧栏的 declined 复核段落(design.md 已接受 180–420px 拖拽)。
  • 安全修复(afterPack shell 注入)已按 review 拆分为独立 PR fix(security): stop interpolating appPath into shell strings in afterPack #69。

验证

  • tsc --noEmit、electron-vite build、check:theme-colors(12 主题)全部通过。
  • 根目录 outline-test.md 多级标题验收文档(含超长标题、重复标题锚点、文内跳转链接),按文档开头清单可逐项手动验证。

@moyu12-ae

Copy link
Copy Markdown
Contributor Author

效果如图所示:
image

@moyu12-ae

Copy link
Copy Markdown
Contributor Author
image

@orange2ai orange2ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢这个 PR,方向完全符合 #64 的真实需求,实现质量在同类方案里是最好的:rAF 节流的 scrollspy、所见即所得与源码模式双支持、文末锁定最后一项、跳转锁防抢高亮、用 color-mix(var(--link-color)) 派生高亮色使 12 个主题零适配、用 ProseMirror Decoration 而非直接改 DOM 避开 unexpected-mutation 重绘——这些设计都是对的。类型检查与构建我这边复验通过。

合并前需要两处修改:

1(必须):文内锚点链接跳转没有接入跳转锁

[main.ts:309-311] 大纲点击走 outlineJumping 锁,但 editor.ts 内的锚点链接 click 处理器([editor.ts:459-462])只调用了 flashHeadingOnArrival,没有通知跳转锁。结果:点 [text](#anchor) 平滑滚动途中,scrollspy 会把大纲高亮切到沿途章节——这与 PR 自带的验收文档 outline-test.md 第三节的承诺直接矛盾。

修法建议:把跳转锁的启停下沉到 editor.ts 共用,或让 flashHeadingOnArrival 顺带通知锁,两条跳转路径行为保持一致。

2(必须):scripts/afterPack.js 的安全修复拆成独立 PR

[afterPack.js:10-16] 把 execSync 换成 execFileSync 消除 shell 注入面,修复本身正确、无副作用,我也认可合并——但它是独立的 fix(security) 提交(23e79ad),与本 PR 功能无关。拆出去单独合,保持 PR 历史可审计。

3(建议,不阻塞)

  • [main.ts:398-413] visualActiveIndex 每帧重跑 querySelectorAll 全量标题并对视口以上每个标题读 rect。outlineItems 在 renderOutline 时已缓存 element,滚动路径可复用缓存、仅在元素失联时回退实时查询,把每帧开销降到 O(命中数)。
  • [editor.ts:101] 900ms 兜底在长距离平滑滚动时会提前播完闪烁带,可放宽到 ~1500ms 或仅在无滚动位移时启用。

4(rebase 提示):本 PR 在 feature-requests.md 里把「可调侧栏」re-affirm 为 declined——维护者刚刚已决定接受可调面板(design.md 已改为允许 180–420px 拖拽并持久化),rebase 时请移除该 declined 复核段落。

处理完 1、2 即可合并。

…veai#64)

- Highlight the outline entry for the section at the top of the viewport
  while scrolling, in both visual and source modes; the entry stays
  revealed in long documents. Colors derive from each theme's link color.
- Flash the landing heading once after outline or anchor-link jumps, held
  until the smooth scroll settles so it plays where the user is looking.
  The band is a ProseMirror decoration: a direct DOM class trips
  ProseMirror's unexpected-mutation redraw, which recreates the heading
  and destroys the band instantly.
- Re-resolve outline entries against the live DOM on click and during
  scrollspy updates: Milkdown recreates heading nodes whenever the
  content is re-set, which silently detached the cached references and
  killed outline jumps entirely.
- Pause scrollspy updates while a click-driven jump is in flight so the
  clicked entry is not overwritten mid-scroll.
- Add hover tooltips so truncated outline titles stay readable.

Side panel reveal now scrolls the panel itself instead of using
scrollIntoView, whose ancestor walk also cancelled the content pane's
smooth scroll.
Add a multi-level heading document for manual verification of the
outline scrollspy, jump flash, and tooltip behavior.
- Emit an editor jump phase signal ('start'/'settle') from
  flashHeadingOnArrival and drive the outline jump lock from it, so
  anchor-link jumps hold the reading-position highlight exactly like
  outline-driven jumps instead of letting scrollspy switch to sections
  passed along the way (review on marswaveai#68).
- Gate both fallback timers on scroll displacement: they re-arm while the
  position keeps changing, so long smooth jumps no longer release early.
- Reuse the cached heading references in the scrollspy hot path and fall
  back to a live query only when a cached node went missing, dropping the
  per-frame querySelectorAll.
- Drop the re-affirmed declined note for the resizable file panel: the
  panel width decision now lives in design.md (180-420px drag).
@moyu12-ae
moyu12-ae force-pushed the fix/outline-scrollspy-feedback branch from 505ae1e to 222f34a Compare September 3, 2026 00:34
@moyu12-ae

Copy link
Copy Markdown
Contributor Author

感谢详尽的 review,两点必须项与两项建议都已处理(force-push 更新,rebase 到最新 main):

  1. 锚点跳转接入跳转锁:新增编辑器跳转 phase 信号(start/settle,由 flashHeadingOnArrival 发出),锚点链接与大纲点击共用同一把锁。CDP 实测:滚动中途大纲高亮保持原章节,落点后切换到目标章节并正常闪烁。
  2. afterPack 安全修复已拆出 → fix(security): stop interpolating appPath into shell strings in afterPack #69。
  3. 建议 3 两项均已采纳:scrollspy 热路径复用缓存引用(失联才回退实时查询),两个兜底计时器改为位移门控(滚动持续时重新武装),不再有固定 900ms 提前释放。
  4. rebase 时已移除 feature-requests.md 中可调侧栏的 declined 复核段落,与 design.md 的 180–420px 决定保持一致。

验证:tsc / build / check-theme-colors 通过,锚点锁与大纲点击回归均已实测。

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