Skip to content

fix(macos): 修复大型 Java 文件编辑与滚动卡顿 - #621

Merged
1lck merged 7 commits into
previewfrom
codex/fix-java-editor-561
Sep 11, 2026
Merged

1lck merged 7 commits into
previewfrom
codex/fix-java-editor-561

Conversation

@xiaoyumuxi

@xiaoyumuxi xiaoyumuxi commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

大型 Java 文件在滚动、光标重绘和结构结果返回时,会在主线程反复遍历全部折叠区域、全文着色并强制全文排版。本次让折叠箭头复用可见行几何,并把 Java 语义着色纳入视口缓存,降低持续滚动与结构结果应用的停顿。

Fixes #561

完整的初始复现报告、单变量实验、配套测量 JSON 和正式修复复测结果见对应 issue 评论。

修复内容

  • 行号栏按起始行索引折叠区域,直接在现有可见行循环中绘制箭头,复用布局和隐藏行判断,消除屏幕外折叠的布局查询与父区域二次遍历。
  • 普通语法颜色和 Java 语义颜色使用同一个视口缓存,按“普通语法 → Java 语义”的顺序补色。平衡区间索引保留跨行 token 及重叠 token 的覆盖顺序,并将写入范围裁剪到目标区间。
  • 文本编辑(包括等长替换)、整体文本替换及分析失败会丢弃旧语义快照;主题变化使已绘制颜色失效。外部文档同步、编辑器内全部替换、全选粘贴,以及缺少编辑范围信息的文本更新,都同步清除旧折叠范围和 gutter 标记,再着色新文本;新结构返回后只恢复仍有效的折叠选择。普通局部编辑保留原有折叠状态。
  • 合并并缓存有效折叠隐藏区间,使用二分查找判断隐藏字符和编辑区间相交,并从着色目标中扣除折叠内容。折叠区外的等长替换不会触发全文排版;区间位移及区间内编辑仍刷新属性,覆盖连续编辑后净位移归零的情况。元数据、回调与悬停状态仍正常更新。
  • 在协调器创建时初始化外观,使浅色模式首次 import 折叠就使用正确颜色。新结构返回、滚动及展开折叠时补齐可见区域颜色。
  • 保留后台全文 Java 结构分析、取消和过期结果校验。非 Java 文件直接结束 Java 折叠刷新,保留编辑行着色和已有缓存,避免延迟后额外重染整个视口。改动限于 macOS 编辑器呈现层。

滚动继续使用现有 AppKit 绘制循环及视口通知合并机制,没有新增高频 SwiftUI 状态写入或父页面重建。

验证

  • 新增 19 个确定性回归测试,覆盖区间索引、跨行/UTF-16、编辑与主题失效、折叠位移和嵌套边界,以及 1000 个折叠区域下的屏幕外布局查询。两轮 review 分别新增 5 个测试(11 个场景)和 3 个测试(6 个场景)。
  • 对照验证中临时回退 4 处关键修复,4 组对应测试均按预期失败,分别检测到旧范围隐藏新文本、额外颜色属性写入、错误的浅色首屏颜色和多余全文布局;对照完成后已恢复修复代码。
  • 第二轮 review 在修复前通过原生编辑入口复现全部替换、全选粘贴和缺少范围信息的更新仍保留旧折叠状态;3 个场景均触发失败断言。修复后全部通过,并确认折叠边界插入文字及普通局部编辑保持正确行为。
  • 最终 197 个相关测试、21 个套件通过,执行约 2.469 秒,无耗时警告;HTML/JUnit/逐项耗时报告已生成并检查。本轮新增测试耗时分别为 24、5、1 毫秒。
  • 移除临时探针后的 Release 构建、测试稳定性检查、服务边界检查及 git diff --check 通过。
  • 此前提交 6709b9a9 的完整 Release 应用使用 8329 行、447 KiB 合成 Java 文件完成三轮修复复测,包含真实 JDT LS 开启组;15 次可见颜色检查、4644 个 token 比较均匹配。本轮 review 修复通过上述回归测试和 Release 构建验证,未重复该性能矩阵。具体计时、对照超时及测量口径见上方 issue 评论。
./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh
./scripts/verify-service-boundaries.sh
git diff --check
./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh \
  --report .artifacts/test-stability/issue-561-review2-final.json -- \
  --filter 'Editor|TextViewport|SyntaxHighlight|highlightedRange'
./scripts/build-macos.sh --configuration release --triple arm64-apple-macosx

本地使用 macOS 27.0 / Apple Silicon / Swift 6.3.3,仓库要求的 Swift 6.2 工具链链接失效。未取得原业务源码,也未搭建原报告中的多模块 Maven 工程;Windows 未变更。完整验证限制已记录在 issue 评论中。测试应用、子进程和本次创建的 JDT LS 缓存均已清理。

@xiaoyumuxi xiaoyumuxi linked an issue Sep 11, 2026 that may be closed by this pull request
2 tasks done
@xiaoyumuxi xiaoyumuxi added the area:performance Performance / 性能 label Sep 11, 2026
@xiaoyumuxi

