我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:文长

引言

现代软件开发中,代码编辑器的功能不断演进,以满足开发者对高效和智能化工具的需求。Monaco Editor 作为一种轻量级但功能强大的代码编辑器,广泛应用于多种开发环境中。在此背景下,Copilot,一款由 GitHub 开发的 AI 编程助手,凭借其智能代码补全和建议功能,迅速吸引了开发者的关注。

本文将探讨如何在 Monaco Editor 中实现在线版 Copilot 功能的代码续写,旨在为用户提供更加高效的编程体验。

Copilot

什么是 Copilot?

Copilot 是由 GitHub 开发的一款人工智能编程助手,它利用机器学习和自然语言处理技术,旨在帮助开发者更高效地编写代码。Copilot 通过分析大量的开源代码库和文档,能够理解开发者的意图并提供实时的代码建议和补全。当然,除了 Copilot ,还有很多类似的产品,如 Cursor、CodeWhisperer、CodeGeeX、通义灵码、iFlyCode …

工作原理

Copilot 基于 OpenAI 的 Codex 模型,该模型经过大量代码和自然语言数据的训练,能够生成符合语法和逻辑的代码。它通过分析开发者的输入和上下文,预测最可能的代码片段,并将其呈现给用户。

使用效果

Copilot 可以在当前光标处自动生成补全代码。如下图所示

简版实现

github copilot 提供了 vs code 的插件,支持在 vs code 中使用,那是否可以在 Web Editor 中也实现一个 Copilot 呢?通过查看 Monaco Editor 的 API ,可以看到是提供了这么一个 Provider 的。

registerInlineCompletionsProvider

registerInlineCompletionsProvider 是 Monaco Editor 中的一个方法,用于注册一个内联补全 Provider。这个功能允许开发者在代码编辑器中提供上下文相关的补全建议,提升用户的编码效率。

registerInlineCompletionsProvider 支持接收 2 个参数:

  • languageId:要给哪个 language 注册这个Provider。这个 Provider 只会在 Monaco Editor 的 language 设置为该 language 时,才会被触发。
  • provider:
    • provideInlineCompletions:该方法用于提供内联补全建议,它根据当前文本模型、光标位置和上下文信息生成适合的补全项,并返回给编辑器。

      • 参数

        • model: editor.ITextModel:当前编辑器的文本模型,包含用户正在编辑的文本。
        • position: Position:光标的当前位置,指示补全建议的上下文。
        • context: InlineCompletionContext:提供有关补全上下文的信息,例如用户输入状态和触发条件。
        • token: CancellationToken:用于取消操作的令牌,确保性能和可控性。
    • freeInlineCompletions:当补全列表不再使用且可以被垃圾回收时,该方法会被调用。允许开发者执行清理操作,释放资源。
      • 参数

        • completions: T:需要释放的补全项集合。
    • handleItemDidShow:当补全项被展示给用户时,该方法会被调用。允许开发者执行特定的逻辑,例如记录日志、更新UI或执行其他操作。
      • 参数

        • completions: T:当前的补全项集合。
        • item: T['items'][number]:被展示的具体补全项。

如下图例子所示

具体实现

思路

在编辑器中,每当内容发生变更时,都会触发 registerInlineCompletionsProvider 。在这个 Provider 中执行补全。整个补全的过程:

  1. 修改光标状态
  2. 获取上下文内容,发送给 AI
  3. 等待 AI 返回补全结果,将 AI 的结果进行返回。这里返回的格式(这里以 Monaco Editor@0.31.1 为例,@0.34 版本开始与此有些差别):
