ruby-lsp 0.4.1 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
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(