oxalica / nil

NIx Language server, an incremental analysis assistant for writing in Nix.
Apache License 2.0
1.39k stars 42 forks source link

Setting formatter via initializationOptions doesn't work #101

Open chasecaleb opened 1 year ago

chasecaleb commented 1 year ago

Overview

I'm not sure if I'm doing it wrong or if there's a bug in either nil or Emacs eglot, so I'm hoping you can help me. I'm trying to switch from lsp-mode to eglot with Emacs 29 and I can't seem to get initialization-time formatter configuration to work. I expect that calling eglot-format-buffer should work regardless of whether the formatter is configured during initialization or at run-time, but only the latter does anything.

For reference, here's an online version of eglot's info page: https://joaotavora.github.io/eglot/#User_002dspecific-configuration

Version info:

caleb@desktop> nil --version
nil 2023-08-09
caleb@desktop> emacs --version
GNU Emacs 29.1
Copyright (C) 2023 Free Software Foundation, Inc.
GNU Emacs comes with ABSOLUTELY NO WARRANTY.
You may redistribute copies of GNU Emacs
under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING.
caleb@desktop> uname -a
Linux desktop 6.4.11 #1-NixOS SMP PREEMPT_DYNAMIC Wed Aug 16 16:32:31 UTC 2023 x86_64 GNU/Linux

Broken: initializationOptions

Config:

