【CodeMirror】系列(二)官网示例(六)自动补全、边栏

一、自动补全

@codemirror/autocomplete 包提供了在编辑器中显示输入建议的功能。这个示例展示了如何启用该功能以及如何编写自己的补全来源。

自动补全是通过在编辑器的配置项中加入 autocompletion 扩展实现的。有些语言包支持内置的自动补全功能,比如HTML包。

默认情况下,当用户键入内容的时候都会自动激活自动补全,查找自动补全项。但你也可以通过配置,只有通过命令command激活自动补全。

扩展显示的补全建议来自于一个或多个自动补全源 CompletionSource。这些源是函数,它们接收一个补全上下文的对象 CompletionContext,并返回一个描述正在补全的范围及可显示选项的对象。补全源可以异步运行,通过返回一个 Promise 来实现。

将补全源连接到编辑器的最简单方式是使用 override 选项。你可以在配置自动补全的扩展时,使用这个选项指定你的补全源。

例如下面代码,先定义一个自动补全源函数:

javascript 复制代码
// 导入 CompletionContext 类型,以便在补全源函数中使用
import {CompletionContext} from "@codemirror/autocomplete"

// 自定义的补全函数,接受一个 CompletionContext 作为参数
function myCompletions(context: CompletionContext) {
  // 使用 matchBefore 方法匹配光标前的单词,返回匹配的范围
  let word = context.matchBefore(/\w*/)

  // 检查匹配的范围是否为空,并且检查不是显式调用的补全
  if (word.from == word.to && !context.explicit) 
    return null // 返回 null 表示没有补全建议

  // 返回补全建议
  return {
    from: word.from, // 返回补全建议的起始位置
    options: [ // 返回的补全选项列表
      {label: "match", type: "keyword"}, // 第一个补全项,类型为关键字
      {label: "hello", type: "variable", info: "(World)"}, // 第二个补全项,类型为变量,附加信息为 "(World)"
      {label: "magic", type: "text", apply: "⠁⭒*.✩.*⭒⠁", detail: "macro"} // 第三个补全项,类型为文本,附加信息和应用字符串
    ]
  }
}

看一下第三项,我们输入 m 的时候,会显示 magic 的提示,并且会通过斜体显示这一项的 detail 内容

使用 enter 应用这一项自动补全之后,代码会被替换成这一项的 apply 属性:

这是提供补全的一种非常粗略的方法,实际上并没有真正考虑编辑上下文。但它展示了补全函数必须执行的基本操作。

  • 确定补全的文本范围:首先,需要找出光标前面可以补全的文本部分。这里使用 matchBefore 方法结合正则表达式来确定该部分文本。
  • 检查补全是否合适:然后要检查补全请求是否合适。explicit 标志表示补全是通过命令显式启动的还是通过输入隐式触发的。一般情况下,只有当补全是显式触发,或者补全位置在某个可以补全的结构之后时,才应该返回补全结果。
  • 构建补全列表并返回:构建一个补全选项列表,并返回它以及补全的起始位置。(结束位置默认为补全发生的位置。)
  • 补全对象的结构:补全本身是一个对象,包含一个 label 属性,提供了在选项列表中显示的文本以及用户选择补全时插入的文本。

默认情况下,补全列表只显示标签。通常你还会想提供一个 type 属性,用于确定补全旁边显示的图标。detail 可以用来显示一个简短的字符串,info 则用于显示更长的文本,当补全被选择时,该长文本会在列表旁边的窗口中展现。

自定义补全选择的行为:如果你想覆盖选择补全时的行为,可以使用 apply 属性。它可以是一个字符串,用来替换补全范围,或者是一个函数,调用时会执行特定的操作。

处理多个语言的补全源:当你作为一个通用扩展提供补全源时,或者处理混合语言文档时,设置全局源并不现实。当没有给出 override 时,插件会使用 EditorState.languageDataAt 方法通过名称 "autocomplete" 查询适合该语言的补全函数。要注册这些函数,你需要在语言对象中使用数据特征。例如,可以在你的状态配置中包含以下代码:

javascript 复制代码
myLanguage.data.of({
  autocomplete: myCompletions
})

你也可以直接在这个属性中包含一个补全对象的数组,这样库会简单地使用这些对象(通过 completeFromList 包装)作为补全源。

