基于Monaco的diffEditor实现内容对比

前言

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

  • monaco默认的效果,行样式没有与我的样式保持一致,只存在两种状态

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

需求分析

  1. 需要计算出新增删除差异各占多少行,这里采用diffEidtor提供的get​Line​Changes方法获取所有行改动,然后分析数据
  2. 如何判断新增行删除行差异行呢?(这里主要想明白,你的状态是跟着视图走的,左侧空行代表新增,右侧空行代表删除、两侧都存在代表更新,是不是一说就明白呢? 但是我之前还结合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
    }
]
  1. 想明白新增行删除行差异行的计算,那么我们就聚焦到这些行变化的颜色,其实也不算复杂,首先将默认行的背景色改为透明、然后我们再根据变更状态添加对应的行装饰器就可以实现我们需要的效果了

代码实现

  1. 设置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;
      }
    }
  1. 注册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}
  />
  1. 当编辑器加载完成时,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 防抖
    })
  }
  1. 基于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的堆栈聚焦行,然后我们在通过行装饰器绘制对应的高亮行。至于堆栈信息只需要绘制对应的堆栈面板接口,是不是感觉就特别清晰了

为什么写这篇文章呢?

  1. 是因为我没有找到相关文章,其他文章都是直接实现DiffEditor效果,并不满足需要的三种状态新增删除差异
  2. 在研发任务排期紧张的时候帮助遇到相同需求的小伙伴减少工作压力,哈哈哈。
相关推荐
chenbin___2 小时前
react native中 createAsyncThunk 的详细说明,及用法示例(转自通义千问)
javascript·react native·react.js
摆烂工程师2 小时前
(2025年11月)开发了 ChatGPT 导出聊天记录的插件,ChatGPT Free、Plus、Business、Team 等用户都可用
前端·后端·程序员
gongzemin3 小时前
使用阿里云ECS部署前端应用
前端·vue.js·后端
用户41180034153413 小时前
Flutter课题汇报
前端
环信3 小时前
实战教程|快速上线音视频通话:手把手教你实现呼叫与接听全流程
前端
Dgua3 小时前
✨TypeScript快速入门第一篇:从基础到 any、unknown、never 的实战解析
前端
用户9714171814273 小时前
前端开发中的跨域问题:Vite 开发环境配置指南
vue.js·vite
海云前端13 小时前
Vue3 大屏项目投屏功能开发:多显示器适配实践
前端
技术小丁3 小时前
使用 HTML + JavaScript 实现酒店订房日期选择器(附完整源码)
前端·javascript