Class: RubyLsp::Addon Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_lsp/addon.rb

Overview

This class is abstract.

To register an add-on, inherit from this class and implement both ‘name` and `activate`

# Example

“‘ruby module MyGem

class MyAddon < Addon
  def activate
    # Perform any relevant initialization
  end

  def name
    "My add-on name"
  end
end

end “‘

Defined Under Namespace

Classes: IncompatibleApiError

Constant Summary collapse

AddonNotFoundError =

: Array

Class.new(StandardError)

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeAddon

: -> void



168
169
170
# File 'lib/ruby_lsp/addon.rb', line 168

def initialize
  @errors = [] #: Array[StandardError]
end

Class Attribute Details

.addon_classesObject (readonly)

: Array



41
42
43
# File 'lib/ruby_lsp/addon.rb', line 41

def addon_classes
  @addon_classes
end

.addonsObject

: Array



35
36
37
# File 'lib/ruby_lsp/addon.rb', line 35

def addons
  @addons
end

.file_watcher_addonsObject

: Array



38
39
40
# File 'lib/ruby_lsp/addon.rb', line 38

def file_watcher_addons
  @file_watcher_addons
end

Class Method Details

.depend_on_ruby_lsp!(*version_constraints) ⇒ Object

Depend on a specific version of the Ruby LSP. This method should only be used if the add-on is distributed in a gem that does not have a runtime dependency on the ruby-lsp gem. This method should be invoked at the top of the ‘addon.rb` file before defining any classes or requiring any files. For example:

“‘ruby RubyLsp::Addon.depend_on_ruby_lsp!(“>= 0.18.0”)

module MyGem

class MyAddon < RubyLsp::Addon
  # ...
end

end “‘ : (*String version_constraints) -> void



157
158
159
160
161
162
163
164
# File 'lib/ruby_lsp/addon.rb', line 157

def depend_on_ruby_lsp!(*version_constraints)
  version_object = Gem::Version.new(RubyLsp::VERSION)

  unless version_constraints.all? { |constraint| Gem::Requirement.new(constraint).satisfied_by?(version_object) }
    raise IncompatibleApiError,
      "Add-on is not compatible with this version of the Ruby LSP. Skipping its activation"
  end
end

.get(addon_name, *version_constraints) ⇒ Object

Get a reference to another add-on object by name and version. If an add-on exports an API that can be used by other add-ons, this is the way to get access to that API.

Important: if the add-on is not found, AddonNotFoundError will be raised. If the add-on is found, but its current version does not satisfy the given version constraint, then IncompatibleApiError will be raised. It is the responsibility of the add-ons using this API to handle these errors appropriately. : (String addon_name, *String version_constraints) -> Addon

Raises:



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/ruby_lsp/addon.rb', line 125

def get(addon_name, *version_constraints)
  if version_constraints.empty?
    raise IncompatibleApiError, "Must specify version constraints when accessing other add-ons"
  end

  addon = addons.find { |addon| addon.name == addon_name }
  raise AddonNotFoundError, "Could not find add-on '#{addon_name}'" unless addon

  version_object = Gem::Version.new(addon.version)

  unless version_constraints.all? { |constraint| Gem::Requirement.new(constraint).satisfied_by?(version_object) }
    raise IncompatibleApiError,
      "Constraints #{version_constraints.inspect} is incompatible with #{addon_name} version #{addon.version}"
  end

  addon
end

.inherited(child_class) ⇒ Object

Automatically track and instantiate add-on classes : (singleton(Addon) child_class) -> void



45
46
47
48
# File 'lib/ruby_lsp/addon.rb', line 45

def inherited(child_class)
  addon_classes << child_class
  super
end

.load_addons(global_state, outgoing_queue, include_project_addons: true) ⇒ Object

Discovers and loads all add-ons. Returns a list of errors when trying to require add-ons : (GlobalState global_state, Thread::Queue outgoing_queue, ?include_project_addons: bool) -> Array



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/ruby_lsp/addon.rb', line 52