Copy link
Copy Markdown
Collaborator Author

[P1/P2] 整体文本替换后,旧 fold state 会继续作用在新文本上,直到异步 Java structure 返回。
updateNSView 里直接替换 textView.string 后,只重置了 syntax cache,然后立刻 highlight(),最后才异步 scheduleFoldRefresh();这里没有同步清掉 foldRegions / collapsedFoldIDs / CodeTextView.collapsedRanges。
因为 layout delegate、unfoldedRanges() 和 gutter 都已经改成直接依赖 collapsedRanges,如果新文本在旧 fold offset 上刚好还有字符,这些无关的新内容会在解析完成前被错误折叠/跳过着色。大型 Java 文件的 structure 本身可能需要明显时间,所以不是只有 80ms 的窗口。

建议 review 文案:

On a full-text replacement we keep the previous collapsed ranges active until the async structure refresh completes. rebuildLineIndex() only marks fold attributes dirty; it does not clear collapsedRanges, so the new document can temporarily hide unrelated text and highlight() also excludes those stale ranges. Could we synchronously clear the old fold state before highlighting the replacement, then restore folds when the new structure arrives? A regression test should check that no old range remains hidden immediately after replacing the whole document.

[P2] 非 Java 文件现在每次编辑都会在 80ms 后额外重染一次整个 viewport。
textDidChange 一直都会调用 scheduleFoldRefresh();非 Java 会走其中的 else -> clearJavaStructure()。
但这个 PR 把 clearJavaStructure() 改成了无条件 invalidateText() + highlight()。
结果 Swift/JSON/Markdown 等文件先按编辑行刷新一次,80ms 后 cache 又被全部清空,当前 viewport 再完整 lexical repaint 一次。这和 PR 描述里的“保留非 Java 文件的编辑行刷新行为”不一致。

建议 review 文案:

scheduleFoldRefresh() also runs for non-Java edits, and its non-Java branch calls clearJavaStructure(). Now that clearJavaStructure() unconditionally invalidates syntaxHighlightState and calls highlight(), every non-Java keystroke gets a second viewport-wide syntax repaint ~80 ms later. Previously this path was effectively a no-op when there was no Java state. Can we keep the syntax invalidation/repaint Java-only here so non-Java files retain the existing edited-line refresh behavior?

[P2] foldAttributesNeedRefresh 判定太宽,会把“fold geometry 没变”的等长编辑重新送回全文 layout。
当前只要编辑位置在“最后一个 collapsed range 的末尾之前”,就设成 true。它并没有判断编辑是否真的碰到了任何折叠区间。
因此例如底部折叠了一个 method,然后在文件前部做一次等长替换,所有 fold range 都完全没变,但结构结果回来后仍会走 invalidateLayout(fullRange) + ensureLayout(full container)。这正好重新触发本 PR 想消掉的昂贵路径。

建议 review 文案:

This marks fold attributes dirty for any edit before the end of the last collapsed range, even when the edit is a same-length replacement completely outside every fold. In that case the parser can return identical fold geometry, but foldAttributesNeedRefresh still forces the full-document invalidateLayout/ensureLayout path. Could this be limited to edits that actually intersect a collapsed range (and let geometryChanged handle shifted ranges)? A same-length edit before a collapsed method would be a useful regression case.

[P2] 初次打开带 import fold 的 Java 文件时,light mode 可能先被缓存成 dark syntax colors。
Coordinator.isDarkAppearance 默认是 true。
makeNSView 会先 primeJavaImportFold(...),之后才把 coordinator 的 isDarkAppearance 设成当前 palette。
而这个 PR 新增了 applyFoldState() -> highlight()。
所以 light mode 下,initial import fold 会先用默认 isDarkAppearance = true 着色并写入 paintedRanges;紧接着设成 light 后的正常 highlight() 可能因为 cache 已覆盖而什么也不做。要等 Java structure 返回并再次 invalidate cache 后才纠正。

建议 review 文案:

primeJavaImportFold() runs before Coordinator.isDarkAppearance is initialized from the current palette, but applyFoldState() now calls highlight(). Since the coordinator defaults to dark appearance, opening a Java file with imports in light mode can cache the initial viewport using dark-theme syntax colors; the following correctly configured highlight() then sees those ranges as already painted. Could we initialize the coordinator appearance before priming the fold, or avoid highlighting from that pre-appearance path?

我另外把几个容易误报的点排除了:semantic interval index 的查询/producer precedence 没发现 correctness 问题;Core 生成的 Java fold hidden range 确实从 opening line 之后开始,所以新 gutter 用行首检查隐藏状态这点可以成立。 Core 对 imports、comments 和 brace folds 都是按这个契约生成的。

@1lck
1lck merged commit b770aa2 into preview Sep 11, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:performance Performance / 性能

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 大的Java文件打开编辑器卡顿

2 participants