interface InlineCompletion {
/**
* The text to insert.
* If the text contains a line break, the range must end at the end of a line.
* If existing text should be replaced, the existing text must be a prefix of the text to insert.
*/
readonly text: string;
/**
* The range to replace.
* Must begin and end on the same line.
*/
readonly range?: IRange;
readonly command?: Command;
}
interface InlineCompletions<TItem extends InlineCompletion = InlineCompletion> {
readonly items: readonly TItem[];
}
interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> {
provideInlineCompletions(model: editor.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult<T>;
}

@0.34 及以上版本返回格式:

interface InlineCompletion {
/**
* The text to insert.
* If the text contains a line break, the range must end at the end of a line.
* If existing text should be replaced, the existing text must be a prefix of the text to insert.
*
* The text can also be a snippet. In that case, a preview with default parameters is shown.
* When accepting the suggestion, the full snippet is inserted.
*/
readonly insertText: string | {
snippet: string;
};
/**
* A text that is used to decide if this inline completion should be shown.
* An inline completion is shown if the text to replace is a subword of the filter text.
*/
readonly filterText?: string;
/**
* An optional array of additional text edits that are applied when
* selecting this completion. Edits must not overlap with the main edit
* nor with themselves.
*/
readonly additionalTextEdits?: editor.ISingleEditOperation[];
/**
* The range to replace.
* Must begin and end on the same line.
*/
readonly range?: IRange;
readonly command?: Command;
/**
* If set to `true`, unopened closing brackets are removed and unclosed opening brackets are closed.
* Defaults to `false`.
*/
readonly completeBracketPairs?: boolean;
}
  1. 补全结束,恢复光标状态

过程

  • 设置光标

    在发起补全时,需要将光标变为 loading 状态,但是 monaco editor 自身的配置不满足想要的样式(只支持:'line' | 'block' | 'underline' | 'line-thin' | 'block-outline' | 'underline-thin')。

    monaco editor 的光标并不是原生输入框自带的,也是自行实现的



    通过操作 dom 的形式,使用 createPortal 方法,将 loading 组件渲染到该容器下,然后通过状态控制光标的状态切换。具体实现如下所示:
class Editor extends React.Component {
... switchToLoadingCursor = () => {
const defaultCursor = document.querySelector('.cursors-layer .cursor') as HTMLDivElement;
const defaultCursorRect = defaultCursor.getBoundingClientRect();
const cursorLoadingRect = document
.querySelector('.cursors-layer .cursorLoading')
.getBoundingClientRect(); defaultCursor.style.display = 'none';
this.setState({
cursorLoading: {
left: defaultCursorRect.left - cursorLoadingRect.left + 2,
top: defaultCursorRect.top - cursorLoadingRect.top + 2,
visible: 'visible',
},
});
}; switchToDefaultCursor = () => {
clearTimeout(this.copilotTimer);
if (this.abortController && !this.abortController.signal.aborted) {
this.abortController.abort();
} const defaultCursor = document.querySelector('.cursors-layer .cursor') as HTMLDivElement; defaultCursor.style.display = 'block';
this.setState({
cursorLoading: {
left: 0,
top: 0,
visible: 'hidden',
},
});
}; render() {
const cursorLayer = document.querySelector('.monaco-editor .cursors-layer'); return <>
...
{cursorLayer &&
ReactDOM.createPortal(
<Spin
className="cursorLoading"
style={{
position: 'absolute',
top: cursorLoading.top,
left: cursorLoading.left,
visibility:
cursorLoading.visible as React.CSSProperties['visibility'],
zIndex: 999,
}}
indicator={<LoadingOutlined spin />}
size="small"
/>,
cursorLayer
)}
...
</>
} }

效果如下所示:

  • 获取上下文内容,发送 AI 补全,并将内容返回

    这一步这里做的比较简单,只是将内容获取,发送给 AI ,然后等待结果的返回,结果返回后,将补全内容返回,并切换光标状态。同时,在鼠标点到其他位置时,会取消补全。

    不过,这里没有做规则校验,去校验什么情况下才发起补全行为。

    注意registerInlineCompletionsProvider 是只要内容变化就会触发,所以可能需要做一些优化(如防抖等),避免一直发送/取消请求。
this.keyDownDisposable = this.editorInstance.onKeyDown(this.switchToDefaultCursor);
this.mouseDownDisposable = this.editorInstance.onMouseDown(this.switchToDefaultCursor);
this.inlineCompletionDispose = languages.registerInlineCompletionsProvider(language, {
provideInlineCompletions: (model, position, context, token) => {
return new Promise((resolve) => {
clearTimeout(this.copilotTimer);
if (this.abortController && !this.abortController.signal.aborted) {
this.abortController.abort();
} this.copilotTimer = window.setTimeout(() => {
const codeBeforeCursor = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
const codeAfterCursor = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: model.getLineCount(),
endColumn: model.getLineMaxColumn(model.getLineCount()),
});
let result = '';
this.switchToLoadingCursor(); this.abortController = new AbortController();
api.chatOneAIGC(
{
message: `你是一个${language}补全器,以下是我的上下文:\n上文内容如下:\n${codeBeforeCursor}\n,下文内容如下:\n${codeAfterCursor}\n请你帮我进行补全,只需要返回对应的代码,不需要进行解释。`,
},
).then(({data, code}) => {
if (code === 1) {
resolve({
items: data?.map((content) => ({
text: content,
range: {
startLineNumber: position.lineNumber,
startColumn: position.column,
endLineNumber: position.lineNumber,
endColumn: content.length,
},
}),
});
} else {
resolve({ items: [] });
}
this.switchToDefaultCursor();
})
}, 500);
});
},
freeInlineCompletions(completions) {
console.log('wenchang freeInlineCompletions', completions);
},
handleItemDidShow(completions) {
console.log('wenchang handleItemDidShow', completions);
},
} as languages.InlineCompletionsProvider);

效果

总结

上述例子只是介绍了如何在 Monaco Editor 中实现类似 Copilot 的代码智能补全功能,但是,我们可以发现,只要内容发生变动,都会触发 Provider ,实际上有些场景下,是不应该触发的,这里还需要写相应的判断条件,而非像例子中所示,任何情况下都进行补全。

最后

欢迎关注【袋鼠云数栈UED团队】~

袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

