Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

### Added

- Diff 滚动条总览标尺:diff 增 / 改 / 删与「有评论的行」投影到滚动条旁的总览标尺(编辑模式风格,按 1/3 分道、中间留白,不启用 minimap),拖动滚动条即可快速定位变更与评论位置。增 / 改按行高显示;并排视图删除在左侧 original 编辑器标尺按行高标红,统一(inline)视图下删除行无 model 行号、以删除点标记。

- Diff 标签支持按「变更范围」查看:文件树头部「<n> 个文件」补充范围信息,变为「<n> 个文件 · 全部变更」或「<n> 个文件 · <commit>」,整体可点击弹出下拉,选择查看「全部变更(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 行号,仍需切并排视图创建。
Expand All @@ -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 也不塌缩,文件名位置稳定。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 显示 →
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
useCommentZones,
useDiffComments,
useDiffNav,
useDiffOverviewMarks,
useDiffScope,
useDraftAutoEdit,
useDraftZones,
Expand Down Expand Up @@ -176,6 +177,8 @@ export function DiffView({
return m;
}, [files, drafts]);

// diff 增/删/改投影到滚动条总览标尺左道(编辑模式风格;评论锚点走右道,见下)
useDiffOverviewMarks({ diffEditor, content, selected, renderSideBySide });
// 行内评论标记 + view zone
useCommentZones({
diffEditor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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,
},
},
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -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]);
}
69 changes: 58 additions & 11 deletions apps/desktop/src/renderer/src/lib/monaco-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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<string, LangServiceDefaults | undefined>)[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',
];
Expand All @@ -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);
}
});

Expand All @@ -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);
}
});
Loading