-
Notifications
You must be signed in to change notification settings - Fork 172
/
addon.rb
275 lines (234 loc) · 9.56 KB
/
addon.rb
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# typed: strict
# frozen_string_literal: true
module RubyLsp
# 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
# ```
class Addon
extend T::Sig
extend T::Helpers
abstract!
@addons = T.let([], T::Array[Addon])
@addon_classes = T.let([], T::Array[T.class_of(Addon)])
# Add-on instances that have declared a handler to accept file watcher events
@file_watcher_addons = T.let([], T::Array[Addon])
AddonNotFoundError = Class.new(StandardError)
class IncompatibleApiError < StandardError; end
class << self
extend T::Sig
sig { returns(T::Array[Addon]) }
attr_accessor :addons
sig { returns(T::Array[Addon]) }
attr_accessor :file_watcher_addons
sig { returns(T::Array[T.class_of(Addon)]) }
attr_reader :addon_classes
# Automatically track and instantiate add-on classes
sig { params(child_class: T.class_of(Addon)).void }
def inherited(child_class)
addon_classes << child_class
super
end
# Discovers and loads all add-ons. Returns a list of errors when trying to require add-ons
sig do
params(
global_state: GlobalState,
outgoing_queue: Thread::Queue,
include_project_addons: T::Boolean,
).returns(T::Array[StandardError])
end
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
addon_files.concat(Dir.glob(File.join(global_state.workspace_path, "**", "ruby_lsp/**/addon.rb")))
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
# Unloads all add-ons. Only intended to be invoked once when shutting down the Ruby LSP server
sig { void }
def unload_addons
@addons.each(&:deactivate)
@addons.clear
@addon_classes.clear
@file_watcher_addons.clear
end
# 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.
sig { params(addon_name: String, version_constraints: String).returns(Addon) }
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
# 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
# ```
sig { params(version_constraints: String).void }
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
end
sig { void }
def initialize
@errors = T.let([], T::Array[StandardError])
end
sig { params(error: StandardError).returns(T.self_type) }
def add_error(error)
@errors << error
self
end
sig { returns(T::Boolean) }
def error?
@errors.any?
end
sig { returns(String) }
def formatted_errors
<<~ERRORS
#{name}:
#{@errors.map(&:message).join("\n")}
ERRORS
end
sig { returns(String) }
def errors_details
@errors.map(&:full_message).join("\n\n")
end
# 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
sig { abstract.params(global_state: GlobalState, outgoing_queue: Thread::Queue).void }
def activate(global_state, outgoing_queue); end
# Each add-on should implement `MyAddon#deactivate` and use to perform any clean up, like shutting down a
# child process
sig { abstract.void }
def deactivate; end
# Add-ons should override the `name` method to return the add-on name
sig { abstract.returns(String) }
def name; end
# 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
sig { abstract.returns(String) }
def version; end
# 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
# https://microsoft.github.io/language-server-protocol/specification#window_showMessageRequest
sig { overridable.params(title: String).void }
def handle_window_show_message_response(title); end
# Creates a new CodeLens listener. This method is invoked on every CodeLens request
sig do
overridable.params(
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
uri: URI::Generic,
dispatcher: Prism::Dispatcher,
).void
end
def create_code_lens_listener(response_builder, uri, dispatcher); end
# Creates a new Hover listener. This method is invoked on every Hover request
sig do
overridable.params(
response_builder: ResponseBuilders::Hover,
node_context: NodeContext,
dispatcher: Prism::Dispatcher,
).void
end
def create_hover_listener(response_builder, node_context, dispatcher); end
# Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request
sig do
overridable.params(
response_builder: ResponseBuilders::DocumentSymbol,
dispatcher: Prism::Dispatcher,
).void
end
def create_document_symbol_listener(response_builder, dispatcher); end
sig do
overridable.params(
response_builder: ResponseBuilders::SemanticHighlighting,
dispatcher: Prism::Dispatcher,
).void
end
def create_semantic_highlighting_listener(response_builder, dispatcher); end
# Creates a new Definition listener. This method is invoked on every Definition request
sig do
overridable.params(
response_builder: ResponseBuilders::CollectionResponseBuilder[T.any(
Interface::Location,
Interface::LocationLink,
)],
uri: URI::Generic,
node_context: NodeContext,
dispatcher: Prism::Dispatcher,
).void
end
def create_definition_listener(response_builder, uri, node_context, dispatcher); end
# Creates a new Completion listener. This method is invoked on every Completion request
sig do
overridable.params(
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
node_context: NodeContext,
dispatcher: Prism::Dispatcher,
uri: URI::Generic,
).void
end
def create_completion_listener(response_builder, node_context, dispatcher, uri); end
end
end