nextflow-io / language-server

The Nextflow language server
Apache License 2.0
9 stars 0 forks source link

What is needed to use this with other lsp clients? #43

Closed Lenbok closed 1 week ago

Lenbok commented 3 weeks ago

I tried to connect this language server up in emacs, using lsp-mode, and communication is established between the client and server fine. However when editing one of the examples in the nextflow-patterns repo, my lsp client sends various messages to the server but nothing of interest comes back.

What configuration settings does the language server expect?

e.g., when I trace the server initialization, I get:

[Trace - 06:00:34 PM] Sending request 'initialize - (1)'.
Params: {
  "processId": 81477,
  "rootPath": "/home/len/dev/nextflow-patterns",
  "clientInfo": {
    "name": "emacs",
    "version": "GNU Emacs 29.4.50 (build 2, x86_64-pc-linux-gnu, GTK+ Version 2.24.33, cairo version 1.18.0)\n of 2024-08-02"
  },
  "rootUri": "file:///home/len/dev/nextflow-patterns",
  "capabilities": {
    "general": {
      "positionEncodings": [
        "utf-32",
        "utf-16"
      ]
    },
    "workspace": {
      "workspaceEdit": {
        "documentChanges": true,
        "resourceOperations": [
          "create",
          "rename",
          "delete"
        ]
      },
      "applyEdit": true,
      "symbol": {
        "symbolKind": {
          "valueSet": [
            1,
            2,
            3,
            4,
            5,
            6,
            7,
            8,
            9,
            10,
            11,
            12,
            13,
            14,
            15,
            16,
            17,
            18,
            19,
            20,
            21,
            22,
            23,
            24,
            25,
            26
          ]
        }
      },
      "executeCommand": {
        "dynamicRegistration": false
      },
      "didChangeWatchedFiles": {
        "dynamicRegistration": true
      },
      "workspaceFolders": true,
      "configuration": true,
      "codeLens": {
        "refreshSupport": true
      },
      "fileOperations": {
        "didCreate": false,
        "willCreate": false,
        "didRename": true,
        "willRename": true,
        "didDelete": false,
        "willDelete": false
      }
    },
    "textDocument": {
      "declaration": {
        "dynamicRegistration": true,
        "linkSupport": true
      },
      "definition": {
        "dynamicRegistration": true,
        "linkSupport": true
      },
      "references": {
        "dynamicRegistration": true
      },
      "implementation": {
        "dynamicRegistration": true,
        "linkSupport": true
      },
      "typeDefinition": {
        "dynamicRegistration": true,
        "linkSupport": true
      },
      "synchronization": {
        "willSave": true,
        "didSave": true,
        "willSaveWaitUntil": true
      },
      "documentSymbol": {
        "symbolKind": {
          "valueSet": [
            1,
            2,
            3,
            4,
            5,
            6,
            7,
            8,
            9,
            10,
            11,
            12,
            13,
            14,
            15,
            16,
            17,
            18,
            19,
            20,
            21,
            22,
            23,
            24,
            25,
            26
          ]
        },
        "hierarchicalDocumentSymbolSupport": true
      },
      "formatting": {
        "dynamicRegistration": true
      },
      "rangeFormatting": {
        "dynamicRegistration": true
      },
      "onTypeFormatting": {
        "dynamicRegistration": true
      },
      "rename": {
        "dynamicRegistration": true,
        "prepareSupport": true
      },
      "codeAction": {
        "dynamicRegistration": true,
        "isPreferredSupport": true,
        "codeActionLiteralSupport": {
          "codeActionKind": {
            "valueSet": [
              "",
              "quickfix",
              "refactor",
              "refactor.extract",
              "refactor.inline",
              "refactor.rewrite",
              "source",
              "source.organizeImports"
            ]
          }
        },
        "resolveSupport": {
          "properties": [
            "edit",
            "command"
          ]
        },
        "dataSupport": true
      },
      "completion": {
        "completionItem": {
          "snippetSupport": true,
          "documentationFormat": [
            "markdown",
            "plaintext"
          ],
          "resolveAdditionalTextEditsSupport": true,
          "insertReplaceSupport": true,
          "deprecatedSupport": true,
          "resolveSupport": {
            "properties": [
              "documentation",
              "detail",
              "additionalTextEdits",
              "command"
            ]
          },
          "insertTextModeSupport": {
            "valueSet": [
              1,
              2
            ]
          }
        },
        "contextSupport": true,
        "dynamicRegistration": true
      },
      "signatureHelp": {
        "signatureInformation": {
          "parameterInformation": {
            "labelOffsetSupport": true
          }
        },
        "dynamicRegistration": true
      },
      "documentLink": {
        "dynamicRegistration": true,
        "tooltipSupport": true
      },
      "hover": {
        "contentFormat": [
          "markdown",
          "plaintext"
        ],
        "dynamicRegistration": true
      },
      "foldingRange": {
        "dynamicRegistration": true
      },
      "selectionRange": {
        "dynamicRegistration": true
      },
      "callHierarchy": {
        "dynamicRegistration": false
      },
      "typeHierarchy": {
        "dynamicRegistration": true
      },
      "publishDiagnostics": {
        "relatedInformation": true,
        "tagSupport": {
          "valueSet": [
            1,
            2
          ]
        },
        "versionSupport": true
      },
      "diagnostic": {
        "dynamicRegistration": false,
        "relatedDocumentSupport": false
      },
      "linkedEditingRange": {
        "dynamicRegistration": true
      }
    },
    "window": {
      "workDoneProgress": true,
      "showDocument": {
        "support": true
      }
    }
  },
  "initializationOptions": null,
  "workDoneToken": "1",
  "workspaceFolders": [
    {
      "uri": "file:///home/len/dev/nextflow-patterns",
      "name": "nextflow-patterns"
    }
  ]
}

