1258 字
6 分钟
为 Sora Editor 对接 LSP

什么是 LSP (Language Server Protocol)?#

语言服务器协议 (Language Server Protocol 即 LSP) 定义了编辑器或 IDE 与语言服务器之间使用的协议,该语言服务器提供自动完成、跳转到定义、查找所有引用等语言功能。语言服务器索引格式 (LSIF,发音类似“else if”) 的目标是在开发工具或 Web UI 中支持丰富的代码导航,而无需源代码的本地副本。

更多信息详见:Language Server Protocol

说人话就是一个标准协议,实现了客户端(IDE)的代码自动补全,引用跳转等智能功能。

为什么要使用 LSP?#

首先,一个所谓的真正的 IDE,不能仅仅有基础的代码补全(关键字补全,常用函数补全等),还需要有智能补全。智能补全即实现了跳转到定义,上下文推断补全等。

实现上述步骤并不是只有 LSP 才能做到,有人说我可以自己使用现成的 Parser,对每个文件进行语法分析。然后自己写一套上下文补全。这确实可以,但是很费时间。

LSP 有着开箱即用的特点:无需关注其内部实现,而 IDE 只需要根据它定义的标准进行通信,即可实现目的。

其次,LSP 的产生也是为了解决协议不一致问题。在 LSP 出现以前,每个 IDE 的补全核心实现都不一致。我的 IDE 中转到定义可能是需要十个参数,你的可能是五个参数,并且参数类型各不相同。这就导致重复开发,要费太多时间了。

随着 LSP 的诞生,定义了一套标准协议。现在大家就照这个协议去做就可以,大家都能受益。

LSP 的实现有哪些?#

LSP 并不是只能用一个特定语言实现,而是可以多种。 Language Server Protocol Implementations 给出了各类语言的 LSP 实现以及其实现所使用的语言。

了解了这些以后我们就可以开始正式为 Sora Editor 对接 LSP 了。

如何为 Sora Editor 对接 LSP?#

首先先编译你自己的 LSP Server 软件,然后我们可以使用 ProcessBuilder 去运行它。 由于大部分 LSP 都是通过 stdio 和 stdout 进行通信,因此我们需要向 ProcessBuilder 产生的 Process 写入协议内容。 Sora Editor 已经为我们内置好了相关接口,我们可以通过继承 StreamConnectionProvider 类来处理相关逻辑。你可以参考如下源码(这里以 TypeScript Language Server 为例):

class ProcessStreamConnectionProvider(val context: Context) : StreamConnectionProvider {
// 这里创建我们的 ProcessBuilder, 指定需要运行的 LSP 服务器软件为 /data/user/0/你的包名/files/usr/bin/typescript-language-server
private val processBuilder = ProcessBuilder(context.filesDir.resolve("usr/bin").resolve("typescript-language-server").absolutePath, "--stdio")
.apply {
// 这里设置默认的进程环境,并向 PATH、LD_LIBRARY_PATH 设置 typescript-language-server 当前文件夹路径和所需库文件路径
val env = environment()
env.putAll(System.getenv())
env["PATH"] = "${System.getenv("PATH")}:${context.filesDir.resolve("usr/bin")}"
env["LD_LIBRARY_PATH"] = "${System.getenv("LD_LIBRARY_PATH")}:${context.filesDir.resolve("usr/lib")}"
env["OPENSSL_CONF"] = ""
}
// 重定向 stderr 到 stdout
.redirectErrorStream(true)
private lateinit var process: Process
override val inputStream: InputStream
get() = process.inputStream
override val outputStream: OutputStream
get() = process.outputStream
override fun start() {
process = processBuilder.start()
}
override fun close() {
process.destroy()
}
}

接下来是注册主题和语言配置,你可以在 Application 中完成配置:

class AppContext : Application() {
companion object {
lateinit var instance: AppContext
private set
}
override fun onCreate() {
super.onCreate()
// ... 其它代码
// 添加 AssetsFileResolver,便于直接引用 assets 中的文件
FileProviderRegistry.addProvider(AssetsFileResolver(assets))
// 从 assets 中加载主题
ThemeRegistry.loadTheme(ThemeModel(ThemeSource("theme/quietlight.json", "quietlight")))
// 向 Monarch 注册表加载支持语言的语法配置
MonarchGrammarRegistry.INSTANCE.loadGrammars(monarchLanguages {
language("typescript") {
monarchLanguage = TypescriptLanguage
scopeName = "source.ts"
languageConfiguration = "textmate/typescript/language-configuration.json"
}
})
// ... 其它代码
}
}

接下来只需要应用主题、配置默认语言就行:

val codeEditor = CodeEditor(context).apply {
// 设置 Jetbrains 等宽字体,你也可以换成你自己喜欢的
val typeface =
Typeface.createFromAsset(context.assets, "font/JetBrainsMono-Regular.ttf")
// 给编辑器行号和内容进行应用
typefaceLineNumber = typeface
typefaceText = typeface
// 设置编辑器配色方案
colorScheme = MonarchColorScheme.create(ThemeRegistry.currentTheme)
}

最后一步,连接 LSP:

// 启动一个 Kotlin 协程
CoroutineScope(Dispatchers.IO).launch {
// 可以切换到主线程做一些准备工作
withContext(Dispatchers.Main) {
context.toast("(Kotlin Activity) Starting Language Server...")
codeEditor.isEditable = false
}
// 创建 LanguageServerDefinition
val serverDefinition =
object : CustomLanguageServerDefinition(
"ts", // 这里指定你的目标语言
ServerConnectProvider {
ProcessStreamConnectionProvider(context) // 这个是之前配置的 ProcessStreamConnectionProvider
}
) {}
val typescriptLspDemoFolder = context.filesDir.resolve("typescript-lsp-demo")
val mainFile = typescriptLspDemoFolder.resolve("index.ts")
val lspProject = LspProject(typescriptLspDemoFolder.absolutePath)
lspProject.addServerDefinition(serverDefinition)
var lspEditor: LspEditor
withContext(Dispatchers.Main) {
// 创建 LspEditor
lspEditor = lspProject.createEditor(mainFile.absolutePath)
lspEditor.wrapperLanguage = MonarchLanguage.create("source.ts", true)
lspEditor.editor = codeEditor
lspEditor.isEnableInlayHint = true // 是否启用镶嵌提示
lspEditor.isEnableHover = true // 是否启用悬停
lspEditor.isEnableSignatureHelp = true // 是否启用签名提示
}
// connectWithTimeout() 在 10s 内尝试连接(失败会重试),如果超时会抛出异常
try {
lspEditor.connectWithTimeout()
// 给 LSP Server 指定切换工作目录
lspEditor.requestManager.didChangeWorkspaceFolders(
DidChangeWorkspaceFoldersParams().apply {
this.event = WorkspaceFoldersChangeEvent().apply {
added =
listOf(
WorkspaceFolder(
"file://${typescriptLspDemoFolder}",
"typescript-lsp-demo"
)
)
}
}
)
isLspConnected = true
} catch (e: Exception) {
isLspConnected = false
e.printStackTrace()
}
// 连接成功或失败后进行一些工作
withContext(Dispatchers.Main) {
if (isLspConnected) {
context.toast("Initialized Language server")
} else {
context.toast("Unable to connect language server")
}
codeEditor.editable = true
}
}

TypeScript LSP Demo#

你可以在这个仓库中查看本 Demo 的所有代码:

mucute-qwq
/
TypeScript-LSP-Demo
Waiting for api.github.com...
00K
0K
0K
Waiting...
为 Sora Editor 对接 LSP
https://mucute-qwq.github.io/posts/integrate-lsp-for-sora-editor/
作者
一剪沐橙
发布于
2026-01-10
许可协议
CC BY-NC-SA 4.0