如何在 Monaco 编辑器中正确计算字符范围并使其高亮

294 阅读6分钟

前言

公司产品针对样例中敏感数据的高亮,一直是使用高亮数据字符直接进行匹配的,这样就会有一个问题,比如:某验证码是敏感数据,如果某个数字串之间刚好包含这个验证码的值,那么这区间就会高亮,出现误差。现在需要换成后端返回敏感数据字符的起始下标值,前端计算敏感数据的范围,使高亮数据准确。

image.png

计算字符串中敏感数据在编辑器中的范围

刚接到这个需求,看到这个后端返回的偏移量,笔者以为返回的是在每一行的偏移量,只要算出该敏感数据在编辑器中的每一行就行了。到后面发现后端返回的偏移量是从第一行第一个字符开始,因为,样例原始数据在后端就是一个字符串,没法根据换行符计算在编辑器的哪一行。

没有思路就看看monaco的文档吧,无聊之际,给我发现了一个APIgetPositionAt,用于获取给定偏移量的位置,那么后端返回的起始下标就有了用武之地,可以计算出敏感数据的range,直接救了我🐶命!!!兄弟们,还是要好好看文档!!!附上monaco文档

image.png

getPositionAt

  • 语法: getPositionAt(offset: number): IPosition
  • 参数: offset
  • 返回值: IPosition
  • 描述: 用于获取给定偏移量的位置。 offset 参数是一个 number 类型,表示要获取位置的偏移量。

IPosition

interface IPosition {
   column: number,
   lineNumber: number
}
  • column

类型: number 只读;默认值: -;可选项: -;描述: 

  • lineNumber

类型: number 只读;默认值: -‘可选项: -;描述: 行号

  • 根据getPositionAt获取position
const model = editor.getModel();

const startPosition = model.getPositionAt(valueIndex.startIndex);
const endPosition = model.getPositionAt(valueIndex.endIndex);

const currentRange = {
   startLineNumber: startPosition.lineNumber, // 起始位置的行号。
   startColumn: startPosition.column, // 起始位置的列号。
   endLineNumber: endPosition.lineNumber, // 结束位置的行号。
   endColumn: endPosition.column + 1 // 结束位置的列号。
};

editor.setSelection(currentRange);

image.png

根据以上代码,便可以得出该敏感数据在编辑器中的具体范围,再使用setSelection进行选中该范围

getPositionAt存在的问题(位置有偏差)

在自测过程中,发现如果后端返回类型为htmldoc时,在编辑器中会自动换行,使用getPositionAt进行计算位置时会有偏差,如下图所示:

image.png

image.png

经过排查发现,这是后端返回的原始内容中只要携带\n就会出现计算位置不准确,比如上面的htmltext文本。原本的想法是后端把所有的\n全部去除,但会修改原始内容,直接pass,那么只能由前端重新计算敏感数据的位置。

计算带\n字符串中敏感数据在编辑器中的范围

为了解决这个问题,需要分三步完成操作:

  1. 解析字符串:将字符串按换行符分割成多行,以便精确计算字符的位置。
  2. 计算行列范围:将全局字符偏移量(startIndexendIndex)转换为 Monaco 编辑器中对应的行和列。
  3. 应用高亮:利用 Monaco 的 API 将计算出的范围应用到编辑器中,以实现精准的高亮效果。

实现步骤详解

1. 将字符串分割为行

我们首先需要将原始字符串按照换行符 \n 分割成多个行。这样可以将字符位置从全局偏移量转化为每行的相对偏移量。以下代码展示了如何实现:

const originValue = `'HTTP/1.1 200 OKdate: Thu, 1#################51 GMT\r\nserver: Simple###############/2.7.5\r\ncontent-length: 1#\r\nlast-modified: Wed, 0#################19 GMT\r\ncontent-disposition: attach#############=f.txt\r\ncontent-type: applic############stream\r\n\r\nfile content test!!!!\n 147######12\n'`;

// 分割字符串为行
const lines = originValue.split('\n');

2. 计算字符的行列范围

将计算方法封装成一个方法函数,具体实现见如下代码,传入索引以及行数组即可得到行号和列号:

/**
 * 获取行号和列号
 * @param {number} index 索引
 * @param {string[]} lines 行数组
 */
