File: add-ons.markdown

package info (click to toggle)
ruby-ruby-lsp 0.26.7-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 27,676 kB
  • sloc: ruby: 35,294; javascript: 29; sh: 7; makefile: 4
file content (569 lines) | stat: -rw-r--r-- 21,394 bytes parent folder | download
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
---
layout: default
title: Add-ons
nav_order: 10
parent: Ruby LSP
---

# Add-ons

{: .warning }
> The Ruby LSP add-on system is currently experimental and subject to changes in the API

Need help writing add-ons? Consider joining the `#ruby-lsp-addons` channel in the [Ruby DX Slack
workspace](invite).

## Motivation and goals

Editor features that are specific to certain tools or frameworks can be incredibly powerful. Typically, language servers
are aimed at providing features for a particular programming language (like Ruby!) and not specific tools. This is
reasonable since not every programmer uses the same combination of tools.

Including tool specific functionality in the Ruby LSP would not scale well given the large number of tools in the
ecosystem. It would also create a bottleneck for authors to push new features. Building separate tooling, on the other
hand, increases fragmentation which tends to increase the effort required by users to configure their development
environments.

For these reasons, the Ruby LSP ships with an add-on system that authors can use to enhance the behavior of the base LSP
with tool specific functionality, aimed at

- Allowing gem authors to export Ruby LSP add-ons from their own gems
- Allowing LSP features to be enhanced by add-ons present in the application the developer is currently working on
- Not requiring extra configuration from the user
- Seamlessly integrating with the base features of the Ruby LSP
- Providing add-on authors with the entire static analysis toolkit that the Ruby LSP uses

## Guidelines

When building a Ruby LSP add-on, refer to these guidelines to ensure a good developer experience.

- Performance over features. A single slow request may result in lack of responsiveness in the editor
- There are two types of LSP requests: automatic (e.g.: semantic highlighting) and user initiated (go to definition).
The performance of automatic requests is critical for responsiveness as they are executed every time the user types
- Avoid duplicate work where possible. If something can be computed once and memoized, like configurations, do it
- Do not mutate LSP state directly. Add-ons sometimes have access to important state such as document objects, which
should never be mutated directly, but instead through the mechanisms provided by the LSP specification - like text edits
- Do not over-notify users. It's generally annoying and diverts attention from the current task
- Show the right context at the right time. When adding visual features, think about **when** the information is
relevant for users to avoid polluting the editor

## Building a Ruby LSP add-on