[Trace - 06:00:35 PM] Received response 'initialize - (1)' in 1007ms.
Result: {
  "capabilities": {
    "textDocumentSync": 2,
    "hoverProvider": true,
    "completionProvider": {
      "resolveProvider": null,
      "triggerCharacters": [
        "."
      ]
    },
    "definitionProvider": true,
    "referencesProvider": true,
    "documentSymbolProvider": true,
    "workspaceSymbolProvider": true,
    "codeLensProvider": {
      "resolveProvider": null
    },
    "documentFormattingProvider": true,
    "renameProvider": true,
    "documentLinkProvider": {
      "resolveProvider": null
    },
    "executeCommandProvider": {
      "commands": [
        "nextflow.server.previewDag"
      ]
    },
    "workspace": {
      "workspaceFolders": {
        "supported": true,
        "changeNotifications": true
      }
    },
    "callHierarchyProvider": true
  }
}

[Trace - 06:00:35 PM] Sending notification 'initialized'.
Params: {}

[Trace - 06:00:35 PM] Sending notification 'textDocument/didOpen'.
Params: {
  "textDocument": {
    "uri": "file:///home/len/dev/nextflow-patterns/process-when-empty.nf",
    "languageId": "nextflow",
    "version": 0,
    "text": "#!/usr/bin/env nextflow\n\n/*\n * Copyright (c) 2018, Centre for Genomic Regulation (CRG).\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n * \n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n * \n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n /*\n  * author Paolo Di Tommaso <paolo.ditommaso@gmail.com> \n  */\n\nparams.inputs = ''\n\nprocess foo {\n  debug true  \n  input:\n  val x\n  when:\n  x == 'EMPTY'\n\n  script:\n  '''\n  echo hello\n  ''' \n}\n\nworkflow {\n  reads_ch = params.inputs\n    ? Channel.fromPath(params.inputs, checkIfExists:true)\n    : Channel.empty()\n\n  reads_ch \\\n    | ifEmpty { 'EMPTY' } \\\n    | foo\n}\n"
  }
}

but e.g. jump to definition requests come back empty:

[Trace - 06:19:30 PM] Sending request 'textDocument/definition - (148)'.
Params: {
  "textDocument": {
    "uri": "file:///home/len/dev/nextflow-patterns/process-when-empty.nf"
  },
  "position": {
    "line": 48,
    "character": 8
  }
}

[Trace - 06:19:30 PM] Received response 'textDocument/definition - (148)' in 1ms.
Result: []
[Trace - 06:20:51 PM] Sending request 'textDocument/definition - (152)'.
Params: {
  "textDocument": {
    "uri": "file:///home/len/dev/nextflow-patterns/process-when-empty.nf"
  },
  "position": {
    "line": 50,
    "character": 7
  }
}

[Trace - 06:20:51 PM] Received response 'textDocument/definition - (152)' in 1ms.
Result: []
Lenbok commented 3 weeks ago

If anyone wants to play along, here's my emacs nextflow use-package declaration that attempts to set up this server under both eglot and lsp-mode language server clients. (They call via a bash wrapper script nextflow-language-server that just calls java -jar on the language server jarfile I downloaded from this repo).

