oxalica/nil

Setting formatter via initializationOptions doesn't work

chasecaleb opened this issue · 7 comments

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)

I have the same issue with the same setup.

I also observe that automatic formatting is broken with nil.

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.

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))))

@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?

@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

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