**Note**: the Ruby LSP uses [Sorbet](https://sorbet.org/). We recommend using Sorbet in add-ons as well, which allows
authors to benefit from types declared by the Ruby LSP.

As an example, check out [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails), which is a Ruby LSP add-on to
provide Rails related features.

### Activating the add-on

The Ruby LSP discovers add-ons based on the existence of an `addon.rb` file placed inside a `ruby_lsp` folder.  For
example, `my_gem/lib/ruby_lsp/my_gem/addon.rb`. This file must declare the add-on class, which can be used to perform any
necessary activation when the server starts.

{: .note }
Projects can also define their own private add-ons for functionality that only applies to a particular application. As
long as a file matching `ruby_lsp/**/addon.rb` exists inside of the workspace (not necessarily at the root), it will be
loaded by the Ruby LSP.

```ruby
# frozen_string_literal: true

require "ruby_lsp/addon"

module RubyLsp
  module MyGem
    class Addon < ::RubyLsp::Addon
      # Performs any activation that needs to happen once when the language server is booted
      def activate(global_state, message_queue)
      end

      # Performs any cleanup when shutting down the server, like terminating a subprocess
      def deactivate
      end

      # Returns the name of the add-on
      def name
        "Ruby LSP My Gem"
      end

      # Defining a version for the add-on is mandatory. This version doesn't necessarily need to match the version of
      # the gem it belongs to
      def version
        "0.1.0"
      end
    end
  end
end
```

### Listeners

An essential component to add-ons are listeners. All Ruby LSP requests are listeners that handle specific node types.

Listeners work in conjunction with a `Prism::Dispatcher`, which is responsible for dispatching events during the parsing of Ruby code. Each event corresponds to a specific node in the Abstract Syntax Tree (AST) of the code being parsed.

Here's a simple example of a listener:

```ruby
# frozen_string_literal: true

class MyListener
  def initialize(dispatcher)
    # Register to listen to `on_class_node_enter` events
    dispatcher.register(self, :on_class_node_enter)
  end

  # Define the handler method for the `on_class_node_enter` event
  def on_class_node_enter(node)
    $stderr.puts "Hello, #{node.constant_path.slice}!"
  end
end

dispatcher = Prism::Dispatcher.new
MyListener.new(dispatcher)

parse_result = Prism.parse("class Foo; end")
dispatcher.dispatch(parse_result.value)

# Prints
# => Hello, Foo!
```

In this example, the listener is registered to the dispatcher to listen for the `:on_class_node_enter` event. When a class node is encountered during the parsing of the code, a greeting message is outputted with the class name.

This approach enables all add-on responses to be captured in a single round of AST visits, greatly improving performance.

### Enhancing features

There are two ways to enhance Ruby LSP features. One is handling DSLs that occur at a call site and that do not change
which declarations exist in the project. A great example of this is the Rails `validate` method, which accepts a symbol
that represents a method that gets dynamically invoked. That style of DSL is what we refer to as a [call site
DSL](#dealing-with-call-site-dsls).

```ruby
class User < ApplicationRecord
  # From Ruby's perspective, `:something` is just a regular symbol. It's Rails that defines this as a DSL and specifies
  # that the argument represents a method name.
  #
  # If an add-on wanted to handle go to definition or completion for these symbols, then it would need to enhance the
  # handling for call site DSLs
  validate :something

  private

  def something
  end
end
```

The second way to augment the Ruby LSP is to handle declaration DSLs. These are DSLs that create declarations via
meta-programming. To use another Rails example, `belongs_to` is a DSL that mutates the current class and adds extra
methods based on the arguments passed to it.

DSLs that add extra declarations should be handled through an [indexing enhancement](#dealing-with-declaration-dsls).

```ruby
class User < ApplicationRecord
  # When this method is invoked, a bunch of new methods will be defined in the `User` class, such as `company` and
  # `company=`. By informing the Ruby LSP about the new methods through an indexing enhancement, features such as
  # go to definition, completion, hover, signature help and workspace symbol will automatically pick up the new
  # declaration
  belongs_to :company
end
```

#### Dealing with call site DSLs

To enhance a request, the add-on must create a listener that will collect extra results that will be automatically appended to the
base language server response. Additionally, `Addon` has to implement a factory method that instantiates the listener. When instantiating the
listener, also note that a `ResponseBuilders` object is passed in. This object should be used to return responses back to the Ruby LSP.

For example: to add a message on hover saying "Hello!" on top of the base hover behavior of the Ruby LSP, we can use the
following listener implementation.

```ruby
# frozen_string_literal: true

module RubyLsp
  module MyGem
    class Addon < ::RubyLsp::Addon
      def activate(global_state, message_queue)
        @message_queue = message_queue
        @config = SomeConfiguration.new
      end

      def deactivate
      end

      def name
        "Ruby LSP My Gem"
      end

      def version
        "0.1.0"
      end

      def create_hover_listener(response_builder, node_context, dispatcher)
        # Use the listener factory methods to instantiate listeners with parameters sent by the LSP combined with any
        # pre-computed information in the add-on. These factory methods are invoked on every request
        Hover.new(response_builder, @config, dispatcher)
      end
    end

    class Hover
      # The Requests::Support::Common module provides some helper methods you may find helpful.
      include Requests::Support::Common

      # Listeners are initialized with the Prism::Dispatcher. This object is used by the Ruby LSP to emit the events
      # when it finds nodes during AST analysis. Listeners must register which nodes they want to handle with the
      # dispatcher (see below).
      # Listeners are initialized with a `ResponseBuilders` object. The listener will push the associated content
      # to this object, which will then build the Ruby LSP's response.
      # Additionally, listeners are instantiated with a message_queue to push notifications (not used in this example).
      # See "Sending notifications to the client" for more information.
      def initialize(response_builder, config, dispatcher)
        @response_builder = response_builder
        @config = config

        # Register that this listener will handle `on_constant_read_node_enter` events (i.e.: whenever a constant read
        # is found in the code)
        dispatcher.register(self, :on_constant_read_node_enter)
      end

      # Listeners must define methods for each event they registered with the dispatcher. In this case, we have to
      # define `on_constant_read_node_enter` to specify what this listener should do every time we find a constant
      def on_constant_read_node_enter(node)
        # Certain builders are made available to listeners to build LSP responses. The classes under
        # `RubyLsp::ResponseBuilders` are used to build responses conforming to the LSP Specification.
        # ResponseBuilders::Hover itself also requires a content category to be specified (title, links,
        # or documentation).
        @response_builder.push("Hello!", category: :documentation)
      end
    end
  end
end
```

#### Dealing with declaration DSLs

Add-ons can inform the Ruby LSP about declarations that are made via meta-programming. By ensuring that the index is
populated with all declarations, features like go to definition, hover, completion, signature help and workspace symbol
will all automatically work.

To achieve this the add-on must create an indexing enhancement class and register it. Here's an example of how to do
it. Consider that a gem defines this DSL:

```ruby
class MyThing < MyLibrary::ParentClass
  # After invoking this method from the `MyLibrary::ParentClass`, a method called `new_method` will be created,
  # accepting a single required parameter named `a`
  my_dsl_that_creates_methods

  # Produces this with meta-programming
  # def my_method(a); end
end
```

This is how you could write an enhancement to teach the Ruby LSP to understand that DSL:

```ruby
class MyIndexingEnhancement < RubyIndexer::Enhancement
  # This on call node handler is invoked any time during indexing when we find a method call. It can be used to insert
  # more entries into the index depending on the conditions
  def on_call_node_enter(node)
    return unless @listener.current_owner

    # Return early unless the method call is the one we want to handle
    return unless node.name == :my_dsl_that_creates_methods

    # Create a new entry to be inserted in the index. This entry will represent the declaration that is created via
    # meta-programming. All entries are defined in the `entry.rb` file.
    #
    # In this example, we will add a new method to the index
    location = node.location

    # Create the array of signatures that this method will accept. Every signatures is composed of a list of
    # parameters. The parameter classes represent each type of parameter
    signatures = [
      RubyIndexer::Entry::Signature.new([RubyIndexer::Entry::RequiredParameter.new(name: :a)])
    ]

    @listener.add_method(
      "new_method", # Name of the method
      location,     # Prism location for the node defining this method
      signatures    # Signatures available to invoke this method
    )
  end

  # This method is invoked when the parser has finished processing the method call node.
  # It can be used to perform cleanups like popping a stack...etc.
  def on_call_node_leave(node); end
end
```

With this the Ruby LSP should automatically handle calls to `my_dsl_that_creates_methods` and create an accurate
representation of the declarations that will be available in the runtime.

### Registering formatters

Gems may also provide a formatter to be used by the Ruby LSP. To do that, the add-on must create a formatter runner and
register it. The formatter is used if the `rubyLsp.formatter` option configured by the user matches the identifier
registered.

```ruby
class MyFormatterRubyLspAddon < RubyLsp::Addon
  def name
    "My Formatter"
  end

  def activate(global_state, message_queue)
    # The first argument is an identifier users can pick to select this formatter. To use this formatter, users must
    # have rubyLsp.formatter configured to "my_formatter"
    # The second argument is a class instance that implements the `FormatterRunner` interface (see below)
    global_state.register_formatter("my_formatter", MyFormatterRunner.new)
  end
end

# Custom formatter
class MyFormatter
  # If using Sorbet to develop the add-on, then include this interface to make sure the class is properly implemented
  include RubyLsp::Requests::Support::Formatter

  # Use the initialize method to perform any sort of ahead of time work. For example, reading configurations for your
  # formatter since they are unlikely to change between requests
  def initialize
    @config = read_config_file!
  end

  # IMPORTANT: None of the following methods should mutate the document in any way or that will lead to a corrupt state!

  # Provide formatting for a given document. This method should return the formatted string for the entire document
  def run_formatting(uri, document)
    source = document.source
    formatted_source = format_the_source_using_my_formatter(source)
    formatted_source
  end

  # Provide diagnostics for the given document. This method must return an array of `RubyLsp::Interface::Diagnostic`
  # objects
  def run_diagnostic(uri, document)
  end
end
```

### Sending notifications to the client

Sometimes, add-ons may need to send asynchronous information to the client. For example, a slow request might want to
indicate progress or diagnostics may be computed in the background without blocking the language server.

For this purpose, all add-ons receive the message queue when activated, which is a thread queue that can receive
notifications for the client. The add-on should keep a reference to this message queue and pass it to listeners that are
interested in using it.

**Note**: do not close the message queue anywhere. The Ruby LSP will handle closing the message queue when appropriate.

```ruby
module RubyLsp
  module MyGem
    class Addon < ::RubyLsp::Addon
      def activate(global_state, message_queue)
        @message_queue = message_queue
      end

      def deactivate; end

      def name
        "Ruby LSP My Gem"
      end

      def version
        "0.1.0"
      end

      def create_hover_listener(response_builder, node_context, index, dispatcher)
        MyHoverListener.new(@message_queue, response_builder, node_context, index, dispatcher)
      end
    end

    class MyHoverListener
      def initialize(message_queue, response_builder, node_context, index, dispatcher)
        @message_queue = message_queue

        @message_queue << Notification.new(
          message: "$/progress",
          params: Interface::ProgressParams.new(
            token: "progress-token-id",
            value: Interface::WorkDoneProgressBegin.new(kind: "begin", title: "Starting slow work!"),
          ),
        )
      end
    end
  end
end
```

### Registering for file update events

By default, the Ruby LSP listens for changes to files ending in `.rb` to continuously update its index when Ruby source
code is modified. If your add-on uses a tool that is configured through a file (like RuboCop and its `.rubocop.yml`)
you can register for changes to these files and react when the configuration changes.

**Note**: you will receive events from `ruby-lsp` and other add-ons as well, in addition to your own registered ones.


```ruby
module RubyLsp
  module MyGem
    class Addon < ::RubyLsp::Addon
      def activate(global_state, message_queue)
        register_additional_file_watchers(global_state, message_queue)
      end

      def deactivate; end

      def version
        "0.1.0"
      end

      def name
        "My Addon"
      end

      def register_additional_file_watchers(global_state, message_queue)
        # Clients are not required to implement this capability
        return unless global_state.supports_watching_files

        message_queue << Request.register_watched_files(
          "ruby-lsp-my-gem-file-watcher",
          "**/.my-config.yml",
          registration_id: "my-config-watcher",
        )
      end

      def workspace_did_change_watched_files(changes)
        if changes.any? { |change| change[:uri].end_with?(".my-config.yml") }
          # Do something to reload the config here
        end
      end
    end
  end
end
```

### Dependency constraints

While we figure out a good design for the add-ons API, breaking changes are bound to happen. To avoid having your
add-on accidentally break editor functionality, you should define the version that your add-on depends on. There are
two ways of achieving this.

#### Add-ons that have a runtime dependency on the ruby-lsp

For add-ons that have a runtime dependency on the `ruby-lsp` gem, you can simply use regular gemspec constraints to
define which version is supported.

```ruby
spec.add_dependency("ruby-lsp", "~> 0.6.0")
```

#### Add-ons that do not have a runtime dependency on the ruby-lsp

For add-ons that are defined inside other gems that do not wish to have a runtime dependency on `ruby-lsp`, please use
the following API to ensure compatibility.

{: .note }
If the Ruby LSP is automatically upgraded to a version not supported by an add-on using this approach, the add-on will
simply not be activated with a warning and the functionality will not be available. The author must update to ensure
compatibility with the current state of the API.

```ruby

# Declare that this add-on supports the base Ruby LSP version v0.18.0, but not v0.19 or above
#
# If the Ruby LSP is upgraded to v0.19.0, this add-on will fail gracefully to activate and a warning will be printed
RubyLsp::Addon.depend_on_ruby_lsp!("~> 0.18.0")

module RubyLsp
  module MyGem
    class Addon < ::RubyLsp::Addon
      def activate(global_state, message_queue)
      end

      def deactivate; end

      def version
        "0.1.0"
      end

      def name
        "My Addon"
      end
    end
  end
end
```

### Testing add-ons

When writing unit tests for add-ons, it's essential to keep in mind that code is rarely in its final state while the
developer is coding. Therefore, be sure to test valid scenarios where the code is still incomplete.

For example, if you are writing a feature related to `require`, do not test `require "library"` exclusively. Consider
intermediate states the user might end up while typing. Additionally, consider syntax that is uncommon, yet still valid
Ruby.

```ruby
# Still no argument
require

# With quotes autocompleted, but no content on the string
require ""

# Using uncommon, but valid syntax, such as invoking require directly on Kernel using parenthesis
Kernel.require("library")
```

The Ruby LSP exports a test helper which creates a server instance with a document already initialized with the desired
content. This is useful to test the integration of your add-on with the language server.

Add-ons are automatically loaded, so simply executing the desired language server request should already include your
add-on's contributions.

```ruby
require "test_helper"
require "ruby_lsp/test_helper"

class MyAddonTest < Minitest::Test
  def test_my_addon_works
    source =  <<~RUBY
      # Some test code that allows you to trigger your add-on's contribution
      class Foo
        def something
        end
      end
    RUBY

    with_server(source) do |server, uri|
      # Tell the server to execute the definition request
      server.process_message(
        id: 1,
        method: "textDocument/definition",
        params: {
          textDocument: {
            uri: uri.to_s,
          },
          position: {
            line: 3,
            character: 5
          }
        }
      )

      # Pop the server's response to the definition request
      result = server.pop_response.response
      # Assert that the response includes your add-on's contribution
      assert_equal(123, result.response.location)
    end
  end
end
```