(use-package nextflow-mode
  :ensure (nextflow-mode :host github :repo "edmundmiller/nextflow-mode" :branch "master")
  :config
  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs
                 '(nextflow-mode . ("nextflow-language-server"))))

  (with-eval-after-load 'lsp-mode
    (defvar lsp-nextflow-server-command '("nextflow-language-server"))
    (lsp-register-client
     (make-lsp-client
      :new-connection (lsp-stdio-connection (lambda () lsp-nextflow-server-command))
      :major-modes '(nextflow-mode)
      :multi-root t
      :server-id 'nextflow-lsp))
    (add-to-list 'lsp-language-id-configuration '(nextflow-mode . "nextflow"))
    (lsp-consistency-check lsp-nextflow)))
bentsherman commented 3 weeks ago

Tagging @edmundmiller since he is also trying to set up the language client for emacs

edmundmiller commented 3 weeks ago

Ooo!

I've got the lsp-mode download working with the vsix from the other repo. I think we can combine these and make a PR

edmundmiller commented 3 weeks ago

We could probably use this repo, but it would need CI set up and no guarantee the release numbers stay in sync.

Whatever @bentsherman thinks we should rely on.

bentsherman commented 3 weeks ago

We can set up a repo in the nextflow-io org for you to collaborate

Lenbok commented 3 weeks ago

In terms of lsp-mode (which is the "batteries included" emacs language client), I see @edmundmiller has a fork where he's working on adding that (https://github.com/emacs-lsp/lsp-mode/compare/master...edmundmiller:lsp-mode:master). He should be able to open a PR directly against upstream lsp-mode. (I think it should be able to download the language server jar from this repo's releases rather than the vscode vsix).

I think rather than needing another repo, what is needed is documentation in this repo on what configuration is required/accepted when using other clients, plus testing/debugging against other language clients (and either fixing issues here or in the repos of those other clients).

edmundmiller commented 3 weeks ago

https://github.com/mason-org/mason-registry/pull/7713 https://github.com/mason-org/mason-registry/pull/7480 Couple of neovim PRs

bentsherman commented 3 weeks ago

Are you saying there is one emacs language client that everyone uses to connect to all language servers?

mehalter commented 3 weeks ago

Yeah, isn't that how all of the editors do it? They have an implementation of a LSP client: for emacs there is a plugin that implements it, vim.lsp.Client is the native LSP client in Neovim, etc. These clients define how to connect to a server that implements the LSP spec then you tell it where to connect to the server either one that's already running or how to start it up.

I also was trying to get the language server jar up and running in the Neovim LSP client and got the server running and connected successfully but it didn't provide anything. Formatting/completion/etc. didn't seem to get provided.

bentsherman commented 3 weeks ago

In vscode there is a typescript API that you use to connect vscode to the language server from a vscode extension. So there is no central repository for all vscode <-> language server integrations

Lenbok commented 3 weeks ago

In emacs there is one language client (eglot) that is now part of emacs, and there are a couple of alternatives available as external packages (lsp-mode, lsp-bridge), but as @mehalter notes, generally you choose a client and use it for pretty much all language servers, and it's usually a small amount of configuration to connect the client to the server for a particular language.

I haven't used the groovy language server that this one is derived from, but it might be worth trying in order to see whether it also has similar difficulties with other clients.

edmundmiller commented 2 weeks ago

@Lenbok PR to lsp-mode https://github.com/emacs-lsp/lsp-mode/pull/4606 and then all of the config I used to set it up personally https://github.com/edmundmiller/.doom.d/blob/b5ce985a5bc81629ce53b46fc950a6b47d807d24/modules/lang/nextflow/config.el

Lenbok commented 2 weeks ago

@edmundmiller are you saying you have it actually working and jumping to definitions etc?

edmundmiller commented 2 weeks ago

Yep! I was mainly testing for formatting myself 😬

Give it a shot and let me know if anything isn't working!

Screenshots

Search for symbol image Look for references image Highlighting of imports image Jump to definition works. Might need to clean up the imenu attempts I had in Nextflow-mode before. Completions image Errors list image

Lenbok commented 2 weeks ago

@edmundmiller Thanks, I was able to get the server working using your lsp-mode PR, and it was also the help I needed to get it working under eglot. It does seem the nextflow language server does not have a working "default" configuration, and so the language client has to send minimal configuration. From my experimentation, it must contain a "nextflow.files.exclude" section, so for eglot a minimal project config (i.e. .dir-locals.el) is:

;;; Directory Local Variables
;;; For more information see (info "(emacs) Directory Variables")
((nextflow-mode
  (eglot-workspace-configuration
   . (:nextflow (:files (:exclude [".git" ".nf-test" "work"]))))))

@bentsherman Would you be able to document the configuration options the server knows about and what the defaults are? (Possibly even give the server a default value for the above files directive).

edmundmiller commented 2 weeks ago

@Lenbok I think it's listed out in the schema on the vscode repo. I ran

(lsp-generate-settings "~/src/nf-core/vscode-language-nextflow/package.json" 'lsp-nextflow)

And in the neovim setup https://github.com/mason-org/mason-registry/blob/841ceaba936a1f18a927cb40ff303a3e8efd42ac/packages/nextflow-language-server/package.yaml#L20

Lenbok commented 2 weeks ago

@edmundmiller Neat, I didn't know about that ability to reverse engineer the server settings from the vscode package.json. That could come handy in future. Nevertheless, it feels like there should be some documentation as part of this repo that says what those settings are and do (and if there are any others that don't happen to be mentioned there), since in principle language servers are independent of any particular client such as vscode.

bentsherman commented 1 week ago

Closing in favor of #56