Build Status Platforms Documentation Discord

LanguageClient

这是一个 Swift 库,用于抽象和与实现语言服务器协议的语言服务器进行交互。它构建在 LanguageServerProtocol 库之上。

总体设计

这个库完全基于 LanguageServerProtocol 中的 ServerConnection 协议。其思想是封装并公开逐渐复杂的行为。这有助于保持代码的可管理性,同时为要求较低的需求提供复杂度较低的类型。这也是我尝试的第一个效果还不错的方案。

由于这里的各种类型都遵循 ServerConnection,因此它们的许多功能都包含在 LanguageServerProtocol 的文档中。这包括通过 eventSequence 访问服务器事件。

通信

服务器和客户端之间的原始通信由 JSONRPC 包中的 DataChannel 类型处理。这个包包含两个可能已经满足您的需求:

在创建自定义 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)

InitializingServer

提供自动初始化的 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)
}

RestartingServer

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)

FileEventAsyncSequence

一个使用 FS 事件和 glob 模式来处理 DidChangeWatchedFilesAsyncSequence。 它仅适用于 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 联系我们。

请注意,此项目已使用贡献者行为准则发布。 参与此项目即表示您同意遵守其条款。