我们是袋鼠云数栈 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. GZY.EFCore.BulkExtensions 支持达梦数据库的EF Core批量操作库详解

    前言 EFCore.BulkExtensions是一个常用的EF core 批量处理数据的库. 但是支持的数据库相对较少.特别是.NET5.0版本 连MySQL都无法支持 这个库就是改造的最新EFCo ...

  2. vue3 中屏蔽控制台中的警告信息

    main.js中 const app = Vue.createApp({}); // 屏蔽错误信息 app.config.errorHandler = () => null; // 屏蔽警告信息 ...

  3. uView的DatetimePicker组件在confirm回调中取不到v-model的最新值

    前情 uni-app是我比较喜欢的跨平台框架,它能开发小程序/H5/APP(安卓/iOS),重要的是对前端开发友好,自带的IDE让开发体验非常棒,公司项目就是主推uni-app,在uniapp生态中u ...

  4. 数据湖加速器GooseFS,加速湖上数据分析性能

    数据湖加速器 GooseFS 是由腾讯云推出的高性能.高可用.弹性的分布式缓存方案.依靠对象存储(Cloud Object Storage,COS)作为数据湖存储底座的成本优势,为数据湖生态中的计算应 ...

  5. 使用腾讯云对象存储 COS 作为 Velero 后端存储,实现集群资源备份和还原

    Velero(以前称为 Heptio Ark)是一个开源工具,可以安全地备份和还原,执行灾难恢复以及迁移 Kubernetes 集群资源和持久卷,可以在 TKE 集群或自建 Kubenetes 集群中 ...

  6. nodejs koa2 ocr识别 身份证信息

    1. 安装依赖 npm install baidu-aip-sdk 2.创建AipOcrClient 注:需要到百度api创建应用,拿到所需的APPID/AK/SK https://console.b ...

  7. DBeaver 不错大家都来用 DBeaver 吧

    支持 windows linux 支持 pg 等 n 多数据库

  8. 【数据结构】【直接排序法】Java代码

    public class 直接排序 { /** * 直接排序法 仅排序1轮 * @param arr 数组 * @param ji 基准索引,填写几,就以谁为基准进行一次划分 */ public st ...

  9. Java代码覆盖率super-jacoco

    开源项目地址 https://gitee.com/didiopensource/super-jacoco 项目流程 项目架构 部署步骤 注意:一定要用Linux服务器部署,不要用Windows 准备L ...

  10. 若依管理系统 -- 更换头像上传文件报错(/tmp/tomcat.8013579853364800617.8080/work/Tomcat/localhost/ROOT)

    一.错误情况 1.错误截图 二.错误解决情况 1.若依官方解答的链接 2.若依解答原文 1)原因: 在linux系统中,springboot应用服务再启动(java -jar 命令启动服务)的时候,会 ...