Monaco Editor 实战:添加代码折叠功能

在之前的文章《4 步为 Monaco Editor 添加自定义语言支持》中,我们已经学会了如何使用 monaco-editor 来实现一个支持自定义编程语言的编辑器,但是作为一个标准的代码编辑器,我们还需要它支持代码块折叠功能,这样在编辑内容比较多的代码文件时,可以方便的将其中一些代码块折叠起来,使得在阅读代码时更容易理解整体代码结构。

这一次我们就来看看如何给 Monaco Editor 增加代码块折叠功能。

官方示例

在 Monaco Editor Playground 的 Folding Provider Example 示例中,可以看到如何给一个自定义语言添加代码折叠支持:

monaco.languages.registerFoldingRangeProvider("foldLanguage", {
    provideFoldingRanges: function (model, context, token) {
        return [
            // comment1
            {
                start: 5,
                end: 7,
                kind: monaco.languages.FoldingRangeKind.Comment,
            },
      // ...
    ]
  }
});

可以看到,只需要在注册自定义语言后,再注册一个 foldingRangeProvider 即可,在 provicdeFoldingRanges callback 中,将可以折叠的代码区块,以 start 和 end 行号圈定范围,返回给到 Monaco Editor 即可。

在示例中,它是通过硬编码起始和结束行号的方式来提供可折叠范围的,但是在实际使用过程中,我们不可能预见可以支持代码折叠的范围,这里需要使用编码的方式来动态生成可折叠范围。

需求定义

在开发 MermaidEditor 的代码编辑器时,在部分图表中,代码块会有一些层级关系,为了有更好的编辑体验,我们就需要给代码编辑器加上代码块折叠功能。

目前 Monaco Editor 并没有提供官方的 LSP(Language Server Protocol) 支持,我们需要使用其他方式来实现代码折叠的逻辑。

monaco-editor-add-code-folding-1

通过观察,我们可以发现,对于不同层级的代码块来说,它们通常拥有不同数量的缩进,因此我们可以通过缩进来判断代码行是否属于同一个代码块。

代码实现

获取编辑区内容

在 provideFoldingRanges 函数中有一个参数 model,通过这个变量我们可以获得当前编辑区中的所有文本内容,通过遍历每一行的内容,可以获取到每一行的前置空白字符数量,从而根据空白字符数量来判定代码块折叠范围。

// 用于匹配每一行空白字符的正则表达式
const pattern = /^(\s*)(.+)/;
// 遍历编辑区内容中的每一行代码
for (var i = 1, count = model.getLineCount(); i <= count; i++) {
  const line = model.getLineContent(i);

  // 获取每一行代码中的前置空白字符
  const matches = pattern.exec(line);
  if (matches) {
    // 获取当前行的空白长度,用于匹配后续代码行是否属于这一区块
    const indentLen = matches[1].length;
  }
}

匹配代码折叠区域

在遍历每一行代码后,我们需要判断它后续的代码行,是否属于可折叠代码区块,这里判断的标准是,后续代码行的前置空白字符长度大于当前行的前置空白字符数量,因此所有缩进长度大于当前行的代码区域,都可以被包含进来 。

// 从下一行开始搜索
let endLine = i + 1;
let lastNotEmptyLine = i;
// 遍历到编辑区内容末尾
while (endLine <= count) {
  const lineContent = model.getLineContent(endLine);
  // 获取后续行的前置空白字符长度
  const subMatches = pattern.exec(lineContent);
  if (subMatches != null) {
    // 如果前置空白字符长度大于起始行,就将它包含进折叠区域
    if (subMatches[1].length > indentLen) {
      lastNotEmptyLine = endLine;
    } else {
      break;
    }
  }
  endLine++;
}

请注意,这里额外忽略了空白行,是为了防止空白行会中断折叠区域的判断,这样在处理同等缩进的代码块中,如果出于代码格式考虑添加的空白也不会导致折叠区域判断错误,提前结束折叠区域的判定。

返回代码块折叠区域

if (lastNotEmptyLine > i) {
  ranges.push({
    start: i,
    end: lastNotEmptyLine,
    kind: monaco.languages.FoldingRangeKind.Region,
  });
}

这里返回 Monaco Editor 可以使用的代码块折叠区域数据结构。

完整代码

monacoEditor.languages.registerFoldingRangeProvider("foldLanguage", {
  provideFoldingRanges: function (model, context, token) {
    const ranges = [];
    const pattern = /^(\s*)(.+)/;
    for (var i = 1, count = model.getLineCount(); i <= count; i++) {
      const line = model.getLineContent(i);

      const matches = pattern.exec(line);
      if (matches) {
        const indentLen = matches[1].length;

        let endLine = i + 1;
        let lastNotEmptyLine = i;
        while (endLine <= count) {
          const lineContent = model.getLineContent(endLine);
          const subMatches = pattern.exec(lineContent);
          if (subMatches != null) {
            if (subMatches[1].length > indentLen) {
              lastNotEmptyLine = endLine;
            } else {
              break;
            }
          }
          endLine++;
        }

        if (lastNotEmptyLine > i) {
          ranges.push({
            start: i,
            end: lastNotEmptyLine,
            kind: monaco.languages.FoldingRangeKind.Region,
          });
        }
      }
    }
    return ranges;
  },
});

最终效果

monaco-editor-add-code-folding-2

可以看到,最终 Monaco Editor 正确显示了代码块折叠标记,并且有嵌套的折叠,也能正确识别。

参考资料

发表评论?

0 条评论。

发表评论


注意 - 你可以用以下 HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>