上面提到的简单补全源不需要根据输入内容来过滤补全选项------插件会处理这个问题。它使用一种模糊匹配的方式来过滤和排序与当前输入文本匹配的补全选项,并会高亮显示每个补全项中与输入匹配的字母。

  • 影响排序:如果你想影响补全的排序,可以为补全对象设置一个 boost 属性,来增加或减少它们的匹配分数。这样,就可以让某些补全项在列表中更突出,或者让某些项排在后面。
  • 自定义过滤与排序:如果你确实希望自己过滤和排序补全选项,可以在返回的对象中包含 filter: false 属性,这样就会禁用内置的过滤功能。在这种情况下,你需要手动处理补全项的过滤和排序逻辑。

有些补全源需要在每次按键时重新计算结果,但对于许多补全源来说,这样做是不必要且低效的。它们会针对某个特定的结构返回完整的补全列表,只要用户在这个结构内输入(或删除)文本,就可以使用同一份列表(过滤当前输入)来填充补全列表。

因此,强烈建议在你的补全结果中提供一个 validFor 属性。这个属性应该包含一个函数或正则表达式,用于告诉扩展,只要更新后的输入(结果的 from 属性与补全点之间的范围)与这个值匹配,就可以继续使用这份补全列表。

在上面的 myCompletions 函数中,由于所有的补全项都是简单的单词,像 validFor: /^\w*$/ 这样的值是合适的。

为了使补全源更智能,检查补全点周围的语法树是非常有用的,这可以帮助我们更好地理解正在补全的结构类型。

例如,下面这个补全源专门用于 JavaScript,可以在块注释中补全(部分)JSDoc 标签。

javascript 复制代码
// 从 @codemirror/language 导入语法树工具
import {syntaxTree} from "@codemirror/language"

// 定义可补全的 JSDoc 标签选项
const tagOptions = [
  "constructor", "deprecated", "link", "param", "returns", "type"
].map(tag => ({label: "@" + tag, type: "keyword"})) // 将标签转换为补全对象格式

// 定义补全 JSDoc 标签的函数
function completeJSDoc(context: CompletionContext) {
  // 使用语法树解析器获取光标之前的语法节点
  let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)

  // 检查当前节点是否是块注释,并且判断开始部分是否为 "/**"
  if (nodeBefore.name != "BlockComment" ||
      context.state.sliceDoc(nodeBefore.from, nodeBefore.from + 3) != "/**")
    return null // 如果条件不满足,返回 null,表示没有补全建议

  // 获取光标之前的文本
  let textBefore = context.state.sliceDoc(nodeBefore.from, context.pos)
  
  // 使用正则表达式检查光标前是否有 JSDoc 标签
  let tagBefore = /@\w*$/.exec(textBefore)

  // 如果没有找到标签且补全不是显式请求,返回 null
  if (!tagBefore && !context.explicit) return null

  // 返回补全建议
  return {
    // 确定补全起始位置,如果找到了标签,则从标签的位置开始补全
    from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,

    // 选项是之前定义的可补全 JSDoc 标签
    options: tagOptions,

    // 定义有效输入的正则表达式,当输入为 "@标签" 或者为空时有效
    validFor: /^(@\w*)?$/
  }
}

您现在可以使用这样的扩展来为JavaScript内容启用此完成源,会显示 JSDoc 关键词的自动补全提示。

二、边栏

视图模块提供了为编辑器添加边栏(代码前面的垂直条形)功能。边栏的最简单用法就是在配置中简单地使用 lineNumbers(),这样就可以获得一个行号边栏。但这个模块也可以在你想定义自己的边栏并在其中显示自定义控件时提供帮助。

(一)添加边栏

从概念上讲,编辑器可以并排显示一组边栏(gutters),每个边栏都有自己的样式和内容(尽管通常希望保持它们的默认样式,以便与其他边栏融为一体,看起来像一个大边栏)。对于每一行,每个边栏可以显示一些内容。行号边栏显然会显示行号。

要添加一个边栏,调用 gutter 函数,并将结果包含在你的状态配置中。该扩展相对于其他边栏扩展的位置决定了边栏的顺序。所以,例如,下面这段代码会将我们的边栏放在行号之后:

javascript 复制代码
extensions: [lineNumbers(), gutter({class: "cm-mygutter"})]

不过,除非 cm-mygutter CSS 类设置了最小宽度,否则你将看不到这样的边栏------它会只是一个空元素(在 CSS flexbox 中),浏览器会将其折叠。
在边栏中放置内容

要在边栏中显示内容,我们可以使用 lineMarker 选项,该选项会被调用以确定每可见行上要显示的内容,或者使用 markers 选项,它允许你构建一个持久的标记集(使用在装饰中使用的相同范围集合数据结构)以在边栏中显示。

