Skip to content

Conversation

@katafrakt
Copy link

Motivation

Closes #3785

workspace/didChangeConfiguration and workspace/configuration are part of the spec. They are used to dynamically change the configuration of the server after initialization. With eglot, they are used to provide per-project configuration via .dir-locals.el files (see: https://www.gnu.org/software/emacs/manual/html_node/eglot/Project_002dspecific-configuration.html).

Implementation

This adds support for the pull model of dynamic configuration changes, as described here: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration

Upon receiving workspace/didChangeConfiguration message the Ruby LSP server sends workspace/configuration to the client. The client replies. This reply is different than other replies handled by Ruby LSP so far, as it does not include method and needs to be associated with the request via id. This required implementing a collection of server-sent requests, to be able to match.

The result of the reply is not a hash, instead it's an array of hashes, so it needed to be handled separately as well.

Technically, upon receiving workspace/configuration the server should check if it should register or unregister some capabilities. I intended to do that too, but it started to become messy and also I did not have a way to properly test it, as eglot does not support dynamic registration of capabilities.

Automated Tests

TBH it's a bit short on tests. I wasn't sure where/how to add more. I welcome all the suggestions in that area, because it feels a bit undertested.

Manual Tests

I was able to swap the formatter and linter from Rubocop to StandardRB in eglot using this .dir-locals.el:

 ((nil
   . ((eglot-workspace-configuration
       . (:rubyLsp
          (:formatter "standard" :linters ["standard"]
           :enabledFeatures (:codeActions t :diagnostics t :formatting t)))))))

@graphite-app
Copy link

graphite-app bot commented Nov 3, 2025

How to use the Graphite Merge Queue

Add the label graphite-merge to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

@katafrakt katafrakt force-pushed the support-workspace-didchangeconfiguration branch 2 times, most recently from e3db658 to 21b3410 Compare November 3, 2025 18:09
This adds support for the pull model of dynamic configuration changes,
as described here: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration

Upon receiving `workspace/didChangeConfiguration` message the Ruby LSP
server sends `workspace/configuration` to the client. The client
replies. This reply is different than other replies handled by Ruby LSP
so far, as it does not include `method` and needs to be associated with
the request via `id`. This required implementing a collection of
server-sent requests, to be able to match.

The `result` of the reply is not a hash, instead it's an array of
hashes, so it needed to be handled separately as well.

Technically, upon receiving `workspace/configuration` the server should
check if it should register or unregister some capabilities. I intended
to do that too, but it started to become messy and also I did not have a
way to properly test it.
@katafrakt katafrakt force-pushed the support-workspace-didchangeconfiguration branch from 21b3410 to aebdb8e Compare November 3, 2025 18:09
Comment on lines +219 to +220
options = { initializationOptions: message[:result]&.first }
messages_to_send = @global_state.apply_options(options)
Copy link
Author

@katafrakt katafrakt Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a hack to reuse apply_options, which expects initializationOptions, but perhaps should be properly extracted to a method just accepting the options to apply.

Also, this just takes first hash from the reply's result, which works here, because we only asked for one configuration item, but technically is not exactly in line with the spec.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this method is coupling information that we get during initialization (like the workspace folder) with other settings. It would indeed be nice to split into two methods:

  1. One that handles information required by the spec (workspace folders, capabilities and so on)
  2. Another that handles only the Ruby LSP's specific settings (what gets passed as initializationOptions)

Beyond just improving the design, invoking this twice may have weird consequences, like accidentally changing the negotiated encoding, so I don't think we can reuse it as is.

For example, if the editor and server negotiated UTF-8 initially, invoking this method without passing the capabilities -> general -> positionEncodings data will result in the server choosing UTF-16 (respecting the spec's default encoding), which would mean that editor and server would be trying to communicate using different encodings - leading to crashes, documents being in an invalid state and so on.

@katafrakt katafrakt marked this pull request as ready for review November 3, 2025 18:12
@katafrakt katafrakt requested a review from a team as a code owner November 3, 2025 18:12
private

#: (Hash[Symbol, untyped] message) -> void
def workspace_configuration_did_change(message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding based on the spec is that the parameters of workspace/didChangeConfiguration already include which settings are supposed to be changed.

Why do we need to make a request to the client? Could we use what we receive as parameters instead?

unless @test_mode
while (message = @outgoing_queue.pop)
@global_state.synchronize { @writer.write(message.to_hash) }
@global_state.synchronize do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already process one response for server->client requests here.

Are we able to reuse that and avoid changing the base server?

Comment on lines +219 to +220
options = { initializationOptions: message[:result]&.first }
messages_to_send = @global_state.apply_options(options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this method is coupling information that we get during initialization (like the workspace folder) with other settings. It would indeed be nice to split into two methods:

  1. One that handles information required by the spec (workspace folders, capabilities and so on)
  2. Another that handles only the Ruby LSP's specific settings (what gets passed as initializationOptions)

Beyond just improving the design, invoking this twice may have weird consequences, like accidentally changing the negotiated encoding, so I don't think we can reuse it as is.

For example, if the editor and server negotiated UTF-8 initially, invoking this method without passing the capabilities -> general -> positionEncodings data will result in the server choosing UTF-16 (respecting the spec's default encoding), which would mean that editor and server would be trying to communicate using different encodings - leading to crashes, documents being in an invalid state and so on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support workspace/didChangeConfiguration

2 participants