Monaco Editor 中使用在线版 Copilot的更多相关文章

  1. 手把手教你实现在Monaco Editor中使用VSCode主题

    背景 笔者开源了一个小项目code-run,类似codepen的一个工具,其中代码编辑器使用的是微软的Monaco Editor,这个库是直接从VSCode的源码中生成的,只不过是做了一点修改让它支持 ...

  2. Monaco Editor 中的 Keybinding 机制

    一.前言 前段时间碰到了一个 Keybinding 相关的问题,于是探究了一番,首先大家可能会有两个问题:Monaco Editor 是啥?Keybinding 又是啥? Monaco Editor: ...

  3. 【软工】[技术博客] 用Monaco Editor打造接近vscode体验的浏览器IDE

    [技术博客] 用Monaco Editor打造接近vscode体验的浏览器IDE 官方文档与重要参考资料 官方demo 官方API调用样例 Playground 官方API Doc,但其搜索框不支持模 ...

  4. Vue cli2.0 项目中使用Monaco Editor编辑器

    monaco-editor 是微软出的一条开源web在线编辑器支持多种语言,代码高亮,代码提示等功能,与Visual Studio Code 功能几乎相同. 在项目中可能会用带代码编辑功能,或者展示代 ...

  5. js 在浏览器中使用 monaco editor

    <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content ...

  6. Asp.Net Core 使用Monaco Editor 实现代码编辑器

    在项目中经常有代码在线编辑的需求,比如修改基于Xml的配置文件,编辑Json格式的测试数据等.我们可以使用微软开源的在线代码编辑器Monaco Editor实现这些功能.Monaco Editor是著 ...

  7. Monaco Editor 使用入门

    以前项目是用ace编辑器的,但是总有些不敬人意的地方.前端事件看见的VS Code编辑器Monaco Editor准备更换下,下面介绍一些使用中遇到的一点问题.代码提示 1.项目引用 import * ...

  8. 使用 TypeScript,React,ANTLR 和 Monaco Editor 创建一个自定义 Web 编辑器(二)

    译文来源 欢迎阅读如何使用 TypeScript, React, ANTLR4, Monaco Editor 创建一个自定义 Web 编辑器系列的第二章节, 在这之前建议您阅读使用 TypeScrip ...

  9. TogetherJS – 酷!在网站中添加在线实时协作功能

    TogetherJS是一个免费.开源的 JavaScript 库,来自 Mozilla 实验室,可以实现基于 Web 的在线协作功能.把 TogetherJS 添加到您的网站中,您的用户可以在实时的互 ...

  10. 在Docker中运行torch版的neural style

    相关的代码都在Github上,请参见我的Github,https://github.com/lijingpeng/deep-learning-notes 敬请多多关注哈~~~ 在Docker中运行to ...

随机推荐

  1. 【项目学习】Pendle 项目的简单调研

    项目介绍 将 1 ETH 质押成 1 stETH,年利率为 5%,那么到期后 1 stETH 就能收回 1 ETH(本金)+ 0.05 ETH(收益).而 Pendle 所做的就是把 1 stETH ...

  2. vue全局事件总线

    首先在main.js中app实例中使用生命周期钩子添加组件 new Vue({ router, render: h => h(App), beforeCreate() { Vue.prototy ...

  3. arbitrum 资产桥合约

    资产桥的作用 Rollup 的主要流程中,实际上不包含资产桥,也就是说即使没有资产桥,L2依然能正常运行但是此时L1与L2在数据上是完全独立的两条链,L1不理解L2上的数据(L1只保存L2压缩后的数据 ...

  4. 如何使用图片压缩降低COS流量成本?

    导语 本文将介绍如何通过[图片压缩]能力,让您降本增效的使用 COS ,文章将写得浅显易懂,旨在快速带领用户了解图片压缩的用法及带来的收益. **** 图片压缩为什么会让您降本增效?******** ...

  5. 明察秋毫--用ss工具统计网络栈内存使用

    前言 本文介绍了用ss工具来统计一下当前网络栈的内存使用情况 环境准备 组件 版本 操作系统 Ubuntu 22.04.4 LTS 查看socket内存相关参数,-m 参数 ss -tm State ...

  6. Terraform Aliyun 创建ecs, kubernetes 实例

    Terraform Aliyun 创建ecs, kubernetes 实例 terraform demo for aliyun 创建vpc, 网关, EIP, ecs, kubernetes, Ser ...

  7. 虚拟机安装 Win10 ,无法启动,报错EFI Network ... Time out

    问题情况 VMWare 16 安装 win10 的镜像文件,无法启动,报错 EFI Network ... Time out 解决办法 虚拟机设置中 固件类型 改用 BIOS 已解决

  8. Timestamp和LocalDateTime 互转

    jdk:1.81.Timestamp 转 LocalDateTime Timestamp time = Timestamp.from(Instant.now());LocalDateTime loca ...

  9. 记住我 token保存到数据库

    这里使用jpa+mysql <dependency> <groupId>org.springframework.boot</groupId> <artifac ...

  10. Vue3项目运行时报错误:TypeError Cannot read properties of undefined (reading 'filter')

    let matched = this.$route.mached.filter(item => item.name);方法报错:TypeError Cannot read properties ...