export function getLineAndColumn(index, lines) {
  let line = 0; // 当前行索引
  let column = index; // 当前列索引(初始为字符偏移量)

  /**
   * 遍历每一行。lineLength:当前行的长度,加 1 是因为换行符 \n 也占一个字符。
   * 判断当前偏移量是否在当前行内:
   *  如果偏移量小于当前行的长度,则说明目标字符在这一行,返回行号和列号。
   *  如果偏移量超过当前行的长度,则减去当前行的长度,更新剩余偏移量,继续检查下一行。
   * 如果遍历完所有行,偏移量仍未完全分配,则认为字符在最后一行的某个位置。
   */
  // 遍历每一行,计算行和列
  while (line < lines.length) {
    const lineLength = lines[line].length + 1; // 每行的字符数(+1 包括换行符)
    if (column < lineLength) {
      return { lineNumber: line + 1, column: column + 1 }; // Monaco 编辑器的行、列从 1 开始
    }
    column -= lineLength; // 减去当前行的总长度,更新偏移量
    line++; // 检查下一行
  }
  // 如果超出范围,返回最后一行
  return { lineNumber: lines.length - 1, column: column }; // 最后一行
}

3. 创建 Monaco 编辑器的范围对象 && 应用高亮到 Monaco 编辑器

Monaco 编辑器的 Range 对象用于表示文本的选中区域。我们需要将前一步计算出的行列位置转换为 Monaco 可识别的格式:

{
  startLineNumber: startPosition.lineNumber,
  startColumn: startPosition.column,
  endLineNumber: endPosition.lineNumber,
  endColumn: endPosition.column + 1
}

下面initEditorRangeList是将后端返回的所有起始下标进行遍历,得出全部敏感数据的Range

const lines = editorContent.split('\n');

const initEditorRangeList =
   locationObj.data.extractValueDtos?.flatMap(
      (extractValue) =>
         extractValue.labelValueIndexList?.flatMap((labelValueIndex) =>
           labelValueIndex.valueIndexList.map((valueIndex) => {
              const startPosition = getLineAndColumn(valueIndex.startIndex,lines);
              const endPosition = getLineAndColumn(valueIndex.endIndex,lines);
              return {
                 startLineNumber: startPosition.lineNumber,
                 startColumn: startPosition.column,
                 endLineNumber: endPosition.lineNumber,
                 endColumn: endPosition.column + 1
              };
           })
         ) || []
      ) || [];

// 仅渲染可见区域内的装饰器,视窗外的内容则延迟加载或忽略。
highlightVisibleRanges(editor, initEditorRangeList);

highlightVisibleRanges高亮指定范围内的敏感数据方法,请继续阅读下文。使用修改之后的计算位置逻辑,实现精准匹配:

image.png

image.png

优化高亮逻辑

有些样例数据过大,为了避免每次渲染量过大,可能导致该样例内的敏感数据没有高亮,可以使用getVisibleRanges()获取编辑器的可见范围,只渲染编辑器可视范围内的敏感数据,当可视范围发生变化时,再次执行匹配高亮逻辑。优化后的代码如下:

/**
 * 高亮指定范围内的敏感数据
 * @param {*} editor 编辑器实例
 * @param {*} rangeList 需要高亮的范围数组
 * @param {*} inlineClassName 自定义高亮类名
 */
export function highlightVisibleRanges(
  editor,
  rangeList,
  inlineClassName = 'mtk5'
) {
  function handler() {
    // 获取编辑器的可见范围
    const visibleRanges = editor.getVisibleRanges();

    const visibleDecorations = rangeList.filter((range) => {
      return visibleRanges.some(
        (visibleRange) =>
          range.startLineNumber >= visibleRange.startLineNumber &&
          range.endLineNumber <= visibleRange.endLineNumber
      );
    });
    
    // 创建装饰器集合,使其高亮
    editor.createDecorationsCollection(
      visibleDecorations.map((range, index) => ({
        id: index,
        range,
        options: { inlineClassName }
      }))
    );
  }

  if (rangeList.length > 0) {
    // 监听编辑器滚动事件并刷新匹配逻辑
    editor.onDidScrollChange(() => {
      handler();
    });
    handler();
  }
}

通过以上步骤,我们成功解决了包含换行符的字符串高亮显示问题。关键在于:

  1. 正确分割字符串:按行处理换行符。
  2. 精确计算字符位置:从全局偏移量到行列的转换。
  3. 应用范围到编辑器:使用 Monaco 的 API 确保高亮区域的准确性。