def load_addons(global_state, outgoing_queue, include_project_addons: true)
  # Require all add-ons entry points, which should be placed under
  # `some_gem/lib/ruby_lsp/your_gem_name/addon.rb` or in the workspace under
  # `your_project/ruby_lsp/project_name/addon.rb`
  addon_files = Gem.find_files("ruby_lsp/**/addon.rb")

  if include_project_addons
    project_addons = Dir.glob("#{global_state.workspace_path}/**/ruby_lsp/**/addon.rb")
    bundle_path = Bundler.bundle_path.to_s
    gems_dir = Bundler.bundle_path.join("gems")

    # Create an array of rejection glob patterns to ignore add-ons already discovered through Gem.find_files if
    # they are also copied inside the workspace for whatever reason. We received reports of projects having gems
    # installed in vendor/bundle despite BUNDLE_PATH pointing elsewhere. Without this mechanism, we will
    # double-require the same add-on, potentially for different versions of the same gem, which leads to incorrect
    # behavior
    reject_glob_patterns = addon_files.map do |path|
      relative_gem_path = Pathname.new(path).relative_path_from(gems_dir)
      first_part, *parts = relative_gem_path.to_s.split(File::SEPARATOR)
      first_part&.gsub!(/-([0-9.]+)$/, "*")
      "**/#{first_part}/#{parts.join("/")}"
    end

    project_addons.reject! do |path|
      path.start_with?(bundle_path) ||
        reject_glob_patterns.any? { |pattern| File.fnmatch?(pattern, path, File::Constants::FNM_PATHNAME) }
    end

    addon_files.concat(project_addons)
  end

  errors = addon_files.filter_map do |addon_path|
    # Avoid requiring this file twice. This may happen if you're working on the Ruby LSP itself and at the same
    # time have `ruby-lsp` installed as a vendored gem
    next if File.basename(File.dirname(addon_path)) == "ruby_lsp"

    require File.expand_path(addon_path)
    nil
  rescue => e
    e
  end

  # Instantiate all discovered add-on classes
  self.addons = addon_classes.map(&:new)
  self.file_watcher_addons = addons.select { |addon| addon.respond_to?(:workspace_did_change_watched_files) }

  # Activate each one of the discovered add-ons. If any problems occur in the add-ons, we don't want to
  # fail to boot the server
  addons.each do |addon|
    addon.activate(global_state, outgoing_queue)
  rescue => e
    addon.add_error(e)
  end

  errors
end

.unload_addonsObject

Unloads all add-ons. Only intended to be invoked once when shutting down the Ruby LSP server : -> void



111
112
113
114
115
116
# File 'lib/ruby_lsp/addon.rb', line 111

def unload_addons
  @addons.each(&:deactivate)
  @addons.clear
  @addon_classes.clear
  @file_watcher_addons.clear
end

Instance Method Details

#activate(global_state, outgoing_queue) ⇒ Object

This method is abstract.

Each add-on should implement ‘MyAddon#activate` and use to perform any sort of initialization, such as reading information into memory or even spawning a separate process : (GlobalState, Thread::Queue) -> void



200
201
202
# File 'lib/ruby_lsp/addon.rb', line 200

def activate(global_state, outgoing_queue)
  raise AbstractMethodInvokedError
end

#add_error(error) ⇒ Object

: (StandardError error) -> self



173
174
175
176
# File 'lib/ruby_lsp/addon.rb', line 173

def add_error(error)
  @errors << error
  self
end

#create_code_lens_listener(response_builder, uri, dispatcher) ⇒ Object

Creates a new CodeLens listener. This method is invoked on every CodeLens request : (ResponseBuilders::CollectionResponseBuilder response_builder, URI::Generic uri, Prism::Dispatcher dispatcher) -> void



238
# File 'lib/ruby_lsp/addon.rb', line 238

def create_code_lens_listener(response_builder, uri, dispatcher); end

#create_completion_listener(response_builder, node_context, dispatcher, uri) ⇒ Object

Creates a new Completion listener. This method is invoked on every Completion request : (ResponseBuilders::CollectionResponseBuilder response_builder, NodeContext node_context, Prism::Dispatcher dispatcher, URI::Generic uri) -> void



262
# File 'lib/ruby_lsp/addon.rb', line 262

def create_completion_listener(response_builder, node_context, dispatcher, uri); end