(add-to-list 'eglot-server-programs
             `(nix-mode . ("nil" :initializationOptions (:nil (:formatting (:command ["nixpkgs-fmt"]))))))

LSP messages shown after loading a nix file and running eglot-format-buffer:

[internal] Wed Aug 23 17:01:54 2023:
(:message "Running language server: nil")
[client-request] (id:1) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 1 :method "initialize" :params
 (:processId 1358112 :rootPath "/home/caleb/code/emacs.nix/" :rootUri "file:///home/caleb/code/emacs.nix" :initializationOptions
  (:nil
   (:formatting
    (:command
     ["nixpkgs-fmt"])))
  :capabilities
  (:workspace
   (:applyEdit t :executeCommand
    (:dynamicRegistration :json-false)
    :workspaceEdit
    (:documentChanges t)
    :didChangeWatchedFiles
    (:dynamicRegistration t)
    :symbol
    (:dynamicRegistration :json-false)
    :configuration t :workspaceFolders t)
   :textDocument
   (:synchronization
    (:dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t)
    :completion
    (:dynamicRegistration :json-false :completionItem
     (:snippetSupport t :deprecatedSupport t :resolveSupport
      (:properties
       ["documentation" "details" "additionalTextEdits"])
      :tagSupport
      (:valueSet
       [1]))
     :contextSupport t)
    :hover
    (:dynamicRegistration :json-false :contentFormat
     ["markdown" "plaintext"])
    :signatureHelp
    (:dynamicRegistration :json-false :signatureInformation
     (:parameterInformation
      (:labelOffsetSupport t)
      :activeParameterSupport t))
    :references
    (:dynamicRegistration :json-false)
    :definition
    (:dynamicRegistration :json-false :linkSupport t)
    :declaration
    (:dynamicRegistration :json-false :linkSupport t)
    :implementation
    (:dynamicRegistration :json-false :linkSupport t)
    :typeDefinition
    (:dynamicRegistration :json-false :linkSupport t)
    :documentSymbol
    (:dynamicRegistration :json-false :hierarchicalDocumentSymbolSupport t :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]))
    :documentHighlight
    (:dynamicRegistration :json-false)
    :codeAction
    (:dynamicRegistration :json-false :codeActionLiteralSupport
     (:codeActionKind
      (:valueSet
       ["quickfix" "refactor" "refactor.extract" "refactor.inline" "refactor.rewrite" "source" "source.organizeImports"]))
     :isPreferredSupport t)
    :formatting
    (:dynamicRegistration :json-false)
    :rangeFormatting
    (:dynamicRegistration :json-false)
    :rename
    (:dynamicRegistration :json-false)
    :inlayHint
    (:dynamicRegistration :json-false)
    :publishDiagnostics
    (:relatedInformation :json-false :codeDescriptionSupport :json-false :tagSupport
     (:valueSet
      [1 2])))
   :window
   (:workDoneProgress t)
   :general
   (:positionEncodings
    ["utf-32" "utf-8" "utf-16"])
   :experimental #s(hash-table size 1 test eql rehash-size 1.5 rehash-threshold 0.8125 data
                               ()))
  :workspaceFolders
  [(:uri "file:///home/caleb/code/emacs.nix" :name "~/code/emacs.nix/")]))
[server-reply] (id:1) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 1 :result
 (:capabilities
  (:codeActionProvider t :completionProvider
   (:triggerCharacters
    ["." "?"])
   :definitionProvider t :documentFormattingProvider t :documentHighlightProvider t :documentLinkProvider
   (:resolveProvider t)
   :documentSymbolProvider t :hoverProvider t :referencesProvider t :renameProvider
   (:prepareProvider t)
   :selectionRangeProvider t :semanticTokensProvider
   (:full
    (:delta :json-false)
    :legend
    (:tokenModifiers
     ["builtin" "conditional" "definition" "delimiter" "escape" "parenthesis" "readonly" "unresolved" "withAttribute"]
     :tokenTypes
     ["boolean" "comment" "constant" "function" "keyword" "number" "operator" "parameter" "path" "property" "punctuation" "string" "struct" "variable"])
    :range t)
   :textDocumentSync
   (:change 2 :openClose t))
  :serverInfo
  (:name "nil" :version "2023-08-09")))
[client-notification] Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :method "initialized" :params #s(hash-table size 1 test eql rehash-size 1.5 rehash-threshold 0.8125 data
                                                            ()))
[client-notification] Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :method "textDocument/didOpen" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix" :version 0 :languageId "nix" :text "{}:\n{\n         foo = \"123\";\n}\n")))
[client-notification] Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :method "workspace/didChangeConfiguration" :params
 (:settings #s(hash-table size 1 test eql rehash-size 1.5 rehash-threshold 0.8125 data
                          ())))
[server-request] (id:0) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 0 :method "workspace/configuration" :params
 (:items
  [(:section "nil")]))
[client-reply] (id:0) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 0 :result
 [nil])
[server-request] (id:1) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 1 :method "client/registerCapability" :params
 (:registrations
  [(:id "workspace/didChangeWatchedFiles" :method "workspace/didChangeWatchedFiles" :registerOptions
    (:watchers
     [(:globPattern "/home/caleb/code/emacs.nix/flake.lock")
      (:globPattern "/home/caleb/code/emacs.nix/flake.nix")]))]))
[client-reply] (id:1) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 1 :result nil)
[server-request] (id:2) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 2 :method "workspace/configuration" :params
 (:items
  [(:section "nil")]))
[client-reply] (id:2) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 2 :result
 [nil])
[server-request] (id:3) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 3 :method "window/workDoneProgress/create" :params
 (:token "nil/loadNixosOptionsProgress"))
[client-reply] (id:3) Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :id 3 :result nil)
[server-notification] Wed Aug 23 17:01:54 2023:
(:jsonrpc "2.0" :method "$/progress" :params
 (:token "nil/loadNixosOptionsProgress" :value
  (:kind "begin" :title "Loading NixOS options from 'nixpkgs'")))
[server-notification] Wed Aug 23 17:01:56 2023:
(:jsonrpc "2.0" :method "$/progress" :params
 (:token "nil/loadNixosOptionsProgress" :value
  (:kind "end")))
[client-request] (id:2) Wed Aug 23 17:01:56 2023:
(:jsonrpc "2.0" :id 2 :method "textDocument/formatting" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :options
  (:tabSize 2 :insertSpaces t :insertFinalNewline t :trimFinalNewlines t)))
[server-reply] (id:2) Wed Aug 23 17:01:56 2023:
(:jsonrpc "2.0" :id 2 :result nil)
[client-request] (id:3) Wed Aug 23 17:01:57 2023:
(:jsonrpc "2.0" :id 3 :method "textDocument/hover" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :position
  (:line 0 :character 0)))
[client-request] (id:4) Wed Aug 23 17:01:57 2023:
(:jsonrpc "2.0" :id 4 :method "textDocument/documentHighlight" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :position
  (:line 0 :character 0)))
[server-reply] (id:3) Wed Aug 23 17:01:57 2023:
(:jsonrpc "2.0" :id 3 :result nil)
[server-reply] (id:4) Wed Aug 23 17:01:57 2023:
(:jsonrpc "2.0" :id 4 :result
 [])

I notice there's a workspace/didChangeConfiguration message here, even though I'm not telling eglot to do that since eglot-workspace-configuration is nil, but I think (:settings #s(hash-table size 1 test eql rehash-size 1.5 rehash-threshold 0.8125 data ()))) represents an empty table?

Working: post-initialization settings

If I use this config instead, formatting works:

(setq-default eglot-workspace-configuration
              '(:nil (:formatting (:command ["nixpkgs-fmt"]))))

Messages:

[internal] Wed Aug 23 17:00:19 2023:
(:message "Running language server: /run/current-system/sw/bin/nil")
[client-request] (id:1) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 1 :method "initialize" :params
 (:processId 1357372 :rootPath "/home/caleb/code/emacs.nix/" :rootUri "file:///home/caleb/code/emacs.nix" :initializationOptions #s(hash-table size 1 test eql rehash-size 1.5 rehash-threshold 0.8125 data
                                                                                                                                               ())
  :capabilities
  (:workspace
   (:applyEdit t :executeCommand
    (:dynamicRegistration :json-false)
    :workspaceEdit
    (:documentChanges t)
    :didChangeWatchedFiles
    (:dynamicRegistration t)
    :symbol
    (:dynamicRegistration :json-false)
    :configuration t :workspaceFolders t)
   :textDocument
   (:synchronization
    (:dynamicRegistration :json-false :willSave t :willSaveWaitUntil t :didSave t)
    :completion
    (:dynamicRegistration :json-false :completionItem
     (:snippetSupport t :deprecatedSupport t :resolveSupport
      (:properties
       ["documentation" "details" "additionalTextEdits"])
      :tagSupport
      (:valueSet
       [1]))
     :contextSupport t)
    :hover
    (:dynamicRegistration :json-false :contentFormat
     ["markdown" "plaintext"])
    :signatureHelp
    (:dynamicRegistration :json-false :signatureInformation
     (:parameterInformation
      (:labelOffsetSupport t)
      :activeParameterSupport t))
    :references
    (:dynamicRegistration :json-false)
    :definition
    (:dynamicRegistration :json-false :linkSupport t)
    :declaration
    (:dynamicRegistration :json-false :linkSupport t)
    :implementation
    (:dynamicRegistration :json-false :linkSupport t)
    :typeDefinition
    (:dynamicRegistration :json-false :linkSupport t)
    :documentSymbol
    (:dynamicRegistration :json-false :hierarchicalDocumentSymbolSupport t :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]))
    :documentHighlight
    (:dynamicRegistration :json-false)
    :codeAction
    (:dynamicRegistration :json-false :codeActionLiteralSupport
     (:codeActionKind
      (:valueSet
       ["quickfix" "refactor" "refactor.extract" "refactor.inline" "refactor.rewrite" "source" "source.organizeImports"]))
     :isPreferredSupport t)
    :formatting
    (:dynamicRegistration :json-false)
    :rangeFormatting
    (:dynamicRegistration :json-false)
    :rename
    (:dynamicRegistration :json-false)
    :inlayHint
    (:dynamicRegistration :json-false)
    :publishDiagnostics
    (:relatedInformation :json-false :codeDescriptionSupport :json-false :tagSupport
     (:valueSet
      [1 2])))
   :window
   (:workDoneProgress t)
   :general
   (:positionEncodings
    ["utf-32" "utf-8" "utf-16"])
   :experimental #s(hash-table size 1 test eql rehash-size 1.5 rehash-threshold 0.8125 data
                               ()))
  :workspaceFolders
  [(:uri "file:///home/caleb/code/emacs.nix" :name "~/code/emacs.nix/")]))
[server-reply] (id:1) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 1 :result
 (:capabilities
  (:codeActionProvider t :completionProvider
   (:triggerCharacters
    ["." "?"])
   :definitionProvider t :documentFormattingProvider t :documentHighlightProvider t :documentLinkProvider
   (:resolveProvider t)
   :documentSymbolProvider t :hoverProvider t :referencesProvider t :renameProvider
   (:prepareProvider t)
   :selectionRangeProvider t :semanticTokensProvider
   (:full
    (:delta :json-false)
    :legend
    (:tokenModifiers
     ["builtin" "conditional" "definition" "delimiter" "escape" "parenthesis" "readonly" "unresolved" "withAttribute"]
     :tokenTypes
     ["boolean" "comment" "constant" "function" "keyword" "number" "operator" "parameter" "path" "property" "punctuation" "string" "struct" "variable"])
    :range t)
   :textDocumentSync
   (:change 2 :openClose t))
  :serverInfo
  (:name "nil" :version "2023-08-09")))
[client-notification] Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :method "initialized" :params #s(hash-table size 1 test eql rehash-size 1.5 rehash-threshold 0.8125 data
                                                            ()))
[client-notification] Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :method "textDocument/didOpen" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix" :version 0 :languageId "nix" :text "{}:\n{\n         foo = \"123\";\n}\n")))
[client-notification] Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :method "workspace/didChangeConfiguration" :params
 (:settings
  (:nil
   (:formatting
    (:command
     ["nixpkgs-fmt"])))))
[server-request] (id:0) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 0 :method "workspace/configuration" :params
 (:items
  [(:section "nil")]))
[client-reply] (id:0) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 0 :result
 [(:formatting
   (:command
    ["nixpkgs-fmt"]))])
[server-request] (id:1) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 1 :method "client/registerCapability" :params
 (:registrations
  [(:id "workspace/didChangeWatchedFiles" :method "workspace/didChangeWatchedFiles" :registerOptions
    (:watchers
     [(:globPattern "/home/caleb/code/emacs.nix/flake.lock")
      (:globPattern "/home/caleb/code/emacs.nix/flake.nix")]))]))
[client-reply] (id:1) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 1 :result nil)
[server-request] (id:2) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 2 :method "workspace/configuration" :params
 (:items
  [(:section "nil")]))
[client-reply] (id:2) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 2 :result
 [(:formatting
   (:command
    ["nixpkgs-fmt"]))])
[server-request] (id:3) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 3 :method "window/workDoneProgress/create" :params
 (:token "nil/loadNixosOptionsProgress"))
[client-reply] (id:3) Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :id 3 :result nil)
[server-notification] Wed Aug 23 17:00:19 2023:
(:jsonrpc "2.0" :method "$/progress" :params
 (:token "nil/loadNixosOptionsProgress" :value
  (:kind "begin" :title "Loading NixOS options from 'nixpkgs'")))
[server-notification] Wed Aug 23 17:00:20 2023:
(:jsonrpc "2.0" :method "$/progress" :params
 (:token "nil/loadNixosOptionsProgress" :value
  (:kind "end")))
[client-request] (id:2) Wed Aug 23 17:00:26 2023:
(:jsonrpc "2.0" :id 2 :method "textDocument/hover" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :position
  (:line 3 :character 1)))
[client-request] (id:3) Wed Aug 23 17:00:26 2023:
(:jsonrpc "2.0" :id 3 :method "textDocument/documentHighlight" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :position
  (:line 3 :character 1)))
[server-reply] (id:2) Wed Aug 23 17:00:26 2023:
(:jsonrpc "2.0" :id 2 :result nil)
[server-reply] (id:3) Wed Aug 23 17:00:26 2023:
(:jsonrpc "2.0" :id 3 :result
 [])
[client-request] (id:4) Wed Aug 23 17:00:28 2023:
(:jsonrpc "2.0" :id 4 :method "textDocument/formatting" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :options
  (:tabSize 2 :insertSpaces t :insertFinalNewline t :trimFinalNewlines t)))
[server-reply] (id:4) Wed Aug 23 17:00:28 2023:
(:jsonrpc "2.0" :id 4 :result
 [(:newText "{}:\n{\n  foo = \"123\";\n}\n" :range
   (:end
    (:character 0 :line 4)
    :start
    (:character 0 :line 0)))])
[internal] (id:5) Wed Aug 23 17:00:29 2023:
(:deferring :textDocument/hover :id 5 :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :position
  (:line 3 :character 1)))
[internal] (id:6) Wed Aug 23 17:00:29 2023:
(:deferring :textDocument/documentHighlight :id 6 :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :position
  (:line 3 :character 1)))
[client-notification] Wed Aug 23 17:00:29 2023:
(:jsonrpc "2.0" :method "textDocument/didChange" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix" :version 1)
  :contentChanges
  [(:range
    (:start
     (:line 0 :character 0)
     :end
     (:line 4 :character 0))
    :rangeLength 30 :text "{}:\n{\n  foo = \"123\";\n}\n")]))
[internal] Wed Aug 23 17:00:29 2023:
(:maybe-run-deferred
 (6 5))
[client-request] (id:6) Wed Aug 23 17:00:29 2023:
(:jsonrpc "2.0" :id 6 :method "textDocument/documentHighlight" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :position
  (:line 3 :character 1)))
[client-request] (id:5) Wed Aug 23 17:00:29 2023:
(:jsonrpc "2.0" :id 5 :method "textDocument/hover" :params
 (:textDocument
  (:uri "file:///home/caleb/code/emacs.nix/foo.nix")
  :position
  (:line 3 :character 1)))
[server-reply] (id:6) Wed Aug 23 17:00:29 2023:
(:jsonrpc "2.0" :id 6 :result
 [])
[server-reply] (id:5) Wed Aug 23 17:00:29 2023:
(:jsonrpc "2.0" :id 5 :result nil)
DamienCassou commented 1 year ago

I have the same issue with the same setup.

dschrempf commented 1 year ago

I also observe that automatic formatting is broken with nil.

oxalica commented 1 year ago

I'm not familiar with emacs but the log seems weird since it sends initializationOptions but responding [nil] for workspace/configuration requests. I tried to use initializationOptions with workspace/configuration disabled under nvim and it seems to work fine.

Could you set env var NIL_LOG to be nil=debug and provide the stderr of the server? If it's hard to retrieve the server's stderr, NIL_LOG_PATH can also be set to a file path to write the log into.

cmacrae commented 7 months ago

I have this working with the following config. looks like, when you set initializationOptions for a language server, the options are nested under the name of the language server already. so, having your options under (:nil) is actually incorrect.

(use-package eglot
  :ensure nil
  :after inheritenv
  :hook
  (prog-mode . eglot-ensure)
  (prog-mode . (lambda () (add-hook 'before-save-hook 'eglot-format nil t)))
  :config
  (with-eval-after-load 'eglot
    (dolist (mode '((nix-mode . ("nil" :initializationOptions
                                       (:formatting (:command [ "nixpkgs-fmt" ]))))))
      (add-to-list 'eglot-server-programs mode))))
yzhou216 commented 1 month ago

@cmacrae This might be irrelevant to the original issue, but I had some problems setting this up. I did it without using the with-eval-after-load macro and that didn't work although I see the eglot-server-programs getting updated correctly. Your config worked for me, could you please explain why you're doing it this way?

cmacrae commented 1 month ago

@yzhou216 glad it works for you :) without with-eval-after-load eglot may not have finished loading. even though eglot-server-programs may be getting updated, if eglot hasn't finished loading, evaluation of the list may somehow not be effective - i can only surmise without digging into eglot's machinery

yzhou216 commented 1 month ago

@cmacrae Thank you for the explanation! That was very helpful :)