与装饰一样,边栏标记由轻量级的不可变值表示,这些值知道如何将自己渲染为 DOM 节点,以允许更新以声明的方式表示,而不必在每次交易中重新创建大量的 DOM 节点。边栏标记还可以向边栏元素添加 CSS 类。

示例代码

以下代码定义了两个边栏,一个在每个空行上显示一个 ø 符号,另一个通过点击该边栏允许你切换每一行的"断点"标记。第一个很简单:

javascript 复制代码
// 从 @codemirror/view 导入所需的模块
import {EditorView, gutter, GutterMarker} from "@codemirror/view"

// 定义一个空行标记类,继承自 GutterMarker
const emptyMarker = new class extends GutterMarker {
  // 定义如何将标记渲染为 DOM 元素
  toDOM() { 
    return document.createTextNode("ø") // 返回一个文本节点,内容为 "ø"
  }
}

// 创建一个边栏,用于在空行上显示特定标记
const emptyLineGutter = gutter({
  // 定义行标记函数,根据行的起始和结束位置来决定显示内容
  lineMarker(view, line) {
    // 如果行的起始位置和结束位置相同,表示这是一个空行
    return line.from == line.to ? emptyMarker : null // 如果是空行,返回空标记,否则返回 null
  },
  // 设定初始的间隔标记
  initialSpacer: () => emptyMarker // 默认间隔使用空标记
})

为了避免空边栏根本不显示的问题,边栏允许你配置一个"间隔"元素,该元素以隐形的方式渲染在边栏中,以设置其最小宽度。这通常比用 CSS 设置显式宽度并确保其覆盖预期内容更容易。
lineMarker 选项检查该行是否为零长度,如果是,返回我们的标记。

断点边栏则稍微复杂一些。它需要跟踪状态(即断点的位置),为此我们使用一个状态字段,并通过状态效果来更新它。

javascript 复制代码
// 从 @codemirror/state 导入必要的模块
import {StateField, StateEffect, RangeSet} from "@codemirror/state"

// 定义断点效果,描述断点的位置和状态(开启/关闭)
const breakpointEffect = StateEffect.define<{pos: number, on: boolean}>({
  // 定义在应用映射时如何迁移效果值
  map: (val, mapping) => ({pos: mapping.mapPos(val.pos), on: val.on}) // 映射位置,保持状态不变
})

// 定义断点状态字段,使用 RangeSet 来存储断点标记
const breakpointState = StateField.define<RangeSet<GutterMarker>>({
  // 创建函数,初始化为空的 RangeSet
  create() { return RangeSet.empty },
  
  // 更新函数,处理状态变化
  update(set, transaction) {
    // 根据事务的变化映射现有的断点集
    set = set.map(transaction.changes)
    
    // 遍历事务的效果
    for (let e of transaction.effects) {
      if (e.is(breakpointEffect)) { // 如果效果是断点效果
        if (e.value.on) // 如果断点状态是开启
          // 在指定位置添加断点标记
          set = set.update({add: [breakpointMarker.range(e.value.pos)]})
        else // 如果断点状态是关闭
          // 移除指定位置的断点标记
          set = set.update({filter: from => from != e.value.pos})
      }
    }
    return set // 返回更新后的断点集
  }
})

// 切换断点的状态函数
function toggleBreakpoint(view: EditorView, pos: number) {
  // 获取当前状态中的断点
  let breakpoints = view.state.field(breakpointState)
  let hasBreakpoint = false
  
  // 检查指定位置是否已有断点
  breakpoints.between(pos, pos, () => {hasBreakpoint = true})
  
  // 调度一个效果来切换断点状态
  view.dispatch({
    // 发送断点效果,位置和状态(开启或关闭)
    effects: breakpointEffect.of({pos, on: !hasBreakpoint})
  })
}

状态起始时为空,当发生事务时,它会通过变更(如果有的话)来映射断点的位置,并查找添加或移除断点的效果,根据需要调整断点集合。

breakpointGutter 扩展将这个状态字段与边栏结合起来,并为该边栏添加一些样式。

javascript 复制代码
// 定义一个断点标记类,继承自 GutterMarker
const breakpointMarker = new class extends GutterMarker {
  // 定义如何将标记渲染为 DOM 元素
  toDOM() { 
    return document.createTextNode("💔") // 返回一个文本节点,显示为 "💔" 符号
  }
}