#create_definition_listener(response_builder, uri, node_context, dispatcher) ⇒ Object

Creates a new Definition listener. This method is invoked on every Definition request : (ResponseBuilders::CollectionResponseBuilder[(Interface::Location | Interface::LocationLink)] response_builder, URI::Generic uri, NodeContext node_context, Prism::Dispatcher dispatcher) -> void



257
# File 'lib/ruby_lsp/addon.rb', line 257

def create_definition_listener(response_builder, uri, node_context, dispatcher); end

#create_discover_tests_listener(response_builder, dispatcher, uri) ⇒ Object

Creates a new Discover Tests listener. This method is invoked on every DiscoverTests request : (ResponseBuilders::TestCollection response_builder, Prism::Dispatcher dispatcher, URI::Generic uri) -> void



267
# File 'lib/ruby_lsp/addon.rb', line 267

def create_discover_tests_listener(response_builder, dispatcher, uri); end

#create_document_symbol_listener(response_builder, dispatcher) ⇒ Object

Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request : (ResponseBuilders::DocumentSymbol response_builder, Prism::Dispatcher dispatcher) -> void



248
# File 'lib/ruby_lsp/addon.rb', line 248

def create_document_symbol_listener(response_builder, dispatcher); end

#create_hover_listener(response_builder, node_context, dispatcher) ⇒ Object

Creates a new Hover listener. This method is invoked on every Hover request : (ResponseBuilders::Hover response_builder, NodeContext node_context, Prism::Dispatcher dispatcher) -> void



243
# File 'lib/ruby_lsp/addon.rb', line 243

def create_hover_listener(response_builder, node_context, dispatcher); end

#create_semantic_highlighting_listener(response_builder, dispatcher) ⇒ Object

: (ResponseBuilders::SemanticHighlighting response_builder, Prism::Dispatcher dispatcher) -> void



252
# File 'lib/ruby_lsp/addon.rb', line 252

def create_semantic_highlighting_listener(response_builder, dispatcher); end

#deactivateObject

This method is abstract.

Each add-on must implement ‘MyAddon#deactivate` and use to perform any clean up, like shutting down a child process : -> void



208
209
210
# File 'lib/ruby_lsp/addon.rb', line 208

def deactivate
  raise AbstractMethodInvokedError
end

#error?Boolean

: -> bool

Returns:

  • (Boolean)


179
180
181
# File 'lib/ruby_lsp/addon.rb', line 179

def error?
  @errors.any?
end

#errors_detailsObject

: -> String



192
193
194
# File 'lib/ruby_lsp/addon.rb', line 192

def errors_details
  @errors.map(&:full_message).join("\n\n")
end

#formatted_errorsObject

: -> String



184
185
186
187
188
189
# File 'lib/ruby_lsp/addon.rb', line 184

def formatted_errors
  "    \#{name}:\n      \#{@errors.map(&:message).join(\"\\n\")}\n  ERRORS\nend\n"

#handle_window_show_message_response(title) ⇒ Object

Handle a response from a window/showMessageRequest request. Add-ons must include the addon_name as part of the original request so that the response is delegated to the correct add-on and must override this method to handle the response microsoft.github.io/language-server-protocol/specification#window_showMessageRequest : (String title) -> void



233
# File 'lib/ruby_lsp/addon.rb', line 233

def handle_window_show_message_response(title); end

#nameObject

This method is abstract.

Add-ons should override the ‘name` method to return the add-on name : -> String



215
216
217
# File 'lib/ruby_lsp/addon.rb', line 215

def name
  raise AbstractMethodInvokedError
end

#resolve_test_commands(items) ⇒ Object

Resolves the minimal set of commands required to execute the requested tests. Add-ons are responsible for only handling items related to the framework they add support for and have discovered themselves : (Array[Hash[Symbol, untyped]]) -> Array



273
274
275
# File 'lib/ruby_lsp/addon.rb', line 273

def resolve_test_commands(items)
  []
end

#versionObject

This method is abstract.

Add-ons should override the ‘version` method to return a semantic version string representing the add-on’s version. This is used for compatibility checks : -> String



223
224
225
# File 'lib/ruby_lsp/addon.rb', line 223

def version
  raise AbstractMethodInvokedError
end