ruby-lsp 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -52
  3. data/VERSION +1 -1
  4. data/exe/ruby-lsp +12 -0
  5. data/lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb +62 -0
  6. data/lib/ruby_lsp/document.rb +13 -4
  7. data/lib/ruby_lsp/executor.rb +70 -26
  8. data/lib/ruby_lsp/internal.rb +1 -0
  9. data/lib/ruby_lsp/requests/base_request.rb +15 -6
  10. data/lib/ruby_lsp/requests/code_action_resolve.rb +40 -19
  11. data/lib/ruby_lsp/requests/code_actions.rb +5 -4
  12. data/lib/ruby_lsp/requests/diagnostics.rb +4 -4
  13. data/lib/ruby_lsp/requests/document_highlight.rb +3 -3
  14. data/lib/ruby_lsp/requests/document_link.rb +7 -7
  15. data/lib/ruby_lsp/requests/document_symbol.rb +14 -11
  16. data/lib/ruby_lsp/requests/folding_ranges.rb +38 -11
  17. data/lib/ruby_lsp/requests/formatting.rb +18 -5
  18. data/lib/ruby_lsp/requests/hover.rb +7 -6
  19. data/lib/ruby_lsp/requests/inlay_hints.rb +5 -4
  20. data/lib/ruby_lsp/requests/path_completion.rb +9 -3
  21. data/lib/ruby_lsp/requests/selection_ranges.rb +3 -3
  22. data/lib/ruby_lsp/requests/semantic_highlighting.rb +72 -8
  23. data/lib/ruby_lsp/requests/support/highlight_target.rb +5 -4
  24. data/lib/ruby_lsp/requests/support/rails_document_client.rb +7 -6
  25. data/lib/ruby_lsp/requests/support/selection_range.rb +1 -1
  26. data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +2 -2
  27. data/lib/ruby_lsp/requests/support/sorbet.rb +5 -15
  28. data/lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb +39 -0
  29. data/lib/ruby_lsp/server.rb +4 -1
  30. data/lib/ruby_lsp/store.rb +11 -7
  31. data/lib/ruby_lsp/utils.rb +3 -0
  32. metadata +7 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0812eed9a874fa78fbe75cf2e8dc2bd58c2dfb67a61353f732aaba965ae9d66f'
4
- data.tar.gz: 6d9a567aa54c95f5fd7ada3756d4b5575f068bee90f19f99f83f485d91de83a0
3
+ metadata.gz: f7bf79fd6cdc704c8f874f58985f0e72ac034c76bb70a68166654185f6f00cf1
4
+ data.tar.gz: '083edc726c05ffb6e5e3ae63d480b17fa0f7161ee62349665cc57e309acd5598'
5
5
  SHA512:
6
- metadata.gz: 53311978b0c6d32a34e1e69ddeea1d4b154ee0e2bc1cd426fd7e4db6ab46eeb7a5dff7d0516b5c2c722f95f05a0c5051fe7caf874e14740b5023b5f0e79da962
7
- data.tar.gz: ce04150602707622dd0d463a35a2a9c94fe7b24b1e376dbeeae4e45f4487438219f8ab48056658946e9f7cc56136714496fdfbddf7513b77fea334aac2687ae3
6
+ metadata.gz: f6192daf27dfd82dbb505f5184eebd66308690ce10ead4d4cedc4d9c6667be882a8b9de4c87e66eb88ff43e896307be24bbcc6506b0544b496e1bcb3c9804498
7
+ data.tar.gz: bb68928d9a72b3b2873509bf915f753a202a520187bf5f55da1870d552e0ba0917a7d21ea70d20853ac60aa614f7d85d117d7291ec9c0778cab80e4561a7aba0
data/README.md CHANGED
@@ -1,56 +1,50 @@
1
- ![Build Status](https://github.com/Shopify/ruby-lsp/workflows/CI/badge.svg)
1
+ [![Build Status](https://github.com/Shopify/ruby-lsp/workflows/CI/badge.svg)](https://github.com/Shopify/ruby-lsp/actions/workflows/ci.yml)
2
+ [![Ruby LSP extension](https://img.shields.io/badge/VS%20Code-Ruby%20LSP-success?logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp)
3
+ [![Ruby DX Slack](https://img.shields.io/badge/Slack-Ruby%20DX-success?logo=slack)](https://join.slack.com/t/ruby-dx/shared_invite/zt-1s6f4y15t-v9jedZ9YUPQLM91TEJ4Gew)
2
4
 
3
- # Ruby LSP
4
-
5
- This gem is an implementation of the [language server protocol specification](https://microsoft.github.io/language-server-protocol/) for Ruby, used to improve editor features.
6
-
7
- # Overview
8
-
9
- The intention of Ruby LSP is to provide a fast, robust and feature-rich coding environment for Ruby developers.
10
-
11
- It's part of a [wider Shopify goal](https://github.com/Shopify/vscode-shopify-ruby) to provide a state-of-the-art experience to Ruby developers using modern standards for cross-editor features, documentation and debugging.
12
-
13
- It provides many features, including:
14
5
 
15
- * Syntax highlighting
16
- * Linting and formatting
17
- * Code folding
18
- * Selection ranges
6
+ # Ruby LSP
19
7
 
20
- It does not perform typechecking, so its features are implemented on a best-effort basis, aiming to be as accurate as possible.
8
+ The Ruby LSP is an implementation of the [language server protocol](https://microsoft.github.io/language-server-protocol/)
9
+ for Ruby, used to improve rich features in editors. It is a part of a wider goal to provide a state-of-the-art
10
+ experience to Ruby developers using modern standards for cross-editor features, documentation and debugging.
21
11
 
22
- Planned future features include:
12
+ Want to discuss Ruby developer experience? Consider joining the public
13
+ [Ruby DX Slack workspace](https://join.slack.com/t/ruby-dx/shared_invite/zt-1s6f4y15t-v9jedZ9YUPQLM91TEJ4Gew).
23
14
 
24
- * Auto-completion and navigation ("Go To Definition") ([prototype](https://github.com/Shopify/ruby-lsp/pull/429))
25
- * Support for plug-ins to extend behavior
15
+ ## Usage
26
16
 
27
- The Ruby LSP does not perform any type-checking or provide any type-related assistance, but it can be used alongside [Sorbet](https://github.com/sorbet/sorbet)'s LSP server.
17
+ ### With VS Code
28
18
 
29
- At the time of writing, these are the major differences between Ruby LSP and [Solargraph](https://solargraph.org/):
19
+ If using VS Code, all you have to do is install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to
20
+ get the extra features in the editor. Do not install this gem manually.
30
21
 
31
- * Solargraph [uses](https://solargraph.org/guides/yard) YARD documentation to gather information about your project and its gem dependencies. This provides functionality such as context-aware auto-completion and navigation ("Go To Definition")
32
- * Solargraph can be used as a globally installed gem, but Ruby LSP must be added to the Gemfile or gemspec if using RuboCop. (There are pros and cons to each approach)
22
+ ### With other editors
33
23
 
34
- ## Learn More
35
-
36
- * [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
24
+ See [editors](https://github.com/Shopify/ruby-lsp/blob/main/EDITORS.md) for community instructions on setting up the
25
+ Ruby LSP.
37
26
 
38
- ## Usage
39
-
40
- Install the gem. There's no need to require it, since the server is used as a standalone executable.
27
+ The gem can be installed by doing
28
+ ```shell
29
+ gem install ruby-lsp
30
+ ```
41
31
 
32
+ If you decide to add the gem to the bundle, it is not necessary to require it.
42
33
  ```ruby
43
34
  group :development do
44
35
  gem "ruby-lsp", require: false
45
36
  end
46
37
  ```
47
38
 
48
- If using VS Code, install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features in
49
- the editor.
39
+ ### Documentation
50
40
 
51
41
  See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
52
42
  [supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
53
43
 
44
+ ## Learn More
45
+
46
+ * [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
47
+
54
48
  ## Contributing
55
49
 
56
50
  Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/ruby-lsp.
@@ -112,31 +106,29 @@ To add a new expectations test runner for a new request handler:
112
106
  * Tests with expectations will be checked with `assert_expectations`
113
107
  * Tests without expectations will be ran against your new $HANDLER to check that nothing breaks
114
108
 
115
- ## Debugging
116
-
117
- ### Tracing LSP requests and responses
118
-
119
- LSP server tracing can be controlled through the `ruby lsp.trace.server` config key in the `.vscode/settings.json` config file.
120
-
121
- Possible values are:
109
+ ### Debugging
122
110
 
123
- * `off`: no tracing
124
- * `messages`: display requests and responses notifications
125
- * `verbose`: display each request and response as JSON
111
+ ### Debugging Tests
126
112
 
127
- ### Debugging using VS Code
113
+ 1. Open the test file.
114
+ 2. Set a breakpoint(s) on lines by clicking next to their numbers.
115
+ 3. Open VS Code's `Run and Debug` panel.
116
+ 4. At the top of the panel, select `Minitset - current file` and click the green triangle (or press F5).
128
117
 
129
- The `launch.json` contains a 'Minitest - current file' configuration for the debugger.
118
+ ### Debugging Running Ruby LSP Process
130
119
 
131
- 1. Add a breakpoint using the VS Code UI.
132
- 1. Open the relevant test file.
133
- 1. Open the **Run and Debug** panel on the sidebar.
134
- 1. Ensure `Minitest - current file` is selected in the top dropdown.
135
- 1. Press `F5` OR click the green triangle next to the top dropdown. VS Code will then run the test file with debugger activated.
136
- 1. When the breakpoint is triggered, the process will pause and VS Code will connect to the debugger and activate the debugger UI.
137
- 1. Open the Debug Console view to use the debugger's REPL.
120
+ 1. Open the `vscode-ruby-lsp` project in VS Code.
121
+ 2. [`vscode-ruby-lsp`] Open VS Code's `Run and Debug` panel.
122
+ 3. [`vscode-ruby-lsp`] Select `Run Extension` and click the green triangle (or press F5).
123
+ 4. [`vscode-ruby-lsp`] Now VS Code will:
124
+ - Open another workspace as the `Extension Development Host`.
125
+ - Run `vscode-ruby-lsp` extension in debug mode, which will start a new `ruby-lsp` process with the `--debug` flag.
126
+ 5. Open `ruby-lsp` in VS Code.
127
+ 6. [`ruby-lsp`] Run `bin/rdbg -A` to connect to the running `ruby-lsp` process.
128
+ 7. [`ruby-lsp`] Use commands like `b <file>:<line>` or `b Class#method` to set breakpoints and type `c` to continue the process.
129
+ 8. In your `Extension Development Host` project (e.g. [`Tapioca`](https://github.com/Shopify/tapioca)), trigger the request that will hit the breakpoint.
138
130
 
139
- ## Spell Checking
131
+ ### Spell Checking
140
132
 
141
133
  VS Code users will be prompted to enable the [Code Spell Checker](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) extension.
142
134
  By default this will be enabled for all workspaces, but you can choose to selectively enable or disable it per workspace.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.1
1
+ 0.4.3
data/exe/ruby-lsp CHANGED
@@ -19,4 +19,16 @@ rescue
19
19
  end
20
20
 
21
21
  require_relative "../lib/ruby_lsp/internal"
22
+
23
+ if ARGV.include?("--debug")
24
+ sockets_dir = "/tmp/ruby-lsp-debug-sockets"
25
+ Dir.mkdir(sockets_dir) unless Dir.exist?(sockets_dir)
26
+ # ruby-debug-ENV["USER"] is an implicit naming pattern in ruby/debug
27
+ # if it's not present, rdbg will not find the socket
28
+ socket_identifier = "ruby-debug-#{ENV["USER"]}-#{File.basename(Dir.pwd)}.sock"
29
+ ENV["RUBY_DEBUG_SOCK_PATH"] = "#{sockets_dir}/#{socket_identifier}"
30
+
31
+ require "debug/open_nonstop"
32
+ end
33
+
22
34
  RubyLsp::Server.new.start
@@ -0,0 +1,62 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "rubocop"
5
+ require "sorbet-runtime"
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module RubyLsp
10
+ # Prefer using `Interface`, `Transport` and `Constant` aliases
11
+ # within the `RubyLsp` module, without having to prefix with
12
+ # `LanguageServer::Protocol`
13
+ #
14
+ # @example
15
+ # # bad
16
+ # module RubyLsp
17
+ # class FoldingRanges
18
+ # sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::FoldingRange], Object)) }
19
+ # def run; end
20
+ # end
21
+ #
22
+ # # good
23
+ # module RubyLsp
24
+ # class FoldingRanges
25
+ # sig { override.returns(T.all(T::Array[Interface::FoldingRange], Object)) }
26
+ # def run; end
27
+ # end
28
+ # end
29
+ class UseLanguageServerAliases < RuboCop::Cop::Base
30
+ extend RuboCop::Cop::AutoCorrector
31
+
32
+ ALIASED_CONSTANTS = T.let([:Interface, :Transport, :Constant].freeze, T::Array[Symbol])
33
+
34
+ MSG = "Use constant alias `%{constant}`."
35
+
36
+ def_node_search :ruby_lsp_modules, <<~PATTERN
37
+ (module (const nil? :RubyLsp) ...)
38
+ PATTERN
39
+
40
+ def_node_search :lsp_constant_usages, <<~PATTERN
41
+ (const (const (const nil? :LanguageServer) :Protocol) {:Interface | :Transport | :Constant})
42
+ PATTERN
43
+
44
+ def on_new_investigation
45
+ return if processed_source.blank?
46
+
47
+ ruby_lsp_modules(processed_source.ast).each do |ruby_lsp_mod|
48
+ lsp_constant_usages(ruby_lsp_mod).each do |node|
49
+ lsp_const = node.children.last
50
+
51
+ next unless ALIASED_CONSTANTS.include?(lsp_const)
52
+
53
+ add_offense(node, message: format(MSG, constant: lsp_const)) do |corrector|
54
+ corrector.replace(node, lsp_const)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -15,11 +15,19 @@ module RubyLsp
15
15
  sig { returns(String) }
16
16
  attr_reader :source
17
17
 
18
- sig { params(source: String, encoding: String).void }
19
- def initialize(source, encoding = "utf-8")
18
+ sig { returns(Integer) }
19
+ attr_reader :version
20
+
21
+ sig { returns(String) }
22
+ attr_reader :uri
23
+
24
+ sig { params(source: String, version: Integer, uri: String, encoding: String).void }
25
+ def initialize(source:, version:, uri:, encoding: "utf-8")
20
26
  @cache = T.let({}, T::Hash[Symbol, T.untyped])
21
27
  @encoding = T.let(encoding, String)
22
28
  @source = T.let(source, String)
29
+ @version = T.let(version, Integer)
30
+ @uri = T.let(uri, String)
23
31
  @unparsed_edits = T.let([], T::Array[EditShape])
24
32
  @syntax_error = T.let(false, T::Boolean)
25
33
  @tree = T.let(SyntaxTree.parse(@source), T.nilable(SyntaxTree::Node))
@@ -48,8 +56,8 @@ module RubyLsp
48
56
  result
49
57
  end
50
58
 
51
- sig { params(edits: T::Array[EditShape]).void }
52
- def push_edits(edits)
59
+ sig { params(edits: T::Array[EditShape], version: Integer).void }
60
+ def push_edits(edits, version:)
53
61
  edits.each do |edit|
54
62
  range = edit[:range]
55
63
  scanner = create_scanner
@@ -60,6 +68,7 @@ module RubyLsp
60
68
  @source[start_position...end_position] = edit[:text]
61
69
  end
62
70
 
71
+ @version = version
63
72
  @unparsed_edits.concat(edits)
64
73
  @cache.clear
65
74
  end
@@ -41,7 +41,11 @@ module RubyLsp
41
41
  warn("Ruby LSP is ready")
42
42
  VOID
43
43
  when "textDocument/didOpen"
44
- text_document_did_open(uri, request.dig(:params, :textDocument, :text))
44
+ text_document_did_open(
45
+ uri,
46
+ request.dig(:params, :textDocument, :text),
47
+ request.dig(:params, :textDocument, :version),
48
+ )
45
49
  when "textDocument/didClose"
46
50
  @notifications << Notification.new(
47
51
  message: "textDocument/publishDiagnostics",
@@ -50,7 +54,11 @@ module RubyLsp
50
54
 
51
55
  text_document_did_close(uri)
52
56
  when "textDocument/didChange"
53
- text_document_did_change(uri, request.dig(:params, :contentChanges))
57
+ text_document_did_change(
58
+ uri,
59
+ request.dig(:params, :contentChanges),
60
+ request.dig(:params, :textDocument, :version),
61
+ )
54
62
  when "textDocument/foldingRange"
55
63
  folding_range(uri)
56
64
  when "textDocument/documentLink"
@@ -66,6 +74,16 @@ module RubyLsp
66
74
  when "textDocument/formatting"
67
75
  begin
68
76
  formatting(uri)
77
+ rescue Requests::Formatting::InvalidFormatter => error
78
+ @notifications << Notification.new(
79
+ message: "window/showMessage",
80
+ params: Interface::ShowMessageParams.new(
81
+ type: Constant::MessageType::ERROR,
82
+ message: "Configuration error: #{error.message}",
83
+ ),
84
+ )
85
+
86
+ nil
69
87
  rescue StandardError => error
70
88
  @notifications << Notification.new(
71
89
  message: "window/showMessage",
@@ -128,7 +146,7 @@ module RubyLsp
128
146
  sig { params(uri: String).returns(T::Array[Interface::DocumentLink]) }
129
147
  def document_link(uri)
130
148
  @store.cache_fetch(uri, :document_link) do |document|
131
- RubyLsp::Requests::DocumentLink.new(uri, document).run
149
+ RubyLsp::Requests::DocumentLink.new(document).run
132
150
  end
133
151
  end
134
152
 
@@ -139,15 +157,15 @@ module RubyLsp
139
157
  end
140
158
  end
141
159
 
142
- sig { params(uri: String, content_changes: T::Array[Document::EditShape]).returns(Object) }
143
- def text_document_did_change(uri, content_changes)
144
- @store.push_edits(uri, content_changes)
160
+ sig { params(uri: String, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
161
+ def text_document_did_change(uri, content_changes, version)
162
+ @store.push_edits(uri: uri, edits: content_changes, version: version)
145
163
  VOID
146
164
  end
147
165
 
148
- sig { params(uri: String, text: String).returns(Object) }
149
- def text_document_did_open(uri, text)
150
- @store.set(uri, text)
166
+ sig { params(uri: String, text: String, version: Integer).returns(Object) }
167
+ def text_document_did_open(uri, text, version)
168
+ @store.set(uri: uri, source: text, version: version)
151
169
  VOID
152
170
  end
153
171
 
@@ -197,7 +215,7 @@ module RubyLsp
197
215
 
198
216
  sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
199
217
  def formatting(uri)
200
- Requests::Formatting.new(uri, @store.get(uri)).run
218
+ Requests::Formatting.new(@store.get(uri), formatter: @store.formatter).run
201
219
  end
202
220
 
203
221
  sig do
@@ -240,7 +258,7 @@ module RubyLsp
240
258
  def code_action(uri, range, context)
241
259
  document = @store.get(uri)
242
260
 
243
- Requests::CodeActions.new(uri, document, range, context).run
261
+ Requests::CodeActions.new(document, range, context).run
244
262
  end
245
263
 
246
264
  sig { params(params: T::Hash[Symbol, T.untyped]).returns(Interface::CodeAction) }
@@ -259,6 +277,15 @@ module RubyLsp
259
277
  ),
260
278
  )
261
279
  raise Requests::CodeActionResolve::CodeActionError
280
+ when Requests::CodeActionResolve::Error::InvalidTargetRange
281
+ @notifications << Notification.new(
282
+ message: "window/showMessage",
283
+ params: Interface::ShowMessageParams.new(
284
+ type: Constant::MessageType::ERROR,
285
+ message: "Couldn't find an appropriate location to place extracted refactor",
286
+ ),
287
+ )
288
+ raise Requests::CodeActionResolve::CodeActionError
262
289
  else
263
290
  result
264
291
  end
@@ -267,7 +294,7 @@ module RubyLsp
267
294
  sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
268
295
  def diagnostic(uri)
269
296
  response = @store.cache_fetch(uri, :diagnostics) do |document|
270
- Requests::Diagnostics.new(uri, document).run
297
+ Requests::Diagnostics.new(document).run
271
298
  end
272
299
 
273
300
  Interface::FullDocumentDiagnosticReport.new(kind: "full", items: response.map(&:to_lsp_diagnostic)) if response
@@ -300,9 +327,26 @@ module RubyLsp
300
327
  def initialize_request(options)
301
328
  @store.clear
302
329
  @store.encoding = options.dig(:capabilities, :general, :positionEncodings)
303
- enabled_features = options.dig(:initializationOptions, :enabledFeatures) || []
330
+ formatter = options.dig(:initializationOptions, :formatter)
331
+ @store.formatter = formatter unless formatter.nil?
332
+
333
+ configured_features = options.dig(:initializationOptions, :enabledFeatures)
334
+
335
+ enabled_features = case configured_features
336
+ when Array
337
+ # If the configuration is using an array, then absent features are disabled and present ones are enabled. That's
338
+ # why we use `false` as the default value
339
+ Hash.new(false).merge!(configured_features.to_h { |feature| [feature, true] })
340
+ when Hash
341
+ # If the configuration is already a hash, merge it with a default value of `true`. That way clients don't have
342
+ # to opt-in to every single feature
343
+ Hash.new(true).merge!(configured_features)
344
+ else
345
+ # If no configuration was passed by the client, just enable every feature
346
+ Hash.new(true)
347
+ end
304
348
 
305
- document_symbol_provider = if enabled_features.include?("documentSymbols")
349
+ document_symbol_provider = if enabled_features["documentSymbols"]
306
350
  Interface::DocumentSymbolClientCapabilities.new(
307
351
  hierarchical_document_symbol_support: true,
308
352
  symbol_kind: {
@@ -311,19 +355,19 @@ module RubyLsp
311
355
  )
312
356
  end
313
357
 
314
- document_link_provider = if enabled_features.include?("documentLink")
358
+ document_link_provider = if enabled_features["documentLink"]
315
359
  Interface::DocumentLinkOptions.new(resolve_provider: false)
316
360
  end
317
361
 
318
- hover_provider = if enabled_features.include?("hover")
362
+ hover_provider = if enabled_features["hover"]
319
363
  Interface::HoverClientCapabilities.new(dynamic_registration: false)
320
364
  end
321
365
 
322
- folding_ranges_provider = if enabled_features.include?("foldingRanges")
366
+ folding_ranges_provider = if enabled_features["foldingRanges"]
323
367
  Interface::FoldingRangeClientCapabilities.new(line_folding_only: true)
324
368
  end
325
369
 
326
- semantic_tokens_provider = if enabled_features.include?("semanticHighlighting")
370
+ semantic_tokens_provider = if enabled_features["semanticHighlighting"]
327
371
  Interface::SemanticTokensRegistrationOptions.new(
328
372
  document_selector: { scheme: "file", language: "ruby" },
329
373
  legend: Interface::SemanticTokensLegend.new(
@@ -335,29 +379,29 @@ module RubyLsp
335
379
  )
336
380
  end
337
381
 
338
- diagnostics_provider = if enabled_features.include?("diagnostics")
382
+ diagnostics_provider = if enabled_features["diagnostics"]
339
383
  {
340
384
  interFileDependencies: false,
341
385
  workspaceDiagnostics: false,
342
386
  }
343
387
  end
344
388
 
345
- on_type_formatting_provider = if enabled_features.include?("onTypeFormatting")
389
+ on_type_formatting_provider = if enabled_features["onTypeFormatting"]
346
390
  Interface::DocumentOnTypeFormattingOptions.new(
347
391
  first_trigger_character: "{",
348
392
  more_trigger_character: ["\n", "|"],
349
393
  )
350
394
  end
351
395
 
352
- code_action_provider = if enabled_features.include?("codeActions")
396
+ code_action_provider = if enabled_features["codeActions"]
353
397
  Interface::CodeActionOptions.new(resolve_provider: true)
354
398
  end
355
399
 
356
- inlay_hint_provider = if enabled_features.include?("inlayHint")
400
+ inlay_hint_provider = if enabled_features["inlayHint"]
357
401
  Interface::InlayHintOptions.new(resolve_provider: false)
358
402
  end
359
403
 
360
- completion_provider = if enabled_features.include?("completion")
404
+ completion_provider = if enabled_features["completion"]
361
405
  Interface::CompletionOptions.new(
362
406
  resolve_provider: false,
363
407
  trigger_characters: ["/"],
@@ -370,14 +414,14 @@ module RubyLsp
370
414
  change: Constant::TextDocumentSyncKind::INCREMENTAL,
371
415
  open_close: true,
372
416
  ),
373
- selection_range_provider: enabled_features.include?("selectionRanges"),
417
+ selection_range_provider: enabled_features["selectionRanges"],
374
418
  hover_provider: hover_provider,
375
419
  document_symbol_provider: document_symbol_provider,
376
420
  document_link_provider: document_link_provider,
377
421
  folding_range_provider: folding_ranges_provider,
378
422
  semantic_tokens_provider: semantic_tokens_provider,
379
- document_formatting_provider: enabled_features.include?("formatting"),
380
- document_highlight_provider: enabled_features.include?("documentHighlights"),
423
+ document_formatting_provider: enabled_features["formatting"] && formatter != "none",
424
+ document_highlight_provider: enabled_features["documentHighlights"],
381
425
  code_action_provider: code_action_provider,
382
426
  document_on_type_formatting_provider: on_type_formatting_provider,
383
427
  diagnostic_provider: diagnostics_provider,
@@ -5,6 +5,7 @@ require "sorbet-runtime"
5
5
  require "syntax_tree"
6
6
  require "language_server-protocol"
7
7
  require "benchmark"
8
+ require "bundler"
8
9
 
9
10
  require "ruby-lsp"
10
11
  require "ruby_lsp/utils"
@@ -28,16 +28,24 @@ module RubyLsp
28
28
  sig { abstract.returns(Object) }
29
29
  def run; end
30
30
 
31
- sig { params(node: SyntaxTree::Node).returns(LanguageServer::Protocol::Interface::Range) }
31
+ # Syntax Tree implements `visit_all` using `map` instead of `each` for users who want to use the pattern
32
+ # `result = visitor.visit(tree)`. However, we don't use that pattern and should avoid producing a new array for
33
+ # every single node visited
34
+ sig { params(nodes: T::Array[SyntaxTree::Node]).void }
35
+ def visit_all(nodes)
36
+ nodes.each { |node| visit(node) }
37
+ end
38
+
39
+ sig { params(node: SyntaxTree::Node).returns(Interface::Range) }
32
40
  def range_from_syntax_tree_node(node)
33
41
  loc = node.location
34
42
 
35
- LanguageServer::Protocol::Interface::Range.new(
36
- start: LanguageServer::Protocol::Interface::Position.new(
43
+ Interface::Range.new(
44
+ start: Interface::Position.new(
37
45
  line: loc.start_line - 1,
38
46
  character: loc.start_column,
39
47
  ),
40
- end: LanguageServer::Protocol::Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
48
+ end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
41
49
  )
42
50
  end
43
51
 
@@ -89,14 +97,15 @@ module RubyLsp
89
97
  # If the node's start character is already past the position, then we should've found the closest node already
90
98
  break if position < loc.start_char
91
99
 
100
+ # If there are node types to filter by, and the current node is not one of those types, then skip it
101
+ next if node_types.any? && node_types.none? { |type| candidate.is_a?(type) }
102
+
92
103
  # If the current node is narrower than or equal to the previous closest node, then it is more precise
93
104
  closest_loc = closest.location
94
105
  if loc.end_char - loc.start_char <= closest_loc.end_char - closest_loc.start_char
95
106
  parent = T.let(closest, SyntaxTree::Node)
96
107
  closest = candidate
97
108
  end
98
-
99
- break if node_types.any? { |type| candidate.is_a?(type) }
100
109
  end
101
110
 
102
111
  [closest, parent]
@@ -30,6 +30,7 @@ module RubyLsp
30
30
  class Error < ::T::Enum
31
31
  enums do
32
32
  EmptySelection = new
33
+ InvalidTargetRange = new
33
34
  end
34
35
  end
35
36
 
@@ -45,11 +46,44 @@ module RubyLsp
45
46
  source_range = @code_action.dig(:data, :range)
46
47
  return Error::EmptySelection if source_range[:start] == source_range[:end]
47
48
 
49
+ return Error::InvalidTargetRange if @document.syntax_error?
50
+
48
51
  scanner = @document.create_scanner
49
52
  start_index = scanner.find_char_position(source_range[:start])
50
53
  end_index = scanner.find_char_position(source_range[:end])
51
- extraction_source = T.must(@document.source[start_index...end_index])
52
- source_line_indentation = T.must(T.must(@document.source.lines[source_range.dig(:start, :line)])[/\A */]).size
54
+ extracted_source = T.must(@document.source[start_index...end_index])
55
+
56
+ # Find the closest statements node, so that we place the refactor in a valid position
57
+ closest_statements = locate(T.must(@document.tree), start_index, node_types: [SyntaxTree::Statements]).first
58
+ return Error::InvalidTargetRange if closest_statements.nil?
59
+
60
+ # Find the node with the end line closest to the requested position, so that we can place the refactor
61
+ # immediately after that closest node
62
+ closest_node = closest_statements.child_nodes.compact.min_by do |node|
63
+ distance = source_range.dig(:start, :line) - (node.location.end_line - 1)
64
+ distance <= 0 ? Float::INFINITY : distance
65
+ end
66
+
67
+ # When trying to extract the first node inside of a statements block, then we can just select one line above it
68
+ target_line = if closest_node == closest_statements.child_nodes.first
69
+ closest_node.location.start_line - 1
70
+ else
71
+ closest_node.location.end_line
72
+ end
73
+
74
+ lines = @document.source.lines
75
+ indentation = T.must(T.must(lines[target_line - 1])[/\A */]).size
76
+
77
+ target_range = {
78
+ start: { line: target_line, character: indentation },
79
+ end: { line: target_line, character: indentation },
80
+ }
81
+
82
+ variable_source = if T.must(lines[target_line]).strip.empty?
83
+ "\n#{" " * indentation}#{NEW_VARIABLE_NAME} = #{extracted_source}"
84
+ else
85
+ "#{NEW_VARIABLE_NAME} = #{extracted_source}\n#{" " * indentation}"
86
+ end
53
87
 
54
88
  Interface::CodeAction.new(
55
89
  title: "Refactor: Extract Variable",
@@ -60,7 +94,10 @@ module RubyLsp
60
94
  uri: @code_action.dig(:data, :uri),
61
95
  version: nil,
62
96
  ),
63
- edits: edits_to_extract_variable(source_range, extraction_source, source_line_indentation),
97
+ edits: [
98
+ create_text_edit(source_range, NEW_VARIABLE_NAME),
99
+ create_text_edit(target_range, variable_source),
100
+ ],
64
101
  ),
65
102
  ],
66
103
  ),
@@ -69,22 +106,6 @@ module RubyLsp
69
106
 
70
107
  private
71
108
 
72
- sig do
73
- params(range: Document::RangeShape, source: String, indentation: Integer)
74
- .returns(T::Array[Interface::TextEdit])
75
- end
76
- def edits_to_extract_variable(range, source, indentation)
77
- target_range = {
78
- start: { line: range.dig(:start, :line), character: indentation },
79
- end: { line: range.dig(:start, :line), character: indentation },
80
- }
81
-
82
- [
83
- create_text_edit(range, NEW_VARIABLE_NAME),
84
- create_text_edit(target_range, "#{NEW_VARIABLE_NAME} = #{source}\n#{" " * indentation}"),
85
- ]
86
- end
87
-
88
109
  sig { params(range: Document::RangeShape, new_text: String).returns(Interface::TextEdit) }
89
110
  def create_text_edit(range, new_text)
90
111
  Interface::TextEdit.new(