Skip to content

Commit

Permalink
Refactor client capabilities into its own class (#2805)
Browse files Browse the repository at this point in the history
### Motivation

As pointed out by @andyw8, our implementation of the global state was growing a little too much and so was the body of `apply_options`. This PR extracts a new object to store client capabilities, so that we can organize concerns a bit better.

### Implementation

Extracted the client capabilities into an object and adapted the code that was using them for the new structure.

### Automated Tests

Updated existing tests.
  • Loading branch information
vinistock authored Oct 30, 2024
1 parent 33fd4e9 commit 4d885c5
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 37 deletions.
4 changes: 3 additions & 1 deletion lib/ruby_lsp/base_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ def start
# If the client supports request delegation and we're working with an ERB document and there was
# something to parse, then we have to maintain the client updated about the virtual state of the host
# language source
if document.parse! && @global_state.supports_request_delegation && document.is_a?(ERBDocument)
if document.parse! && @global_state.client_capabilities.supports_request_delegation &&
document.is_a?(ERBDocument)

send_message(
Notification.new(
method: "delegate/textDocument/virtualState",
Expand Down
60 changes: 60 additions & 0 deletions lib/ruby_lsp/client_capabilities.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# typed: strict
# frozen_string_literal: true

module RubyLsp
# This class stores all client capabilities that the Ruby LSP and its add-ons depend on to ensure that we're
# not enabling functionality unsupported by the editor connecting to the server
class ClientCapabilities
extend T::Sig

sig { returns(T::Boolean) }
attr_reader :supports_watching_files,
:supports_request_delegation,
:window_show_message_supports_extra_properties

sig { void }
def initialize
# The editor supports watching files. This requires two capabilities: dynamic registration and relative pattern
# support
@supports_watching_files = T.let(false, T::Boolean)

# The editor supports request delegation. This is an experimental capability since request delegation has not been
# standardized into the LSP spec yet
@supports_request_delegation = T.let(false, T::Boolean)

# The editor supports extra arbitrary properties for `window/showMessageRequest`. Necessary for add-ons to show
# dialogs with user interactions
@window_show_message_supports_extra_properties = T.let(false, T::Boolean)

# Which resource operations the editor supports, like renaming files
@supported_resource_operations = T.let([], T::Array[String])
end

sig { params(capabilities: T::Hash[Symbol, T.untyped]).void }
def apply_client_capabilities(capabilities)
workspace_capabilities = capabilities[:workspace] || {}

file_watching_caps = workspace_capabilities[:didChangeWatchedFiles]
if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
@supports_watching_files = true
end

@supports_request_delegation = capabilities.dig(:experimental, :requestDelegation) || false
supported_resource_operations = workspace_capabilities.dig(:workspaceEdit, :resourceOperations)
@supported_resource_operations = supported_resource_operations if supported_resource_operations

supports_additional_properties = capabilities.dig(
:window,
:showMessage,
:messageActionItem,
:additionalPropertiesSupport,
)
@window_show_message_supports_extra_properties = supports_additional_properties || false
end

sig { returns(T::Boolean) }
def supports_rename?
@supported_resource_operations.include?("rename")
end
end
end
36 changes: 6 additions & 30 deletions lib/ruby_lsp/global_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,14 @@ class GlobalState
attr_reader :encoding

sig { returns(T::Boolean) }
attr_reader :supports_watching_files,
:experimental_features,
:supports_request_delegation,
:top_level_bundle,
:window_show_message_supports_extra_properties

sig { returns(T::Array[String]) }
attr_reader :supported_resource_operations
attr_reader :experimental_features, :top_level_bundle

sig { returns(TypeInferrer) }
attr_reader :type_inferrer

sig { returns(ClientCapabilities) }
attr_reader :client_capabilities

sig { void }
def initialize
@workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
Expand All @@ -44,12 +40,9 @@ def initialize
@has_type_checker = T.let(true, T::Boolean)
@index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
@supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
@supports_watching_files = T.let(false, T::Boolean)
@experimental_features = T.let(false, T::Boolean)
@type_inferrer = T.let(TypeInferrer.new(@index), TypeInferrer)
@addon_settings = T.let({}, T::Hash[String, T.untyped])
@supports_request_delegation = T.let(false, T::Boolean)
@supported_resource_operations = T.let([], T::Array[String])
@top_level_bundle = T.let(
begin
Bundler.with_original_env { Bundler.default_gemfile }
Expand All @@ -59,7 +52,7 @@ def initialize
end,
T::Boolean,
)
@window_show_message_supports_extra_properties = T.let(false, T::Boolean)
@client_capabilities = T.let(ClientCapabilities.new, ClientCapabilities)
end

sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
Expand Down Expand Up @@ -137,32 +130,15 @@ def apply_options(options)
end
@index.configuration.encoding = @encoding

file_watching_caps = options.dig(:capabilities, :workspace, :didChangeWatchedFiles)
if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
@supports_watching_files = true
end

@experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
@client_capabilities.apply_client_capabilities(options[:capabilities]) if options[:capabilities]

addon_settings = options.dig(:initializationOptions, :addonSettings)
if addon_settings
addon_settings.transform_keys!(&:to_s)
@addon_settings.merge!(addon_settings)
end

@supports_request_delegation = options.dig(:capabilities, :experimental, :requestDelegation) || false
supported_resource_operations = options.dig(:capabilities, :workspace, :workspaceEdit, :resourceOperations)
@supported_resource_operations = supported_resource_operations if supported_resource_operations

supports_additional_properties = options.dig(
:capabilities,
:window,
:showMessage,
:messageActionItem,
:additionalPropertiesSupport,
)
@window_show_message_supports_extra_properties = supports_additional_properties || false

notifications
end

Expand Down
1 change: 1 addition & 0 deletions lib/ruby_lsp/internal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
require "ruby_lsp/utils"
require "ruby_lsp/static_docs"
require "ruby_lsp/scope"
require "ruby_lsp/client_capabilities"
require "ruby_lsp/global_state"
require "ruby_lsp/server"
require "ruby_lsp/type_inferrer"
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_lsp/requests/rename.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def perform

# If the client doesn't support resource operations, such as renaming files, then we can only return the basic
# text changes
unless @global_state.supported_resource_operations.include?("rename")
unless @global_state.client_capabilities.supports_rename?
return Interface::WorkspaceEdit.new(changes: changes)
end

Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_lsp/requests/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def perform; end
).void
end
def delegate_request_if_needed!(global_state, document, char_position)
if global_state.supports_request_delegation &&
if global_state.client_capabilities.supports_request_delegation &&
document.is_a?(ERBDocument) &&
document.inside_host_language?(char_position)
raise DelegateRequestError
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_lsp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def run_initialize(message)
send_message(Result.new(id: message[:id], response: response))

# Not every client supports dynamic registration or file watching
if global_state.supports_watching_files
if @global_state.client_capabilities.supports_watching_files
send_message(
Request.new(
id: @current_request_id,
Expand Down
6 changes: 3 additions & 3 deletions test/global_state_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_watching_files_if_supported
},
},
})
assert(state.supports_watching_files)
assert(state.client_capabilities.supports_watching_files)
end

def test_watching_files_if_not_supported
Expand All @@ -136,7 +136,7 @@ def test_watching_files_if_not_supported
},
},
})
refute(state.supports_watching_files)
refute(state.client_capabilities.supports_watching_files)
end

def test_watching_files_if_not_reported
Expand All @@ -146,7 +146,7 @@ def test_watching_files_if_not_reported
workspace: {},
},
})
refute(state.supports_watching_files)
refute(state.client_capabilities.supports_watching_files)
end

def test_linter_specification
Expand Down

0 comments on commit 4d885c5

Please sign in to comment.