// 创建断点边栏的扩展,包括状态字段、边栏设置和样式
const breakpointGutter = [
  breakpointState, // 包含断点状态字段
  
  // 定义边栏,配置其外观和行为
  gutter({
    class: "cm-breakpoint-gutter", // 设置边栏的 CSS 类名
    markers: v => v.state.field(breakpointState), // 设置边栏中显示的标记为当前状态字段中的断点集合
    initialSpacer: () => breakpointMarker, // 使用断点标记作为初始间隔,确保边栏有最小宽度
    domEventHandlers: {
      // 定义鼠标按下事件处理
      mousedown(view, line) {
        toggleBreakpoint(view, line.from) // 切换鼠标点击位置的断点状态
        return true // 返回 true,表示事件已被处理
      }
    }
  }),

  // 定义边栏的基础主题样式
  EditorView.baseTheme({
    ".cm-breakpoint-gutter .cm-gutterElement": {
      color: "red", // 设置断点边栏中标记的颜色为红色
      paddingLeft: "5px", // 设置左侧内边距
      cursor: "default" // 设置鼠标悬停时的光标样式
    }
  })
]

domEventHandlers 选项允许您为该边栏指定事件处理程序。在这里,我们设置了一个 mousedown 事件处理程序,用于切换用户点击的行的断点。

以下是一个带有断点边栏的编辑器示例,断点边栏位于行号之前,空行边栏在其之后。这样的布局设计可以帮助用户直观地查看代码的状态和结构:

(二)自定义行号间距

lineNumbers 函数同样接受配置参数,允许您添加事件处理程序或自定义行号的显示方式。

以下示例展示了如何自定义行号的格式,通过 formatNumber 选项将行号格式化为十六进制字符串:

javascript 复制代码
const hexLineNumbers = lineNumbers({
  formatNumber: n => n.toString(16)
})

此外,您还可以向行号边栏添加标记,这样可以在受到影响的行上替代默认的行号。这个功能通过 lineNumberMarkers 面貌实现,类似于在自定义边栏中使用的标记,但可以通过任何扩展提供,而不是仅仅针对单个边栏进行配置。

下面是一个示例,演示如何在行号边栏中使用标记:

javascript 复制代码
// 定义一个行号标记类,继承自 GutterMarker
const specialLineMarker = new class extends GutterMarker {
  toDOM() { 
    return document.createTextNode("★") // 返回一个特殊字符作为行号的替代
  }
}

// 在某些行添加行号标记
const lineNumberMarkers = StateField.define<RangeSet<GutterMarker>>({
  create() { return RangeSet.empty; }, // 初始化为空的标记集合
  update(set, transaction) {
    set = set.map(transaction.changes); // 映射变更
    for (let e of transaction.effects) {
      if (e.is(specialEffect)) { // 如果效果是特殊效果
        // 根据效果的状态决定是否添加标记
        set = e.value.on ? set.update({add: [specialLineMarker.range(e.value.pos)]}) : set.update({filter: from => from !== e.value.pos});
      }
    }
    return set; // 返回更新后的标记集合
  }
});

// 定义行号边栏,集成标记
const numberedGutter = [
  hexLineNumbers, // 使用十六进制行号格式
  gutter({
    class: "cm-line-number-gutter", // 设置边栏的 CSS 类名
    markers: v => v.state.field(lineNumberMarkers), // 从状态字段获取行号标记
  })
];
相关推荐
rocky1914 分钟前
谷歌浏览器插件 录制动态 DOM 元素
前端
谁还不是一个打工人8 分钟前
css解决边框四个角有颜色
前端·css
AI绘画咪酱8 分钟前
【CSDN首发】Stable Diffusion从零到精通学习路线分享
人工智能·学习·macos·ai作画·stable diffusion·aigc
笑远1 小时前
Vim/Vi 常用命令速查手册
linux·编辑器·vim
海晨忆1 小时前
【Vue】v-if和v-show的区别
前端·javascript·vue.js·v-show·v-if
Ven%1 小时前
vscode报错:unins000.exe 尝试在目标目录创建文件时发生一个错误
ide·vscode·编辑器
小麟有点小靈1 小时前
VSCode写java时常用的快捷键
java·vscode·编辑器
1024小神1 小时前
在GitHub action中使用添加项目中配置文件的值为环境变量
前端·javascript
齐尹秦2 小时前
CSS 列表样式学习笔记
前端
Mnxj2 小时前
渐变边框设计
前端