diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ff448..48d397d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ### Added +- Diff 滚动条总览标尺:diff 增 / 改 / 删与「有评论的行」投影到滚动条旁的总览标尺(编辑模式风格,按 1/3 分道、中间留白,不启用 minimap),拖动滚动条即可快速定位变更与评论位置。增 / 改按行高显示;并排视图删除在左侧 original 编辑器标尺按行高标红,统一(inline)视图下删除行无 model 行号、以删除点标记。 + - Diff 标签支持按「变更范围」查看:文件树头部「 个文件」补充范围信息,变为「 个文件 · 全部变更」或「 个文件 · 」,整体可点击弹出下拉,选择查看「全部变更(PR base..head)」或某个 commit 的变更(该 commit 的 `parent..sha`)。提交 / 活动标签页点击 commit 不再跳浏览器,而是切到 Diff 标签本地渲染该 commit 的变更。commit 视图为只读 diff(行内评论 / 草稿锚定在 PR 全量 diff 行号上,不套用于单 commit)。`diff:listChangedFiles` / `getFileContent` / `getBlame` 增加可选 base/head 范围参数。 - Diff 支持给「删除行」新增行内评论 / 草稿:此前 hover「+」只挂在 head 侧(新增行),现并排视图下 base 侧(删除 / 上下文行)也可 hover「+」创建,锚定 `side: 'old'`(发布映射 Bitbucket `lineType: removed / fileType: FROM`)。统一(inline)视图下删除行以 view zone 呈现、无可 hover 行号,仍需切并排视图创建。 @@ -27,10 +29,14 @@ - 超大组件按「容器 + 领域组件 + hooks + 工具方法」分层拆分:ChatPane、SettingsModal、MainPane、StatusBar - 业务逻辑下沉所属领域:PR 列表 / 详情 / 工作区归 `features/pr`;App 主入口退化为组合根,启动 / 布局 / 更新提示等拆成 app 级 hooks - 抽出通用基础组件 `Modal` / `StatusChip`;状态栏 chip 按归属下沉到各 feature + - DiffView 退化为组合根:数据流(变更文件 / 内容 / 评论 / blame / 范围 / 跳转)拆成 hooks,行内 view-zone 装配抽象为通用 `mountInlineZones`,行内评论渲染独立成域;评论渲染原语(`useCommentThread` / `CommentMarkdown`)与「活动」标签页共用 + - DiffSearchPanel / DraftZone / ChatInputBar 三个单体组件拆分:搜索算法、read/edit/publish 状态机、命令解析 / 输入状态机各抽为 util / hook,组件退化为瘦渲染 + - `components/common` 收敛 `index` barrel,跨域 import 统一走 barrel - 其它整理:目录归并、工具方法去重、main 进程 splash 拆分 ### Fixed +- Monaco 控制台噪音报错治理:只读 diff + 着色用不到的 typescript/javascript · json · css · html 语言服务从源头关闭(对各 `*Defaults` 传空 `ModeConfiguration`,不注册任何 provider),消除其向未注册 worker 发 RPC 抛出的 `Missing requestHandler or method: …`(`getNavigationTree` / `getSyntacticDiagnostics` 等整族);着色走 tokenizer 不受影响。剩余 Monaco 上游已知竞态(`TextModel got disposed before DiffEditorWidget model got reset`)作为已知问题默认静默,需诊断时 `localStorage.setItem('meebox.monacoDebug','1')` 再刷新可看明细——仅命中白名单消息,其它异常照常抛出。 - PR 头部与详情页的评审状态 chip(pending / approved / needs_work、reviewer 的 approved / needs work / pending)此前为写死英文,现按界面语言出国际化文案(新增 `prStatus` 文案集,四语言)。 - 切换不同 PR 时 diff 文件树「左栏空白 → 文件树整体弹出」的抖动:DiffView 改为 stale-while-loading——引入 `loadedPrId` 标记当前已渲染内容所属 PR,切 PR 期间保留旧树 / 旧内容渲染、上盖加载遮罩(延迟 150ms,命中缓存的快切换直接换新),并门控 content / comments / blame 拉取(避免「新 localId + 旧选中文件」错拉),新文件列表 ready 后整体替换。 - diff 文件树首次加载时文件名被图标渲染推移的抖动:图标改用固定 16px 占位槽包裹,iconify 的 svg 晚一帧进 DOM 也不塌缩,文件名位置稳定。 diff --git a/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/DiffPane.tsx b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/DiffPane.tsx index a1edea3..cf775f7 100644 --- a/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/DiffPane.tsx +++ b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/DiffPane.tsx @@ -47,7 +47,13 @@ export function DiffPane({ minimap: { enabled: false }, fontSize, scrollBeyondLastLine: false, + // 关掉 diff 专属的合并总览列(renderOverviewRuler=true 会在两侧滚动条之外再加一条宽列, + // 跟 VS Code 编辑模式「滚动条内打标」不一致)。改走编辑模式效果:内层 modified 编辑器自带的 + // overview ruler(默认渲染、独立于 minimap)+ 行内评论装饰的 overviewRuler 投影(见 useCommentZones)。 renderOverviewRuler: false, + // 显式 3 道:让 overview ruler 按 1/3 分道(diff 占左道、评论占右道,各 1/3 宽), + // 避免被按 2 道算成各占一半,色条更细。 + overviewRulerLanes: 3, // 显式开 glyph margin,给行内评论标记留位置 glyphMargin: true, // 空白字符可视化:toolbar 按钮控制;'all' 时空格显示 · / Tab 显示 → diff --git a/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/DiffView.tsx b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/DiffView.tsx index 36ed916..20e5661 100644 --- a/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/DiffView.tsx +++ b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/DiffView.tsx @@ -20,6 +20,7 @@ import { useCommentZones, useDiffComments, useDiffNav, + useDiffOverviewMarks, useDiffScope, useDraftAutoEdit, useDraftZones, @@ -176,6 +177,8 @@ export function DiffView({ return m; }, [files, drafts]); + // diff 增/删/改投影到滚动条总览标尺左道(编辑模式风格;评论锚点走右道,见下) + useDiffOverviewMarks({ diffEditor, content, selected, renderSideBySide }); // 行内评论标记 + view zone useCommentZones({ diffEditor, diff --git a/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/index.ts b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/index.ts index fd000a4..a853ed4 100644 --- a/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/index.ts +++ b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/index.ts @@ -10,5 +10,6 @@ export { useBlame, type BlameState } from './useBlame'; export { useDraftAutoEdit, type DraftAutoEdit } from './useDraftAutoEdit'; export { useDiffNav, type PendingNav, type PendingScroll } from './useDiffNav'; export { useCommentZones } from './useCommentZones'; +export { useDiffOverviewMarks } from './useDiffOverviewMarks'; export { useDraftZones } from './useDraftZones'; export { useLineCommentAdder } from './useLineCommentAdder'; diff --git a/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/useCommentZones.tsx b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/useCommentZones.tsx index 28843b1..f8a9dd8 100644 --- a/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/useCommentZones.tsx +++ b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/useCommentZones.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { type editor as MonacoEditor } from 'monaco-editor'; +import { editor as MonacoEditorNs, type editor as MonacoEditor } from 'monaco-editor'; import type { PrComment } from '@meebox/shared'; import type { DiffChangedFile } from '@meebox/ipc'; import { @@ -67,6 +67,12 @@ export function useCommentZones(opts: { glyphMarginClassName: 'monaco-comment-glyph', glyphMarginHoverMessage: { value: renderHoverMd(cs) }, linesDecorationsClassName: 'monaco-comment-line-deco', + // 评论锚点行在滚动条总览标尺投一个蓝色刻度(与评论 glyph 同色系), + // 用户拖滚动条一眼可见「哪里有评论」;minimap 仍关闭。 + overviewRuler: { + color: '#3794ff', + position: MonacoEditorNs.OverviewRulerLane.Right, + }, }, })); diff --git a/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/useDiffOverviewMarks.ts b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/useDiffOverviewMarks.ts new file mode 100644 index 0000000..da03f6e --- /dev/null +++ b/apps/desktop/src/renderer/src/components/features/pr/tabs/diff/hooks/useDiffOverviewMarks.ts @@ -0,0 +1,90 @@ +import { useEffect } from 'react'; +import { editor as MonacoEditorNs, type editor as MonacoEditor } from 'monaco-editor'; +import type { DiffChangedFile } from '@meebox/ipc'; +import type { LoadedContent } from '../diff-types'; + +// 增/改绿、删红(与 GitHub diff 配色同系)。diff 标记走 overview ruler 的 Left 道, +// 跟评论锚点(Right 道,见 useCommentZones)分列,互不遮挡。 +const ADDED_COLOR = '#3fb950'; +const REMOVED_COLOR = '#f85149'; + +/** + * 把 diff 增/删/改投影到内层编辑器自带的 overview ruler(编辑模式风格,单条滚动条标尺), + * 替代 diff 专属的 renderOverviewRuler 宽列(那条在并排视图会多出独立列,见 DiffPane)。 + * + * - modified 编辑器:增 / 改行绿色;纯删在删除点打一条红 tick + * - 并排视图下 original 编辑器:删 / 改行红色 + * + * diff 异步算完后 getLineChanges() 才有值;首帧已算完直接刷,否则等 onDidUpdateDiff。 + */ +export function useDiffOverviewMarks(opts: { + diffEditor: MonacoEditor.IStandaloneDiffEditor | null; + content: LoadedContent | null; + selected: DiffChangedFile | null; + renderSideBySide: boolean; +}): void { + const { diffEditor, content, selected, renderSideBySide } = opts; + useEffect(() => { + if (!diffEditor || !content || !selected) return; + const modifiedEditor = diffEditor.getModifiedEditor(); + const originalEditor = diffEditor.getOriginalEditor(); + const modCol = modifiedEditor.createDecorationsCollection([]); + const origCol = originalEditor.createDecorationsCollection([]); + + const Lane = MonacoEditorNs.OverviewRulerLane; + const deco = ( + startLine: number, + endLine: number, + color: string, + position: number, + ): MonacoEditor.IModelDeltaDecoration => ({ + range: { startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: 1 }, + options: { overviewRuler: { color, position } }, + }); + + const refresh = (): void => { + const changes = diffEditor.getLineChanges() ?? []; + const modDecos: MonacoEditor.IModelDeltaDecoration[] = []; + const origDecos: MonacoEditor.IModelDeltaDecoration[] = []; + for (const c of changes) { + const isInsert = c.originalEndLineNumber === 0; // 纯增(原始侧无行) + const isDelete = c.modifiedEndLineNumber === 0; // 纯删(修改侧无行) + // 增 / 改:modified 侧 modifiedStart..End 标绿(左道) + if (!isDelete) { + modDecos.push( + deco(c.modifiedStartLineNumber, c.modifiedEndLineNumber, ADDED_COLOR, Lane.Left), + ); + } + // 删 / 改的「移除部分」标红: + if (!isInsert) { + if (renderSideBySide) { + // 并排:红画在左侧 original 编辑器自带 ruler(左道),与右侧绿互不干扰 + origDecos.push( + deco(c.originalStartLineNumber, c.originalEndLineNumber, REMOVED_COLOR, Lane.Left), + ); + } else { + // 统一视图:original 编辑器不可见,红 tick 与绿同画在左道(同一条 diff 泳道)。 + // 被删行在 unified 下是 view zone(无 model 行号),只能标在删除点 modifiedStartLineNumber; + // 改块同一行既绿又红时绿覆盖红 → 改块呈绿、纯删那行(无绿)呈红。 + const line = Math.max(1, c.modifiedStartLineNumber); + modDecos.push(deco(line, line, REMOVED_COLOR, Lane.Left)); + } + } + } + modCol.set(modDecos); + origCol.set(origDecos); + }; + + if (diffEditor.getLineChanges() != null) refresh(); + const disp = diffEditor.onDidUpdateDiff(refresh); + return () => { + disp.dispose(); + try { + modCol.clear(); + origCol.clear(); + } catch { + /* editor disposed */ + } + }; + }, [diffEditor, content, selected, renderSideBySide]); +} diff --git a/apps/desktop/src/renderer/src/lib/monaco-setup.ts b/apps/desktop/src/renderer/src/lib/monaco-setup.ts index 006351e..605793e 100644 --- a/apps/desktop/src/renderer/src/lib/monaco-setup.ts +++ b/apps/desktop/src/renderer/src/lib/monaco-setup.ts @@ -7,6 +7,14 @@ import * as monaco from 'monaco-editor'; import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import { loader } from '@monaco-editor/react'; +// 4 个「带 worker 后端语言服务」的 contribution 子模块(editor.main 已加载,这里再引一次只为 +// 取其具名导出的 *Defaults;ES module 单例不会重复执行)。它们的运行期 JS 具名导出 +// typescriptDefaults / jsonDefaults / cssDefaults … 但 .d.ts 误为 `export {}`(monaco 0.55 ESM +// 打包缺陷),故按 namespace 引入、下方经 unknown 转成已知形状取用,不引入 any。 +import * as tsLang from 'monaco-editor/esm/vs/language/typescript/monaco.contribution.js'; +import * as jsonLang from 'monaco-editor/esm/vs/language/json/monaco.contribution.js'; +import * as cssLang from 'monaco-editor/esm/vs/language/css/monaco.contribution.js'; +import * as htmlLang from 'monaco-editor/esm/vs/language/html/monaco.contribution.js'; // Vite 的 ?worker import 返回一个可 new 的 Worker 构造类。 self.MonacoEnvironment = { @@ -17,22 +25,61 @@ self.MonacoEnvironment = { loader.config({ monaco }); +/** + * 关掉 4 个「带 worker 后端语言服务」的语言族的全部特性:typescript/javascript(ts.worker)、 + * json(json.worker)、css/scss/less(css.worker)、html/handlebars/razor(html.worker)。 + * + * 本应用只用 Monaco 做只读 diff + 语法着色(着色走 monarch tokenizer,与语言服务无关),不需要 + * 补全 / 悬浮 / 符号 / 诊断 / 格式化等;而这些特性会向对应 *.worker 发 RPC(getNavigationTree / + * getSyntacticDiagnostics / doValidation …)。但我们只注册了 base editor.worker(见上 getWorker) + * → 这些方法找不到 handler,抛 `Missing requestHandler or method: …`。 + * + * 传空 ModeConfiguration(各字段均 optional,缺省即不注册对应 provider)→ 这些 provider 不再注册、 + * 不再发 RPC,从源头消除整族报错(下方 window 兜底退化为纯保险)。其余 80+ 语言是 monarch + * tokenizer 纯着色、无 worker 后端,不受影响也无需处理。 + */ +interface LangServiceDefaults { + // 传空 ModeConfiguration(各字段均 optional,缺省即不注册对应 provider)= 关掉全部特性 + setModeConfiguration(modeConfiguration: object): void; +} +// 运行期具名导出存在(见各 contribution.js 的 export),但其 .d.ts 误为 `export {}`,故经 +// unknown 转成已知 *Defaults 形状取用(不引入 any);名字缺失时 filter 兜底,未来 monaco 改名不崩。 +const defaultsOf = (mod: unknown, names: readonly string[]): LangServiceDefaults[] => + names + .map((n) => (mod as Record)[n]) + .filter((d): d is LangServiceDefaults => typeof d?.setModeConfiguration === 'function'); + +for (const d of [ + ...defaultsOf(tsLang, ['typescriptDefaults', 'javascriptDefaults']), + ...defaultsOf(jsonLang, ['jsonDefaults']), + ...defaultsOf(cssLang, ['cssDefaults', 'scssDefaults', 'lessDefaults']), + ...defaultsOf(htmlLang, ['htmlDefaults', 'handlebarDefaults', 'razorDefaults']), +]) { + d.setModeConfiguration({}); +} + /** * Monaco 在 DiffEditor 快速切换文件时,会出现 model 已 dispose 但 widget 还在 * 收尾的竞态;以及"内置 ts/js language contribution 主动询问 inlayHints / * quickInfo / navigationTree / ...,editor.worker 没实现对应方法"的一系列运行时 * 报错。两类都不影响渲染,但污染 console 让用户以为应用挂了。 * - * 仅按消息前缀黑名单吞掉,不动其他真实业务错误,也不动 Monaco 各 language - * 默认配置(保持 vendor 默认行为,未来换 monaco 版本时不用维护补丁)。 + * 仅按消息前缀黑名单吞掉,不动其他真实业务错误。 * - * `Missing requestHandler or method:` 整族都源于同一机制(Monaco 给 worker - * 发 RPC 找不到 handler),用前缀一勺烩;不一一列出 getQuickInfoAtPosition / - * getCompletionsAtPosition / provideInlayHints 这种 whack-a-mole 名单。 + * `Missing requestHandler or method:` 整族源于 worker RPC 找不到 handler(已在上方从源头 + * 关闭对应语言服务,这里留作双保险);`TextModel got disposed …` 是 DiffEditor 切换 model + * 的 dispose 竞态。两类都是 Monaco 上游已知问题、不影响渲染、应用侧无法根除 → **默认静默忽略** + * (作为已知问题)。需诊断时在 devtools 执行 `localStorage.setItem('meebox.monacoDebug','1')` + * 再刷新,即可看到被吞的报错明细。 */ -const BENIGN_MONACO_ERROR_PREFIXES = [ - 'Missing requestHandler or method:', -]; +const MONACO_DEBUG = (() => { + try { + return localStorage.getItem('meebox.monacoDebug') === '1'; + } catch { + return false; + } +})(); +const BENIGN_MONACO_ERROR_PREFIXES = ['Missing requestHandler or method:']; const BENIGN_MONACO_ERROR_SUBSTRINGS = [ 'TextModel got disposed before DiffEditorWidget model got reset', ]; @@ -47,8 +94,8 @@ function isBenignMonacoError(msg: unknown): boolean { window.addEventListener('error', (e) => { if (isBenignMonacoError(e.message) || isBenignMonacoError(e.error?.message)) { e.preventDefault(); - // 留一行 console.warn 便于诊断,避免完全静默 - console.warn('[monaco] suppressed benign error:', e.message); + // 默认静默(已知问题);开 meebox.monacoDebug 才打一行便于诊断 + if (MONACO_DEBUG) console.warn('[monaco] suppressed benign error:', e.message); } }); @@ -57,6 +104,6 @@ window.addEventListener('unhandledrejection', (e) => { const msg = typeof reason === 'string' ? reason : (reason as { message?: unknown })?.message; if (isBenignMonacoError(msg)) { e.preventDefault(); - console.warn('[monaco] suppressed benign rejection:', msg); + if (MONACO_DEBUG) console.warn('[monaco] suppressed benign rejection:', msg); } });