ChimeHQ / LanguageClient

Language Server Protocol (LSP) client for Swift
BSD 3-Clause "New" or "Revised" License
94 stars 10 forks source link

[Request] - Maybe a "LSP 101 - Setup"? #1

Open tikimcfee opened 2 years ago

tikimcfee commented 2 years ago

Hey there, and thanks for your time!

First off, thank you for sharing this repository and work to open up access to the Swift LSP for more folks to use. It's a great effort, and I've been excited to try and use the tool for a while now.

Unfortunately, I'm hitting that oft-expected LSP wall of, "There are so many moving pieces I don't know which piece to learn first to start learning more." Between build caches, local process instances, and quite literally everything in between in the Swift build and runtime, I am finding myself continuously at a loss for how to set this thing up.

My request is for a baby's-first-LSP-call guide or tutorial on how to interact with this client, and how to use it to interact with the server at the other end. Some simple step-by-step guide that shows:

With that corpus of a tutorial, I feel a lot of the other pathways to the LSP are opened. Since setup is a huge portion of the work, and getting into a "working state" is most of the trouble, following those first few requests and gotchas gives a feel for the API surface, and how things are "meant to work".

In any and all ways I can, I offer support to help write this! If that means reading through some of your suggested tutorials or notes and compiling them into a working document, I'm for it. Or, if it means being on a video call and working through individual steps and documenting it, I'm for that too. I am immensely interested in learning this technology and having others be able to do the same, so take very little salt when I say I really want this to work, haha.

Thanks for reading the text wall, and again, thanks for the great library and history of development around SwiftLSP.

mattmassicotte commented 2 years ago

Hello!

Thanks for taking the time to write this up. It's really appreciated. And, your offer to help is awesome!

This library has a bunch of somewhat-composable pieces. The "simplest" one is LocalProcessServer. You do need to figure out the options to provide to actually start it up. But, from there, it isn't super-easy, because LSP has a strict initialization sequence you have to follow.

It might be easier to get going with InitializingServer. This thing is also a Server instance, but it will automatically do the initialization dance behind the scenes. Your job is to provide the initializeParamsProvider, which gives it the info it needs to invoke LocalProcessServer on your behalf.

First, do you have a server you'd like to interact with? Or, a language you'd like to operate on? If not, it could be interesting to see if sourcekit-lsp is installed with Xcode. That could make for an easy intro server.

tikimcfee commented 2 years ago

Awesome, I appreciate that note on the server differences. I did a little more experimentation after my first run through and made a bit of progress. Here's a quick rundown of current state:

I'm tossing a sample class below showing a quick run now that I've got the LocalProcessServer pointing to the right executable. Also the first set of logs from running with this config. I'm totally certain the configuration is wrong, but at least I've got it talking to the right thing.

Next step I'm hoping for is getting that right config into place so the lsp understands what to index and what to read from, although I'm not quite sure if that's the right way to think of this configuration. I'm only aware of a small caveat being when or how the requested files are built to have the compile results cached. If I can get any good response back from it, I feel like I can start putting together a small layer around my main interests - definitions, type lookups.

Let's say I wanted to read a definition from that sample file as a one off request. What do you see as a gross misunderstood of the lib/protocol in the sample below? 😛

class TestLSPStartup {
    let lspPath = "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp"
    let definitionURI = "file:///Users/lugos/udev/manicmind/LookAtThat/Interop/Caches/LockingCache.swift"

