monaco-editor-merge-conflict
v0.2.1
Published
VS Code-like inline merge conflict actions and preview for Monaco Editor
Maintainers
Readme
English | 简体中文
monaco-editor-merge-conflict
基于 monaco-editor >= 0.55.1 的 VS Code 风格 Git 冲突预览、处理与截图包。
示例截图
JavaScript 冲突

Vue 3 SFC 冲突


能力
- 普通 Git 两方冲突与 diff3
||||||| base冲突解析; - 当前/传入区块着色、接受当前、接受传入、接受双方、Diff 比较;
- VS Code 风格文件路径头;
- 每个冲突可传入 HEAD/Incoming 的 ref、作者、commit、提交时间和 message;提交详情仅显示在冲突标记行,不在文件头重复展示;
- 提交信息直接显示在
HEAD(当前更改 · 用户 · message · YYYY-MM-DD HH:mm:ss · commit)标记行; - 长 commit message 使用两侧独立弹性布局和省略号保护编辑器宽度,悬停可查看完整信息;
- 从浏览器
File、Blob、File-like 对象或{ content, filePath }载入; - 文件级 Source/Target
add | modify | delete | rename冲突建模与状态标识; - 每个冲突分别截图,或多个冲突合成一张长图;
- 截图可配置冲突上方/下方代码行数,不受当前编辑器 viewport 高度限制;
.vue文件识别及轻量 Vue SFC tokenizer;- 不依赖 DOM 的 JSON 分析与纯文本解决 API,适合 Skill、Agent、CLI 和服务端编排;
- npm Browser ESM、ESM、CommonJS 和 IIFE/CDN 构建产物。
monaco-editor npm 包不包含 VS Code Workbench 的 Merge Conflict 扩展。本包只组合 Monaco 0.55.1 的公开 Decorations、View Zones、executeEdits 和 Diff Editor API,不引用 esm/vs/... 私有模块。
安装与开发
npm install monaco-editor monaco-editor-merge-conflict本仓库本地运行:
npm install
npm run dev
npm run typecheck
npm test
npm run build文件预览
import * as monaco from 'monaco-editor';
import {
createMergeConflictFileEditor,
loadConflictFile,
zhCNMessages,
} from 'monaco-editor-merge-conflict/browser';
import 'monaco-editor-merge-conflict/style.css';
const source = await loadConflictFile(fileInput.files![0], {
// 浏览器不会暴露用户机器上的完整路径,路径由业务显式传入。
filePath: 'packages/web/src/App.vue',
});
const getConflictMetadata = (_conflict, index) => ({
current: {
ref: 'HEAD',
author: 'Alice Chen',
commit: '86f5a62',
committedAt: '2026-08-26T18:42:13+08:00',
message: 'fix(editor): preserve the current implementation',
},
incoming: {
ref: index === 0 ? 'feature/login' : 'feature/layout',
author: 'Bob Lin',
commit: '516f350',
committedAt: '2026-08-27T09:16:48+08:00',
message: 'feat(editor): add the incoming implementation',
},
});
const fileEditor = createMergeConflictFileEditor(monaco, container, {
source,
// 默认就是 1920 × 1080;这里可以按宿主布局覆盖。
previewSize: { width: 1920, height: 1080 },
editorOptions: { theme: 'vs-dark', minimap: { enabled: false } },
controllerOptions: {
messages: zhCNMessages,
getConflictMetadata,
// 默认最多显示 50 个 Unicode 字符;hover 中仍保留完整 message。
commitMessageMaxLength: 50,
},
});
fileEditor.dispose();如果已经自行创建 Monaco Editor,也可以只挂载底层 controller。默认“比较更改”打开内置 Monaco Diff Editor;业务已有抽屉/路由时可以通过 onCompare 接管。
CLI
安装包后可以使用 monaco-merge-conflict,也可以直接通过 npx 调用:
# 输出适合 Agent/Skill 消费的 JSON
npx monaco-merge-conflict analyze src/App.vue
# 文件包含完整冲突块时退出码为 1,否则为 0
npx monaco-merge-conflict check src/App.vue
# 解决第 0 个冲突并输出到 stdout
npx monaco-merge-conflict resolve src/App.vue \
--conflict 0 \
--resolution incoming
# 写到新文件;使用 --write 可以原地修改
npx monaco-merge-conflict resolve src/App.vue \
--conflict 0 \
--resolution both \
--output src/App.resolved.vue三个命令都支持用 - 从 stdin 读取。resolve 默认不会覆盖输入文件,只有显式传入 --write 才会原地写入。
文件级冲突
文本区块冲突之外,可以从两个文件状态构造预览:
import {
createConflictFileFromSides,
getFileConflictKind,
} from 'monaco-editor-merge-conflict';
const source = createConflictFileFromSides({
filePath: 'services/health/check.go',
source: {
change: 'add',
ref: 'HEAD',
content: 'package health\n\nfunc Check() bool { return true }',
},
target: {
change: 'delete',
ref: 'feature/remove-health-check',
},
});
getFileConflictKind(source.fileConflict!); // add/delete支持 add/add、add/delete、delete/modify、modify/delete、rename/rename 等组合。Delete 一侧内容为空,因此接受 Delete 侧会得到空文件。delete/delete 没有可预览内容,构造函数会拒绝该组合。文件级状态保留在结构化数据 API 中,不在文件头或截图中重复展示。
发布后的截图使用
截图依赖 Monaco、DOM 和 Canvas,因此需要在浏览器应用中调用。当前 Node.js CLI 提供 analyze、check 和 resolve,不能直接渲染 PNG。
下面是完整的浏览器 ESM 示例:从用户上传的冲突文件生成截图,并下载每个冲突区块对应的 PNG。
<input id="conflict-file" type="file" />import * as monaco from 'monaco-editor';
import {
captureMergeConflictScreenshots,
loadConflictFile,
zhCNMessages,
} from 'monaco-editor-merge-conflict/browser';
import 'monaco-editor-merge-conflict/style.css';
async function captureUploadedFile(file: File) {
const source = await loadConflictFile(file, {
// 浏览器不会暴露用户机器上的绝对路径,需要由业务主动传入。
filePath: `packages/cli/src/${file.name}`,
});
return captureMergeConflictScreenshots(monaco, {
source,
// separate:每个冲突一张图;combined:所有冲突合成一张长图。
mode: 'separate',
contextBefore: 8,
contextAfter: 12,
// 不传 width 时,按照最长可见内容自适应宽度。
minWidth: 480,
maxWidth: 1920,
pixelRatio: 2,
controllerOptions: {
messages: zhCNMessages,
commitMessageMaxLength: 50,
getConflictMetadata: (_conflict, index) => ({
current: {
ref: 'HEAD',
author: 'Alice Chen',
commit: '86f5a62',
committedAt: '2026-08-26T18:42:13+08:00',
message: 'fix(cli): preserve custom welcome messages',
},
incoming: {
ref: index === 0 ? 'feature/version' : 'feature/profile',
author: 'Bob Lin',
commit: '516f350',
committedAt: '2026-08-27T09:16:48+08:00',
message: 'feat(cli): print version and usage details',
},
}),
},
});
}
const input = document.querySelector<HTMLInputElement>('#conflict-file');
const file = input?.files?.[0];
const screenshots = file ? await captureUploadedFile(file) : [];
screenshots.forEach((screenshot, index) => {
const url = URL.createObjectURL(screenshot.blob);
const link = document.createElement('a');
link.href = url;
link.download = `conflict-${index + 1}.png`;
link.click();
URL.revokeObjectURL(url);
});如果只需要在页面上展示截图,可以直接使用返回结果中的 dataUrl:
const firstScreenshot = screenshots[0];
if (firstScreenshot) {
const image = new Image();
image.src = firstScreenshot.dataUrl;
document.body.append(image);
}如果已经持有冲突文件的字符串内容,不需要先调用 loadConflictFile。使用 combined 可以把多个冲突区块生成一张长图:
const [longScreenshot] = await captureMergeConflictScreenshots(monaco, {
source: {
filePath: 'packages/cli/src/base.js',
languageId: 'javascript',
content: conflictContent,
},
mode: 'combined',
contextBefore: 5,
contextAfter: 5,
});每个截图结果包含 blob、dataUrl、filePath、conflictIndexes、startLineNumber、endLineNumber、width 和 height。不传 width 时,截图函数会测量 Monaco 渲染行、操作条和文件路径,以最长可见内容决定宽度;传入 width 可固定宽度,也可用 minWidth/maxWidth 限制自适应范围。离屏编辑器会扩展到完整内容高度,所以大冲突不会被当前 viewport 裁切。最终 PNG 最大尺寸仍受浏览器 Canvas 实现上限约束;超大文件建议使用 mode: 'separate'。
批量任务复用一个 Editor
处理几十或上百个文件时,可以持有一个 renderer,依次等待每个文件截图完成。renderer 只创建一个 Monaco Editor,每次通过 setModel 切换新模型,并在继续前释放上一个 controller 和 model:
import * as monaco from 'monaco-editor';
import {
createMergeConflictScreenshotRenderer,
} from 'monaco-editor-merge-conflict/browser';
const renderer = createMergeConflictScreenshotRenderer(monaco, {
mode: 'separate',
contextBefore: 3,
contextAfter: 3,
pixelRatio: 1,
maxWidth: 1280,
});
try {
for (const source of conflictFiles) {
const screenshots = await renderer.capture({ source });
await saveImmediately(source, screenshots);
}
} finally {
renderer.dispose();
}不要对同一个 renderer 使用 Promise.all:一个 Editor 同一时刻只能展示一个 model,因此 API 会主动拒绝并发截图。建议每次拿到结果就立即写入文件并释放引用,不要在内存中长期保存全部 dataUrl。
无头浏览器示例
Playwright
examples/playwright-screenshot 是一个独立项目:从 npm 安装本包,通过 Playwright 启动无头 Chromium,载入存在冲突的 TypeScript 文件,并把 merge-conflict.png 导出到示例目录。
cd examples/playwright-screenshot
npm install
npx playwright install chromium
npm run screenshotPuppeteer
examples/puppeteer-screenshot 从 npm 安装已发布包,通过 Puppeteer 启动无头 Chrome,并在示例目录导出自己的 merge-conflict.png。
cd examples/puppeteer-screenshot
npm install
npm run screenshot100 文件批量案例
examples/playwright-batch-100 会生成 100 个冲突文件,只启动一个浏览器页面,并复用一个 Monaco Editor 导出全部 PNG。
cd examples/playwright-batch-100
npm install
npx playwright install chromium
npm run screenshot程序化与自动化调用
纯函数不需要 Monaco、DOM 或 worker,输入输出可 JSON 序列化:
import {
analyzeMergeConflictText,
resolveMergeConflictText,
} from 'monaco-editor-merge-conflict';
const analysis = analyzeMergeConflictText({
content,
filePath: 'src/App.vue',
getConflictMetadata,
});
// analysis.schemaVersion === 1
// analysis.conflicts[n].current / ancestor / incoming
const result = resolveMergeConflictText({
content,
conflict: 0, // 或 analysis.conflicts[0].id
resolution: 'both',
});
console.log(result.content, result.remainingConflictCount);推荐 Agent/Skill 流程:先调用 analyzeMergeConflictText,把结构化区块交给模型决策,再以冲突 index/id 调用 resolveMergeConflictText;不要让模型直接计算行偏移。
Vue
loadConflictFile 会把 .vue 映射为 vue。createMergeConflictFileEditor 和截图函数会自动调用 registerVueLanguage,提供 template、script、style 的基础着色。完整 Vue 诊断、跳转和补全不属于本包,应由宿主接入 Volar。
Demo 的 demo/examples 提供 Go、YAML、Vue 2 Options API、Vue 3 script setup、JavaScript 和 TypeScript 冲突文件,其中 JavaScript 示例包含两个冲突区块,另有一个 Current 60 行 + Incoming 60 行的 YAML 长冲突截图示例。下拉列表还覆盖 Add/Delete、Delete/Modify、Add/Add 和 Rename/Rename 文件级冲突。
源码模块
src/
├── core/ # 类型、解析器、格式化器、纯文本操作
├── editor/ # Controller、文件预览与 Diff 弹窗
├── files/ # File/Blob/文件路径与 language id
├── languages/ # Vue SFC language registration
├── screenshot/ # 多区块和长截图
├── styles/ # 编辑器、文件头、冲突装饰样式
└── index.ts # npm 公共入口发布入口
| 场景 | 入口 |
| --- | --- |
| Browser/打包器 ESM | dist/index.js 或包子路径 /browser |
| Node/打包器 CommonJS | dist/index.cjs |
| CDN/IIFE | dist/index.global.js,全局名 MonacoMergeConflict |
| 类型 | dist/index.d.ts |
| 样式 | monaco-editor-merge-conflict/style.css |
IIFE 仍由调用方把 Monaco namespace 作为第一个参数传入,不会重复打包 Monaco。
设计边界
- 仅操作同时具备
<<<<<<<、=======、>>>>>>>的完整块,避免误改残缺内容; - 冲突标记必须从行首开始且长度至少为 7;
- “接受双方更改”为 current 后 incoming,不进行 AST 或语义合并;
- 完整四窗格 Merge Editor(Base / Current / Incoming / Result)属于更高层产品形态,可复用本包解析和 resolve API 扩展。
