这是一个 Swift 库,用于抽象和与实现语言服务器协议的语言服务器进行交互。它构建在 LanguageServerProtocol 库之上。
这个库完全基于 LanguageServerProtocol 中的 ServerConnection
协议。其思想是封装并公开逐渐复杂的行为。这有助于保持代码的可管理性,同时为要求较低的需求提供复杂度较低的类型。这也是我尝试的第一个效果还不错的方案。
由于这里的各种类型都遵循 ServerConnection
,因此它们的许多功能都包含在 LanguageServerProtocol 的文档中。这包括通过 eventSequence
访问服务器事件。
服务器和客户端之间的原始通信由 JSONRPC 包中的 DataChannel
类型处理。这个包包含两个可能已经满足您的需求:
DataChannel.localProcessChannel
:在同一台机器上本地运行服务器DataChannel.userScriptDirectory
:使用 NSUserUnixTask
来支持用户应用程序脚本,以便更好地与沙盒进程集成在创建自定义 DataChannel 时,真正重要的是确保所有数据都双向传递,包括特定于 LSP 的成帧信息。这种成帧类似于 HTTP 头部,看起来可能不太合适。
设置正确的环境变量对于语言服务器通常至关重要。 macOS 上的可执行文件将不会继承用户的 shell 环境。捕获 shell 环境变量是一件棘手的事情。 尽管名称如此,ProcessInfo.processInfo.userEnvironment
捕获的是process
环境,而不是用户的。
如果您需要帮助,请查看 ProcessEnv。
语言服务器协议是有状态的。某些消息类型具有顺序依赖性。在使用 async
方法时,您必须注意这一点。我发现队列至关重要。如果需要,可以看看 这个。
这是在没有额外功能的情况下运行本地服务器的方式。 它使用 JSONRPC DataChannel
类型上的扩展来启动并与长时间运行的进程进行通信。
// Set up parameters to launch the server process
let params = Process.ExecutionParameters(
path: "/path/to/server-executable",
arguments: [],
environment: ProcessInfo.processInfo.userEnvironment
)
// create a DataChannel to handle communication
let channel = try DataChannel.localProcessChannel(
parameters: params,
terminationHandler: { print("terminated") }
)
// finally, make a server you can interact with
let server = JSONRPCServerConnection(dataChannel: channel)
提供自动初始化的 Server
包装器。 它负责协议初始化握手,并以延迟的方式在第一个消息上进行。
import LanguageClient
import LanguageServerProtocol
import Foundation
let executionParams = Process.ExecutionParameters(
path: "/usr/bin/sourcekit-lsp",
environment: ProcessInfo.processInfo.userEnvironment
)
let channel = try DataChannel.localProcessChannel(
parameters: executionParams,
terminationHandler: { print("terminated") }
)
let localServer = JSONRPCServerConnection(dataChannel: channel)
let docURL = URL(fileURLWithPath: "/path/to/your/test.swift")
let projectURL = docURL.deletingLastPathComponent()
let provider: InitializingServer.InitializeParamsProvider = {
// you may need to fill in more of the textDocument field for completions
// to work, depending on your server
let capabilities = ClientCapabilities(workspace: nil,
textDocument: nil,
window: nil,
general: nil,
experimental: nil)
// pay careful attention to rootPath/rootURI/workspaceFolders, as different servers will
// have different expectations/requirements here
return InitializeParams(processId: Int(ProcessInfo.processInfo.processIdentifier),
locale: nil,
rootPath: nil,
rootUri: projectURL.path(percentEncoded: false),
initializationOptions: nil,
capabilities: capabilities,
trace: nil,
workspaceFolders: nil)
}
let server = InitializingServer(server: localServer, initializeParamsProvider: provider)
Task {
let docContent = try String(contentsOf: docURL)
let doc = TextDocumentItem(
uri: docURL.absoluteString,
languageId: .swift,
version: 1,
text: docContent
)
let docParams = DidOpenTextDocumentParams(textDocument: doc)
try await server.textDocumentDidOpen(params: docParams)
// make sure to pick a reasonable position within your test document
let pos = Position(line: 5, character: 25)
let completionParams = CompletionParams(
uri: docURL.absoluteString,
position: pos,
triggerKind: .invoked,
triggerCharacter: nil
)
let completions = try await server.completion(params: completionParams)
print("completions: ", completions)
}
Server
包装器,如果底层进程崩溃,它提供透明的服务器端状态恢复。 它在内部使用 InitializingServer
。 使用这种类型是最复杂的,因为它需要能够查询项目编辑器的当前状态以进行状态恢复。
import LanguageClient
import LanguageServerProtocol
import JSONRPC
typealias MyRestartingServer = RestartingServer<JSONRPCServerConnection>
let executionParams = Process.ExecutionParameters(
path: "/usr/bin/sourcekit-lsp",
environment: ProcessInfo.processInfo.userEnvironment
)
let projectURL = URL(fileURLWithPath: "path/to/open/project")
let serverProvider: MyRestartingServer.ServerProvider = {
let channel = try DataChannel.localProcessChannel(
parameters: executionParams,
terminationHandler: { print("terminated") }
)
return JSONRPCServerConnection(dataChannel: channel)
}
let openDocumentProvider: MyRestartingServer.TextDocumentItemProvider = { uri in
// you will have to use the provided uri to look up the actual content of the real document
return TextDocumentItem(
uri: uri,
languageId: "swift",
version: 1,
text: "contents of file"
)
}
let paramProvider: InitializingServer.InitializeParamsProvider = {
// most of these are placeholders, you will probably need more configuration
let capabilities = ClientCapabilities(
workspace: nil,
textDocument: nil,
window: nil,
general: nil,
experimental: nil
)
return InitializeParams(
processId: Int(ProcessInfo.processInfo.processIdentifier),
locale: nil,
rootPath: nil,
rootUri: projectURL.path(percentEncoded: false),
initializationOptions: nil,
capabilities: capabilities,
trace: nil,
workspaceFolders: nil
)
}
let config = MyRestartingServer.Configuration(
serverProvider: serverProvider,
textDocumentItemProvider: openDocumentProvider,
initializeParamsProvider: paramProvider
)
let server = MyRestartingServer(configuration: config)
一个使用 FS 事件和 glob 模式来处理 DidChangeWatchedFiles
的 AsyncSequence
。 它仅适用于 macOS。
您可以使用 eventSequence
响应服务器事件。 在这里要小心,因为有些服务器需要对某些请求进行响应。 还有一种可能,并非所有请求类型都已在 LanguageServerProtocol 中的 ServerRequest 类型中进行了映射。
Task {
for await event in server.eventSequence {
print("receieved event:", event)
switch event {
case let .request(id: id, request: request):
request.relyWithError(MyError.unsupported)
default:
print("dropping notification/error")
}
}
}
我们很乐意收到您的来信! 通过 issue 或 pull request 联系我们。
请注意,此项目已使用贡献者行为准则发布。 参与此项目即表示您同意遵守其条款。