    func start(_ done: @escaping () -> Void) {
        let execution = Process.ExecutionParameters(
            path: lspPath,
            arguments: [],
            environment: [:],
            currentDirectoryURL: nil
        )

        let server = LocalProcessServer(executionParameters: execution)
        server.logMessages = true

        let initializer = InitializingServer(server: server)
        initializer.initializeParamsProvider = { initializationReceiver in
            let params = InitializeParams(
                processId: 1,
                rootPath: nil, // nil => "no folder is open"
                rootURI: nil, // "some-document-uri-as-string"
                initializationOptions: nil, // AnyCodable?
                capabilities: ClientCapabilities(
                    workspace: nil, // <#T##ClientCapabilities.Workspace?#>
                    textDocument: nil, // <#T##TextDocumentClientCapabilities?#>
                    window: nil, // <#T##WindowClientCapabilities?#>
                    general: nil, // <#T##GeneralClientCapabilities?#>
                    experimental: nil // <#T##LSPAny#> == AnyCodable?
                ),
                trace: nil, // Tracing?
                workspaceFolders: nil // [WorkspaceFolder]?
            )
            initializationReceiver(.success(params))
        }

        initializer.definition(
            params: .init(
                uri: definitionURI,
                position: Position(line: 1, character: 1)
            ),
            block: { result in
                switch result {
                case .success(let response):
                    switch response {
                    case .optionA(let location):
                        print(location)
                    case .optionB(let locations):
                        print(locations)
                    case .optionC(let locationLinks):
                        print(locationLinks)
                    case .none:
                        print("Success response, no result value")
                    }
                case .failure(let error):
                    print(error)
                }
                done()
            }
        )
    }
}
2022-05-10 18:10:28.985218-0700 [ProtocolTransport] sending: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"processId":1}}
2022-05-10 18:10:29.004963-0700 [ProtocolTransport] received: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"hoverProvider":true,"implementationProvider":true,"colorProvider":true,"codeActionProvider":true,"foldingRangeProvider":true,"documentHighlightProvider":true,"definitionProvider":true,"documentSymbolProvider":true,"executeCommandProvider":{"commands":["semantic.refactor.command"]},"completionProvider":{"resolveProvider":false,"triggerCharacters":["."]},"referencesProvider":true,"textDocumentSync":{"willSave":true,"save":{"includeText":false},"openClose":true,"change":2,"willSaveWaitUntil":false},"workspaceSymbolProvider":true}}}
2022-05-10 18:10:29.006254-0700 [ProtocolTransport] sending: {"jsonrpc":"2.0","method":"initialized","params":{}}
2022-05-10 18:10:29.006573-0700 [ProtocolTransport] sending: {"jsonrpc":"2.0","id":2,"method":"textDocument\/definition","params":{"textDocument":{"uri":"file:\/\/\/Users\/lugos\/udev\/manicmind\/LookAtThat\/Interop\/Caches\/LockingCache.swift"},"position":{"line":1,"character":1}}}
2022-05-10 18:10:29.006942-0700 [ProtocolTransport] received: {"jsonrpc":"2.0","id":2,"result":[]}
2022-05-10 18:10:29.007184-0700 [StdioDataTransport] stderr: [2022-05-10 18:10:29.005] no workspace found
[]
mattmassicotte commented 2 years ago

Ok lots of progress! It's also great to know that sourcekit-lsp in included.

The first problem I see is in your InitializeParams. First, rootPath, rootURI and workspaceFolders are all nil. All the servers I have worked with require at least one of these to be non-nil. This is likely why the server is reporting no workspace found. Try setting rootURI, since it doesn't look like sourcekit-lsp supports workspaceFolders.

Aside: determining what capabilities servers actually have can be tricky, because you are seeing here only statically-available capabilities. Those may be dependent on your client's capabilities. But, servers can also register capabilities dynamically, and have their own dependencies. For example, gopls (the official Go server) will only dynamically register support for semantic tokens, and only after a client responds to a workspaceConfiguration request. I write all this because I know that sourcekit-lsp does support semantic tokens, but it has not reported it to you here.

I'm also unsure if you can get away with all nil ClientCapabilities, but maybe. This will be, like many things with LSP, server-dependent.

Here's the flow you need:

InitializingServer takes care of the first two operations on first interaction, but the next two are up to you. It may be possible to perform queries on documents you have not opened via didOpenTextDocument. LSP mirrors the IDE UI concepts in many ways, so it could be that it is necessary, or again, might be server-dependent.

mattmassicotte commented 2 years ago

Filled in some more details in the README that hopefully help!