什么是 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 的所有代码: