前言
最近收到一个需求,实现两个配置文件对比的能。一开始想着那简单直接用采用monaco的diffEditor组件就可以了。在开发的时候发现没这么简单,因为monaco内置的diffEditor只有两种状态新增、删除,但是我们产品需要我们存在三种状态新增、删除、更新
-
monaco默认的效果,行样式没有与我的样式保持一致,只存在两种状态

-
我需要实现效果,行样式保持一致,并且存在三种状态

需求分析
- 需要计算出
新增、删除、差异各占多少行,这里采用diffEidtor提供的getLineChanges方法获取所有行改动,然后分析数据 - 如何判断
新增行、删除行、差异行呢?(这里主要想明白,你的状态是跟着视图走的,左侧空行代表新增,右侧空行代表删除、两侧都存在代表更新,是不是一说就明白呢? 但是我之前还结合charChanges算了好久,后面发现根本就不需要)
css
1. 因为originalStartLineNumber和originalEndLineNumber为1,而modifiedStartLineNumber和modifiedEndLineNumber是1-2。那么表示第一行为更新状态、第二行为新增状态
2. 由于originalStartLineNumber和originalEndLineNumber为3,但是modifiedEndLineNumber为0,那么表示更新后被移除了,则第三行为删除状态
[ { "originalStartLineNumber": 1, "originalEndLineNumber": 1, "modifiedStartLineNumber": 1, "modifiedEndLineNumber": 2, "charChanges": [...]
},
{
"originalStartLineNumber": 3,
"originalEndLineNumber": 3,
"modifiedStartLineNumber": 3,
"modifiedEndLineNumber": 0
}
]
- 想明白
新增行、删除行、差异行的计算,那么我们就聚焦到这些行变化的颜色,其实也不算复杂,首先将默认行的背景色改为透明、然后我们再根据变更状态添加对应的行装饰器就可以实现我们需要的效果了
代码实现
- 设置diffEditor变化的背景色为透明
sass
// 覆盖Monaco Editor的默认diff样式
.monaco-diff-editor .line-insert {
background-color: transparent !important;
}
.monaco-diff-editor .line-delete {
background-color: transparent !important;
}
.monaco-editor .line-insert {
background-color: transparent !important;
}
.monaco-editor .line-delete {
background-color: transparent !important;
}
// 将整行的char-delete和line-delete背景设为透明,但保留字符级别的删除标记
.monaco-diff-editor .char-delete[style*='width:100%'] {
background-color: transparent !important;
}
.monaco-diff-editor .char-insert[style*='width:100%'] {
background-color: transparent !important;
}
// 简单的diff行样式 - 参考断点行的实现方式
.diff-line-added {
background-color: #44ca6240 !important;
}
.diff-line-deleted {
background-color: #f87d7c40 !important;
}
.diff-line-modified {
background-color: #ffad5d40 !important;
}
// 暗色主题
.monaco-editor.vs-dark {
// 覆盖暗色主题下的Monaco默认样式
.line-insert {
background-color: transparent !important;
}
.line-delete {
background-color: transparent !important;
}
.diff-line-added {
background-color: #44ca6260 !important;
}
.diff-line-deleted {
background-color: #f87d7c60 !important;
}
.diff-line-modified {
background-color: #ffad5d60 !important;
}
.char-delete[style*='width:100%'] {
background-color: transparent !important;
}
.char-insert[style*='width:100%'] {
background-color: transparent !important;
}
}
- 注册DiffEditor编辑器,主要关注的是onMount的处理
ts
<DiffEditor
width="900"
height="300"
language="javascript"
theme={
this.props.colorMode === ColorMode.Light
? 'vs-light'
: 'vs-dark'
}
original={leftTest}
modified={rightTest}
options={options}
onMount={this.editorDidMount}
{...config}
/>
- 当编辑器加载完成时,onDidUpdateDiff监听文本变化,然后执行applyCustomDiffDecorations
ts
editorDidMount(editor, monaco) {
this.diffEditor = editor
this.monaco = monaco
// 调用 onRef 回调,将当前组件实例传递给父组件
this.onRef(this)
// 防抖函数,避免频繁调用
let debounceTimer = null
// 监听差异更新事件
editor.onDidUpdateDiff(() => {
// 清除之前的定时器
if (debounceTimer) {
clearTimeout(debounceTimer)
}
// 设置新的定时器,延迟执行
debounceTimer = setTimeout(() => {
this.applyCustomDiffDecorations()
}, 100) // 100ms 防抖
})
}
- 基于monaco的[deltaDecorations]实现行装饰器,
stats就是新增、删除、差异的数据统计
ts
// 应用自定义diff装饰并计算差异统计
applyCustomDiffDecorations() {
if (!this.diffEditor || !this.monaco) return
const lineChanges = this.diffEditor.getLineChanges()
if (!lineChanges || lineChanges.length === 0) {
// 清除之前的装饰
if (this.originalDecorationIds) {
this.diffEditor
.getOriginalEditor()
.deltaDecorations(this.originalDecorationIds, [])
}
if (this.modifiedDecorationIds) {
this.diffEditor
.getModifiedEditor()
.deltaDecorations(this.modifiedDecorationIds, [])
}
// 重置差异统计
this.updateDiffStatsIfChanged({
additions: 0,
deletions: 0,
modifications: 0,
})
return
}
const originalEditor = this.diffEditor.getOriginalEditor()
const modifiedEditor = this.diffEditor.getModifiedEditor()
const originalDecorations = []
const modifiedDecorations = []
// 差异统计
const stats = {
additions: 0,
deletions: 0,
modifications: 0,
}
// 使用Map来记录每一行的变更类型,避免重复处理
const allOriginalLineTypes = new Map() // 左侧编辑器行类型
const allModifiedLineTypes = new Map() // 右侧编辑器行类型
lineChanges.forEach((change) => {
const originalStartLine = change.originalStartLineNumber
const originalEndLine = change.originalEndLineNumber
const modifiedStartLine = change.modifiedStartLineNumber
const modifiedEndLine = change.modifiedEndLineNumber
// 当前变更的行类型
const originalLineTypes = new Map() // 左侧编辑器行类型
const modifiedLineTypes = new Map() // 右侧编辑器行类型
// 根据用户提供的规则进行判断
if (originalEndLine === 0 && modifiedEndLine > 0) {
for (let i = modifiedStartLine; i <= modifiedEndLine; i++) {
modifiedLineTypes.set(i, 'added')
}
} else if (originalEndLine > 0 && modifiedEndLine === 0) {
for (let i = originalStartLine; i <= originalEndLine; i++) {
originalLineTypes.set(i, 'deleted')
}
} else if (originalEndLine > 0 && modifiedEndLine > 0) {
// 规则3: 两边都有行号,需要根据行数差异判断
const originalLines = originalEndLine - originalStartLine + 1
const modifiedLines = modifiedEndLine - modifiedStartLine + 1
if (originalLines === modifiedLines) {
// 行数相同,全部标记为修改
for (let i = originalStartLine; i <= originalEndLine; i++) {
originalLineTypes.set(i, 'modified')
}
for (let i = modifiedStartLine; i <= modifiedEndLine; i++) {
modifiedLineTypes.set(i, 'modified')
}
} else {
// 行数不同,按照用户规则处理
const minLines = Math.min(originalLines, modifiedLines)
if (originalLines > modifiedLines) {
// 左侧行数更多:对应行标记为修改,多出的左侧行标记为删除
for (let i = 0; i < minLines; i++) {
originalLineTypes.set(originalStartLine + i, 'modified')
modifiedLineTypes.set(modifiedStartLine + i, 'modified')
}
// 多出的左侧行标记为删除
for (let i = minLines; i < originalLines; i++) {
originalLineTypes.set(originalStartLine + i, 'deleted')
}
} else {
for (let i = 0; i < minLines; i++) {
originalLineTypes.set(originalStartLine + i, 'modified')
modifiedLineTypes.set(modifiedStartLine + i, 'modified')
}
// 多出的右侧行标记为新增
for (let i = minLines; i < modifiedLines; i++) {
modifiedLineTypes.set(modifiedStartLine + i, 'added')
}
}
}
}
// 统计各类型行数
const addedCount = Array.from(modifiedLineTypes.values()).filter(
(type) => type === 'added',
).length
const deletedCount = Array.from(originalLineTypes.values()).filter(
(type) => type === 'deleted',
).length
const modifiedCount = Math.max(
Array.from(originalLineTypes.values()).filter(
(type) => type === 'modified',
).length,
Array.from(modifiedLineTypes.values()).filter(
(type) => type === 'modified',
).length,
)
stats.additions += addedCount
stats.deletions += deletedCount
stats.modifications += modifiedCount
// 将当前变更的行类型合并到全局Map中
originalLineTypes.forEach((type, lineNumber) => {
allOriginalLineTypes.set(lineNumber, type)
})
modifiedLineTypes.forEach((type, lineNumber) => {
allModifiedLineTypes.set(lineNumber, type)
})
// 根据行类型添加装饰器
// 处理左侧编辑器
originalLineTypes.forEach((type, lineNumber) => {
if (type === 'deleted') {
// 删除行 - 添加红色背景装饰
originalDecorations.push({
range: new this.monaco.Range(lineNumber, 1, lineNumber, 1),
options: {
isWholeLine: true,
className: 'diff-line-deleted',
},
})
} else if (type === 'modified') {
// 修改行 - 添加橙色背景
originalDecorations.push({
range: new this.monaco.Range(lineNumber, 1, lineNumber, 1),
options: {
isWholeLine: true,
className: 'diff-line-modified',
},
})
}
})
// 处理右侧编辑器
modifiedLineTypes.forEach((type, lineNumber) => {
if (type === 'added') {
// 新增行 - 添加绿色背景装饰
modifiedDecorations.push({
range: new this.monaco.Range(lineNumber, 1, lineNumber, 1),
options: {
isWholeLine: true,
className: 'diff-line-added',
},
})
} else if (type === 'modified') {
// 修改行 - 添加橙色背景
modifiedDecorations.push({
range: new this.monaco.Range(lineNumber, 1, lineNumber, 1),
options: {
isWholeLine: true,
className: 'diff-line-modified',
},
})
}
})
})
// 更新差异统计
this.updateDiffStatsIfChanged(stats)
// 应用装饰并保存装饰ID以便后续清理
this.originalDecorationIds = originalEditor.deltaDecorations(
this.originalDecorationIds || [],
originalDecorations,
)
this.modifiedDecorationIds = modifiedEditor.deltaDecorations(
this.modifiedDecorationIds || [],
modifiedDecorations,
)
}
总结
这一节主要讲解了monaco的DiffEditor实现配置文件对比。在这一章我们也初步学习了Monaco的行装饰器的使用,其实编辑器的debugger模式,先基于DAP协议获取到当前debugger的堆栈聚焦行,然后我们在通过行装饰器绘制对应的高亮行。至于堆栈信息只需要绘制对应的堆栈面板接口,是不是感觉就特别清晰了
为什么写这篇文章呢?
- 是因为我没有找到相关文章,其他文章都是直接实现
DiffEditor效果,并不满足需要的三种状态新增、删除、差异。 - 在研发任务排期紧张的时候帮助遇到相同需求的小伙伴减少工作压力,哈哈哈。