Beekeeper Studio UI Kit 的 Language Server Protocol(LSP)集成指南:lsConfig 配置、Helpers API 与底层实现
【免费下载链接】beekeeper-studioModern and easy to use SQL client for MySQL, Postgres, SQLite, SQL Server, and more. Linux, MacOS, and Windows.项目地址: https://gitcode.com/GitHub_Trending/be/beekeeper-studio
导读
Beekeeper Studio UI Kit 是 Beekeeper Studio 开源 SQL 客户端(README.md)的 UI 组件库,其内置的 Text Editor 组件完整支持微软提出的 Language Server Protocol(LSP),让编辑器可以与任意实现了该协议的语言服务器对接,获得智能代码补全、实时诊断、悬停提示、格式化与语义级高亮等能力。本文以 apps/ui-kit/docs/language-server-protocol.md 为骨架,结合仓库内 Text Editor 组件的真实源码(apps/ui-kit/lib/components/text-editor/目录),完整讲解lsConfig配置、bks-lsp-ready事件、LSP Helpers 调用方式、语义令牌与格式化的底层实现,并给出可直接运行的 JavaScript 语言服务器接入示例,帮助你快速在自己的应用中集成一个具备完整 LSP 能力的代码编辑器。
LSP 在 UI Kit 中的定位
Language Server Protocol 定义了一套文本编辑器与语言服务器之间的标准通信协议:语言服务器负责提供与具体语言相关的智能能力(智能补全、错误检查、格式化等),编辑器只需按协议发起请求即可,无需为每种语言单独实现解析逻辑。
Beekeeper Studio UI Kit 将这套能力封装进了bks-text-editor(以及 SQL 场景下的bks-sql-text-editor)组件中。从源码结构看,LSP 支持链路由三部分组成:
- LanguageServerClient.ts:对
@marimo-team/codemirror-languageserver的LanguageServerClient与@open-rpc/client-js的 RPCClient的轻量封装,负责初始化、能力探测与请求转发; - ls.ts:把语言服务器客户端接入 CodeMirror 6 的扩展入口,负责 URI 转换、WebSocket 传输层与各项功能的开关;
- mixin.ts:在 Vue 组件层暴露
ls()方法并转发bks-lsp-ready事件。
其中LanguageServerConfiguration类型定义位于 types.ts。
通过 lsConfig 启用 LSP
启用 LSP 只需要给 Text Editor 组件设置lsConfig属性。当检测到lsConfig存在时,Text Editor 会创建语言服务器客户端并挂载对应扩展(见 TextEditor.ts)。
lsConfig 完整参数说明
| 参数 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
languageId | string | 是 | 文档的语言 ID,例如"javascript"、"typescript"、"sql"。组件初始化时会把它与lsConfig合并后传给语言服务器客户端 |
rootUri | string | 是 | 工作区根目录的本地路径,例如/path/to/project。缺失时组件会直接抛出"Missing 'rootUri' in lsConfig..."错误(见 TextEditor.ts) |
documentUri | string | 是 | 当前文档的本地路径,例如/path/to/project/file.js。同样为必填项,缺失会抛出"Missing 'documentUri' in lsConfig..."错误(见 TextEditor.ts) |
transport | WebSocketTransport或{ wsUri: string } | 是 | WebSocket 传输层。可直接传@open-rpc/client-js的WebSocketTransport实例,或传一个仅含wsUri的普通对象,框架会内部构造传输层 |
timeout | number | 否 | 单次 RPC 请求的超时时间(毫秒),默认10000(10 秒),见 ls.ts 中的TIMEOUT常量 |
features | ExtendedFeatureOptions | 否 | 功能开关,目前支持semanticTokensEnabled(语义令牌开关,默认true,见 utils.ts),其余字段透传给底层 CodeMirror 语言服务器扩展 |
基础配置示例(与原文档一致):
textEditor.lsConfig = { // Language ID (required) languageId: "javascript", // Workspace root URI (required) rootUri: "/path/to/project", // Document URI (required) documentUri: "/path/to/project/file.js", // WebSocket transport (required) transport: { wsUri: "ws://localhost:3000/lsp" }, // Optional timeout in milliseconds timeout: 10000, };关闭语义令牌
若语言服务器不支持语义令牌,或你希望使用更轻量的纯语法高亮,可以通过features显式关闭:
textEditor.lsConfig = { languageId: "sql", rootUri: "/path/to/project", documentUri: "/path/to/project/query.sql", transport: { wsUri: "ws://localhost:3000/sql-lsp" }, features: { semanticTokensEnabled: false, // 默认 true }, };在 ls.ts 中,只有该开关为真时才会向服务器声明semanticTokens客户端能力并挂载对应的语义令牌扩展。
等待 bks-lsp-ready 事件
语言服务器客户端的初始化是异步的(需要完成initialize握手并拿到服务器能力列表)。因此,任何与语言服务器交互的代码都必须等待bks-lsp-ready事件触发后再执行。
textEditor.addEventListener("bks-lsp-ready", (event) => { console.log("Language server ready with capabilities:", event.detail.capabilities); });从源码看,该事件的产生链路是:客户端在 LanguageServerClient.ts 中监听initializePromise,初始化完成后依次回调注册的onReady回调;ls.ts 通过 CodeMirror 的ViewPlugin把这些回调接出来;最终 mixin.ts 的onLspReady回调把它转成bks-lsp-ready自定义事件向外派发。事件详情中的capabilities就是语言服务器在初始化响应中声明的能力对象(类型定义见 types.ts)。
使用 LSP Helpers 主动发请求
除了编辑器在需要时自动向语言服务器发送请求(例如补全、诊断),你还可以通过textEditor.ls()获取的 helpers 主动发起格式化、语义令牌与自定义命令请求。
textEditor.addEventListener("bks-lsp-ready", async () => { // Get the language server helpers const helpers = textEditor.ls(); // Request a document formatting and apply it await helpers.formatDocument({ tabSize: 2, insertSpaces: true }); // Get the language server client const client = helpers.getClient(); // Request a custom command to the language server await client.request({ method: "workspace/executeCommand", params: { command: "fixAllFixableProblems" }, }); })ls()方法在 mixin.ts 中定义,实际返回的 helpers 对象由 TextEditor.ts 的getLsHelpers()构造,接口类型为 types.ts 中的LanguageServerHelpers。
Helpers 方法一览
| 方法 | 说明 | 参数 |
|---|---|---|
getClient() | 返回语言服务器客户端实例(LanguageServerClient),可用于发送任意 LSP 请求 | 无 |
formatDocument() | 格式化整个文档 | options: LSP.FormattingOptions |
formatDocumentRange() | 格式化文档中指定区间 | range: LSP.Range, options: LSP.FormattingOptions |
requestSemanticTokens() | 请求语义令牌并应用到文档,返回结果 ID | lastResultId?: string |
LSP.FormattingOptions
| 属性 | 类型 | 说明 |
|---|---|---|
tabSize | uinteger | 一个制表符占用的空格数 |
insertSpaces | boolean | 是否优先使用空格代替制表符 |
trimTrailingWhitespace | boolean? | 是否裁剪行尾空白 |
insertFinalNewline | boolean? | 文件末尾无换行时是否补一个换行 |
trimFinalNewlines | boolean? | 是否裁剪文件末尾换行之后的多余换行 |
[key: string] | boolean \| integer \| string \| undefined | 允许携带额外属性 |
LSP.Range 与 LSP.Position
格式化区间使用 LSP 标准坐标(均从 0 开始计数):
| 属性 | 类型 | 说明 |
|---|---|---|
Range.start | LSP.Position | 起始位置 |
Range.end | LSP.Position | 结束位置;如需包含行尾,可将下一行行首作为结束点 |
Position.line | number | 行号(从 0 开始) |
Position.character | number | 字符偏移(从 0 开始) |
LanguageServerClient 公开接口
通过helpers.getClient()拿到的客户端(详见 language-server-client.md 与 LanguageServerClient.ts)提供以下能力:
| 名称 | 类型 | 说明 |
|---|---|---|
ready | boolean | 客户端是否已初始化完成 |
request() | Promise<any> | 向语言服务器发送请求,参数为{ method, params },可选覆盖超时时间 |
onReady() | void | 注册就绪回调;若已就绪则立即调用 |
getCapabilities() | object | 返回服务器能力;未就绪时可能为null |
extension() | Extension[] | 用当前客户端创建一个 CodeMirror 扩展 |
可用功能特性
LSP 集成默认支持以下能力(由 ls.ts 挂载的 CodeMirror 语言服务器扩展提供):
- Code Completion(代码补全):输入时给出智能代码建议。值得注意的是 ls.ts 将
completionMatchBefore设为/.{0}/,即光标前任意位置都能触发手动补全; - Diagnostics(诊断):实时错误与警告高亮;
- Hover Information(悬停提示):悬停符号时展示文档与类型信息;
- Formatting(格式化):应用来自语言服务器的格式化规则(整文档与区间两种);
- Signature Help(签名帮助):函数调用时显示参数信息;
- Semantic Tokens(语义令牌):基于语义信息的增强语法高亮。
其中格式化能力由 UI Kit 自身在客户端能力中声明(textDocument.formatting与textDocument.rangeFormatting的动态注册,见 formatting.ts),语义令牌则声明了 23 种 token 类型与 10 种修饰符(见 semanticTokens.ts)。
实战示例:接入 JavaScript 语言服务器
完整 HTML 示例
下面是文档给出的完整接入示例:创建一个bks-text-editor,设置内容、配置 LSP 并监听就绪事件。
<bks-text-editor id="js-editor"></bks-text-editor> <script> const jsEditor = document.getElementById("js-editor"); // Set content jsEditor.value = `function hello(name) { return "Hello, " + name; }`; // Configure language server jsEditor.lsConfig = { languageId: "javascript", rootUri: "/path/to/project", documentUri: "/path/to/project/script.js", transport: { wsUri: "ws://localhost:3000/javascript-language-server" }, }; // Listen for LSP ready event jsEditor.addEventListener("bks-lsp-ready", (event) => { console.log("Language server ready with capabilities:", event.detail.capabilities); }); </script>仓库自带的真实可运行示例位于 examples/html/main.js,其中bks-sql-text-editor以 TypeScript 语言服务器为例,配置了ws://localhost:3000/server的传输地址、rootUri指向apps/ui-kit/tests/fixtures/目录、documentUri指向其中的test.sql文件,可作为接入参考。
搭建语言服务器
要使用 LSP 功能,你需要运行一个 Text Editor 能连接到的语言服务器。以 JavaScript/TypeScript 语言服务器为例:
- 安装语言服务器:
npm install -g typescript-language-server typescript - 以支持 WebSocket 的方式启动语言服务器(通常需要额外工具把语言服务器暴露为 WebSocket 端点)。
说明:UI Kit 通过 WebSocket 与语言服务器通信,因此需要一个能接受 WebSocket 连接的桥接服务把标准的 stdio 语言服务器转发到
ws://端点。这是接入前的必要前提。
高级用法:直接使用 WebSocketTransport
当需要更精细地控制 WebSocket 连接时,可以跳过{ wsUri }普通对象,直接使用@open-rpc/client-js的WebSocketTransport实例:
import { WebSocketTransport } from '@open-rpc/client-js'; const transport = new WebSocketTransport("ws://localhost:3000/server"); textEditor.lsConfig = { languageId: "javascript", rootUri: "/path/to/project", documentUri: "/path/to/project/file.js", transport, };从 ls.ts 的实现看,两种形式等价:若传入的对象含有wsUri字段,框架内部同样会构造WebSocketTransport;若传入的已是WebSocketTransport实例则直接复用。
深入底层:ls() 扩展的关键实现
URI 规范化与工作区声明
ls.ts 会把配置里的本地路径rootUri、documentUri通过vscode-uri的URI.file()转成file://形式的 URI,再以workspaceFolders: [{ name: "workspace", uri: rootUri }]声明工作区,保证与语言服务器的 URI 约定一致。
workspace/configuration 请求的兼容处理
ls.ts 中有一段值得关注的兼容逻辑:底层语言服务器库不处理workspace/configuration(由服务器发往客户端的请求)。如果客户端不响应,某些服务器(如 sql-language-server)会直接罢工。因此 UI Kit 在传输层拦截消息,一旦识别到workspace/configuration请求,就自动回填result: [null]作为应答,避免服务器挂起。
格式化的防抖与文本编辑应用
formatting.ts 实现了整文档与区间两种格式化:
- 每次格式化调用前会先清除上一次的定时器,再用
100ms防抖合并高频触发(FORMAT_DEBOUNCE_TIME); - 请求方法分别为
textDocument/formatting与textDocument/rangeFormatting; - 拿到服务器的
TextEdit[]后,通过posToOffset()把 LSP 的零基行列坐标换算成 CodeMirror 文档偏移量,再以view.dispatch({ changes })原子应用(见 formatting.ts 与 utils.ts)。
语义令牌的节流与增量刷新
semanticTokens.ts 对语义令牌做了较完整的工程化处理:
- 以
500ms节流合并高频的令牌请求(SEMANTIC_TOKENS_THROTTLE_TIME); - 客户端就绪且服务器声明
semanticTokensProvider时自动发起首次请求; - 文档变更(
update.docChanged)后自动重新请求,并携带上次的resultId优先走textDocument/semanticTokens/full/delta增量通道;服务器不支持 delta 或请求失败时回退到textDocument/semanticTokens/full全量请求; - 令牌数据按 LSP 规范每 5 个整数为一组解码(
deltaLine, deltaChar, length, tokenType, tokenModifiers),换算为绝对行列后用Decoration.mark生成形如cm-semanticToken-<type>、cm-semanticToken-<type>-<modifier>的 CSS 类名; - 若服务器能力中缺少 legend,会使用内置的 23 种 token 类型与 10 种修饰符作为回退 legend(见 semanticTokens.ts),其样式由 text-editor.scss 中的 CSS 变量统一控制。
组件 API 与更多文档
- Text Editor API 文档:
lsConfig属性及 Text Editor 全部公开 API; - Language Server Helpers API:
formatDocument、formatDocumentRange、requestSemanticTokens的完整签名与类型表; - Language Server Client API:
request()、onReady()、getCapabilities()、extension()等客户端方法; - Text Editor 文档:Text Editor 组件总体说明;
- 核心源码:入口配置 TextEditor.ts、客户端封装 LanguageServerClient.ts、类型定义 types.ts、Vue 桥接 mixin.ts 与 props.ts、LSP 扩展目录 extensions/ls/;
- 可运行示例:examples/html/main.js。
【免费下载链接】beekeeper-studioModern and easy to use SQL client for MySQL, Postgres, SQLite, SQL Server, and more. Linux, MacOS, and Windows.项目地址: https://gitcode.com/GitHub_Trending/be/beekeeper-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考