ruby-lsp 0.4.0 → 0.4.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65db08cd80bfe40116f70065b717ad88d1b1fd4fd86c6fcf1de2d5be9636ddce
4
- data.tar.gz: 84a483aef0e88650a0b2bdbd0db0c3a5c1cd1cfff1e87508a09d7d4d8a616f36
3
+ metadata.gz: c5d9c6b487aab55ce25eb2741fff92afa8ca0ee544f21374debacce7f887cc50
4
+ data.tar.gz: 8da9da85900753306f5e8558bc1a42f25b7173ffe1087ea70612c10ef5558e27
5
5
  SHA512:
6
- metadata.gz: 1ff522bae2c2d469c1fb5817c1b9ed65867ad19d64e00c0b14d4faa6b38bd62321c3109292f1021174766e2da240bd17faea1f8185672164307d2dafe88845bb
7
- data.tar.gz: 7e89c4c38b833269e9d9a09f55333d32f6e1ef4872174d92e7b92a048cf51f62a837b7d9d4c217f3cccb9a196bb6d3b004439e4de8cc9e588c9b445a2f0c46e8
6
+ metadata.gz: 3dca630b9cae262f1af8d0aef0300ab7399dafd348db5f578ed7fdffb22a7afd48fe9467aa65e467fb50e16663f8fad32f1d0cc46b30819b06a6b7472700dcc0
7
+ data.tar.gz: f048c6c9fe14d20edefeca7ac504942af34c61699163f0d09ec54f4d80e7469e1bb1762757b96aa3d8d06a6e8168473dacfc932112b2d31ca4b380748eef8899
data/README.md CHANGED
@@ -45,8 +45,9 @@ group :development do
45
45
  end
46
46
  ```
47
47
 
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.
48
+ If using VS Code, install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features
49
+ in the editor. See [editors](https://github.com/Shopify/ruby-lsp/blob/main/EDITORS.md) for community instructions on
50
+ setting up the Ruby LSP in different editors.
50
51
 
51
52
  See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
52
53
  [supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
@@ -114,27 +115,25 @@ To add a new expectations test runner for a new request handler:
114
115
 
115
116
  ## Debugging
116
117
 
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:
122
-
123
- * `off`: no tracing
124
- * `messages`: display requests and responses notifications
125
- * `verbose`: display each request and response as JSON
126
-
127
- ### Debugging using VS Code
128
-
129
- The `launch.json` contains a 'Minitest - current file' configuration for the debugger.
130
-
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.
118
+ ### Debugging Tests
119
+
120
+ 1. Open the test file.
121
+ 2. Set a breakpoint(s) on lines by clicking next to their numbers.
122
+ 3. Open VS Code's `Run and Debug` panel.
123
+ 4. At the top of the panel, select `Minitset - current file` and click the green triangle (or press F5).
124
+
125
+ ### Debugging Running Ruby LSP Process
126
+
127
+ 1. Open the `vscode-ruby-lsp` project in VS Code.
128
+ 2. [`vscode-ruby-lsp`] Open VS Code's `Run and Debug` panel.
129
+ 3. [`vscode-ruby-lsp`] Select `Run Extension` and click the green triangle (or press F5).
130
+ 4. [`vscode-ruby-lsp`] Now VS Code will:
131
+ - Open another workspace as the `Extension Development Host`.
132
+ - Run `vscode-ruby-lsp` extension in debug mode, which will start a new `ruby-lsp` process with the `--debug` flag.
133
+ 5. Open `ruby-lsp` in VS Code.
134
+ 6. [`ruby-lsp`] Run `bin/rdbg -A` to connect to the running `ruby-lsp` process.
135
+ 7. [`ruby-lsp`] Use commands like `b <file>:<line>` or `b Class#method` to set breakpoints and type `c` to continue the process.
136
+ 8. In your `Extension Development Host` project (e.g. [`Tapioca`](https://github.com/Shopify/tapioca)), trigger the request that will hit the breakpoint.
138
137
 
139
138
  ## Spell Checking
140
139
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.4.2
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
@@ -21,9 +21,10 @@ module RubyLsp
21
21
  @encoding = T.let(encoding, String)
22
22
  @source = T.let(source, String)
23
23
  @unparsed_edits = T.let([], T::Array[EditShape])
24
+ @syntax_error = T.let(false, T::Boolean)
24
25
  @tree = T.let(SyntaxTree.parse(@source), T.nilable(SyntaxTree::Node))
25
26
  rescue SyntaxTree::Parser::ParseError
26
- # Do not raise if we failed to parse
27
+ @syntax_error = true
27
28
  end
28
29
 
29
30
  sig { params(other: Document).returns(T::Boolean) }
@@ -68,14 +69,15 @@ module RubyLsp
68
69
  return if @unparsed_edits.empty?
69
70
 
70
71
  @tree = SyntaxTree.parse(@source)
72
+ @syntax_error = false
71
73
  @unparsed_edits.clear
72
74
  rescue SyntaxTree::Parser::ParseError
73
- # Do nothing if we fail parse
75
+ @syntax_error = true
74
76
  end
75
77
 
76
78
  sig { returns(T::Boolean) }
77
79
  def syntax_error?
78
- @unparsed_edits.any?
80
+ @syntax_error
79
81
  end
80
82
 
81
83
  sig { returns(T::Boolean) }
@@ -37,6 +37,9 @@ module RubyLsp
37
37
  case request[:method]
38
38
  when "initialize"
39
39
  initialize_request(request.dig(:params))
40
+ when "initialized"
41
+ warn("Ruby LSP is ready")
42
+ VOID
40
43
  when "textDocument/didOpen"
41
44
  text_document_did_open(uri, request.dig(:params, :textDocument, :text))
42
45
  when "textDocument/didClose"
@@ -63,6 +66,16 @@ module RubyLsp
63
66
  when "textDocument/formatting"
64
67
  begin
65
68
  formatting(uri)
69
+ rescue Requests::Formatting::InvalidFormatter => error
70
+ @notifications << Notification.new(
71
+ message: "window/showMessage",
72
+ params: Interface::ShowMessageParams.new(
73
+ type: Constant::MessageType::ERROR,
74
+ message: "Configuration error: #{error.message}",
75
+ ),
76
+ )
77
+
78
+ nil
66
79
  rescue StandardError => error
67
80
  @notifications << Notification.new(
68
81
  message: "window/showMessage",
@@ -84,6 +97,8 @@ module RubyLsp
84
97
  inlay_hint(uri, request.dig(:params, :range))
85
98
  when "textDocument/codeAction"
86
99
  code_action(uri, request.dig(:params, :range), request.dig(:params, :context))
100
+ when "codeAction/resolve"
101
+ code_action_resolve(request.dig(:params))
87
102
  when "textDocument/diagnostic"
88
103
  begin
89
104
  diagnostic(uri)
@@ -192,7 +207,7 @@ module RubyLsp
192
207
 
193
208
  sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
194
209
  def formatting(uri)
195
- Requests::Formatting.new(uri, @store.get(uri)).run
210
+ Requests::Formatting.new(uri, @store.get(uri), formatter: @store.formatter).run
196
211
  end
197
212
 
198
213
  sig do
@@ -233,11 +248,39 @@ module RubyLsp
233
248
  ).returns(T.nilable(T::Array[Interface::CodeAction]))
234
249
  end
235
250
  def code_action(uri, range, context)
236
- start_line = range.dig(:start, :line)
237
- end_line = range.dig(:end, :line)
238
251
  document = @store.get(uri)
239
252
 
240
- Requests::CodeActions.new(uri, document, start_line..end_line, context).run
253
+ Requests::CodeActions.new(uri, document, range, context).run
254
+ end
255
+
256
+ sig { params(params: T::Hash[Symbol, T.untyped]).returns(Interface::CodeAction) }
257
+ def code_action_resolve(params)
258
+ uri = params.dig(:data, :uri)
259
+ document = @store.get(uri)
260
+ result = Requests::CodeActionResolve.new(document, params).run
261
+
262
+ case result
263
+ when Requests::CodeActionResolve::Error::EmptySelection
264
+ @notifications << Notification.new(
265
+ message: "window/showMessage",
266
+ params: Interface::ShowMessageParams.new(
267
+ type: Constant::MessageType::ERROR,
268
+ message: "Invalid selection for Extract Variable refactor",
269
+ ),
270
+ )
271
+ raise Requests::CodeActionResolve::CodeActionError
272
+ when Requests::CodeActionResolve::Error::InvalidTargetRange
273
+ @notifications << Notification.new(
274
+ message: "window/showMessage",
275
+ params: Interface::ShowMessageParams.new(
276
+ type: Constant::MessageType::ERROR,
277
+ message: "Couldn't find an appropriate location to place extracted refactor",
278
+ ),
279
+ )
280
+ raise Requests::CodeActionResolve::CodeActionError
281
+ else
282
+ result
283
+ end
241
284
  end
242
285
 
243
286
  sig { params(uri: String).returns(T.nilable(Interface::FullDocumentDiagnosticReport)) }
@@ -276,9 +319,26 @@ module RubyLsp
276
319
  def initialize_request(options)
277
320
  @store.clear
278
321
  @store.encoding = options.dig(:capabilities, :general, :positionEncodings)
279
- enabled_features = options.dig(:initializationOptions, :enabledFeatures) || []
322
+ formatter = options.dig(:initializationOptions, :formatter)
323
+ @store.formatter = formatter unless formatter.nil?
324
+
325
+ configured_features = options.dig(:initializationOptions, :enabledFeatures)
326
+
327
+ enabled_features = case configured_features
328
+ when Array
329
+ # If the configuration is using an array, then absent features are disabled and present ones are enabled. That's
330
+ # why we use `false` as the default value
331
+ Hash.new(false).merge!(configured_features.to_h { |feature| [feature, true] })
332
+ when Hash
333
+ # If the configuration is already a hash, merge it with a default value of `true`. That way clients don't have
334
+ # to opt-in to every single feature
335
+ Hash.new(true).merge!(configured_features)
336
+ else
337
+ # If no configuration was passed by the client, just enable every feature
338
+ Hash.new(true)
339
+ end
280
340
 
281
- document_symbol_provider = if enabled_features.include?("documentSymbols")
341
+ document_symbol_provider = if enabled_features["documentSymbols"]
282
342
  Interface::DocumentSymbolClientCapabilities.new(
283
343
  hierarchical_document_symbol_support: true,
284
344
  symbol_kind: {
@@ -287,19 +347,19 @@ module RubyLsp
287
347
  )
288
348
  end
289
349
 
290
- document_link_provider = if enabled_features.include?("documentLink")
350
+ document_link_provider = if enabled_features["documentLink"]
291
351
  Interface::DocumentLinkOptions.new(resolve_provider: false)
292
352
  end
293
353
 
294
- hover_provider = if enabled_features.include?("hover")
354
+ hover_provider = if enabled_features["hover"]
295
355
  Interface::HoverClientCapabilities.new(dynamic_registration: false)
296
356
  end
297
357
 
298
- folding_ranges_provider = if enabled_features.include?("foldingRanges")
358
+ folding_ranges_provider = if enabled_features["foldingRanges"]
299
359
  Interface::FoldingRangeClientCapabilities.new(line_folding_only: true)
300
360
  end
301
361
 
302
- semantic_tokens_provider = if enabled_features.include?("semanticHighlighting")
362
+ semantic_tokens_provider = if enabled_features["semanticHighlighting"]
303
363
  Interface::SemanticTokensRegistrationOptions.new(
304
364
  document_selector: { scheme: "file", language: "ruby" },
305
365
  legend: Interface::SemanticTokensLegend.new(
@@ -311,25 +371,29 @@ module RubyLsp
311
371
  )
312
372
  end
313
373
 
314
- diagnostics_provider = if enabled_features.include?("diagnostics")
374
+ diagnostics_provider = if enabled_features["diagnostics"]
315
375
  {
316
376
  interFileDependencies: false,
317
377
  workspaceDiagnostics: false,
318
378
  }
319
379
  end
320
380
 
321
- on_type_formatting_provider = if enabled_features.include?("onTypeFormatting")
381
+ on_type_formatting_provider = if enabled_features["onTypeFormatting"]
322
382
  Interface::DocumentOnTypeFormattingOptions.new(
323
383
  first_trigger_character: "{",
324
384
  more_trigger_character: ["\n", "|"],
325
385
  )
326
386
  end
327
387
 
328
- inlay_hint_provider = if enabled_features.include?("inlayHint")
388
+ code_action_provider = if enabled_features["codeActions"]
389
+ Interface::CodeActionOptions.new(resolve_provider: true)
390
+ end
391
+
392
+ inlay_hint_provider = if enabled_features["inlayHint"]
329
393
  Interface::InlayHintOptions.new(resolve_provider: false)
330
394
  end
331
395
 
332
- completion_provider = if enabled_features.include?("completion")
396
+ completion_provider = if enabled_features["completion"]
333
397
  Interface::CompletionOptions.new(
334
398
  resolve_provider: false,
335
399
  trigger_characters: ["/"],
@@ -342,15 +406,15 @@ module RubyLsp
342
406
  change: Constant::TextDocumentSyncKind::INCREMENTAL,
343
407
  open_close: true,
344
408
  ),
345
- selection_range_provider: enabled_features.include?("selectionRanges"),
409
+ selection_range_provider: enabled_features["selectionRanges"],
346
410
  hover_provider: hover_provider,
347
411
  document_symbol_provider: document_symbol_provider,
348
412
  document_link_provider: document_link_provider,
349
413
  folding_range_provider: folding_ranges_provider,
350
414
  semantic_tokens_provider: semantic_tokens_provider,
351
- document_formatting_provider: enabled_features.include?("formatting"),
352
- document_highlight_provider: enabled_features.include?("documentHighlights"),
353
- code_action_provider: enabled_features.include?("codeActions"),
415
+ document_formatting_provider: enabled_features["formatting"] && formatter != "none",
416
+ document_highlight_provider: enabled_features["documentHighlights"],
417
+ code_action_provider: code_action_provider,
354
418
  document_on_type_formatting_provider: on_type_formatting_provider,
355
419
  diagnostic_provider: diagnostics_provider,
356
420
  inlay_hint_provider: inlay_hint_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,6 +28,14 @@ module RubyLsp
28
28
  sig { abstract.returns(Object) }
29
29
  def run; end
30
30
 
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
+
31
39
  sig { params(node: SyntaxTree::Node).returns(LanguageServer::Protocol::Interface::Range) }
32
40
  def range_from_syntax_tree_node(node)
33
41
  loc = node.location
@@ -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]
@@ -0,0 +1,121 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![Code action resolve demo](../../misc/code_action_resolve.gif)
7
+ #
8
+ # The [code action resolve](https://microsoft.github.io/language-server-protocol/specification#codeAction_resolve)
9
+ # request is used to to resolve the edit field for a given code action, if it is not already provided in the
10
+ # textDocument/codeAction response. We can use it for scenarios that require more computation such as refactoring.
11
+ #
12
+ # # Example: Extract to variable
13
+ #
14
+ # ```ruby
15
+ # # Before:
16
+ # 1 + 1 # Select the text and use Refactor: Extract Variable
17
+ #
18
+ # # After:
19
+ # new_variable = 1 + 1
20
+ # new_variable
21
+ #
22
+ # ```
23
+ #
24
+ class CodeActionResolve < BaseRequest
25
+ extend T::Sig
26
+ NEW_VARIABLE_NAME = "new_variable"
27
+
28
+ class CodeActionError < StandardError; end
29
+
30
+ class Error < ::T::Enum
31
+ enums do
32
+ EmptySelection = new
33
+ InvalidTargetRange = new
34
+ end
35
+ end
36
+
37
+ sig { params(document: Document, code_action: T::Hash[Symbol, T.untyped]).void }
38
+ def initialize(document, code_action)
39
+ super(document)
40
+
41
+ @code_action = code_action
42
+ end
43
+
44
+ sig { override.returns(T.any(Interface::CodeAction, Error)) }
45
+ def run
46
+ source_range = @code_action.dig(:data, :range)
47
+ return Error::EmptySelection if source_range[:start] == source_range[:end]
48
+
49
+ return Error::InvalidTargetRange if @document.syntax_error?
50
+
51
+ scanner = @document.create_scanner
52
+ start_index = scanner.find_char_position(source_range[:start])
53
+ end_index = scanner.find_char_position(source_range[:end])
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
87
+
88
+ Interface::CodeAction.new(
89
+ title: "Refactor: Extract Variable",
90
+ edit: Interface::WorkspaceEdit.new(
91
+ document_changes: [
92
+ Interface::TextDocumentEdit.new(
93
+ text_document: Interface::OptionalVersionedTextDocumentIdentifier.new(
94
+ uri: @code_action.dig(:data, :uri),
95
+ version: nil,
96
+ ),
97
+ edits: [
98
+ create_text_edit(source_range, NEW_VARIABLE_NAME),
99
+ create_text_edit(target_range, variable_source),
100
+ ],
101
+ ),
102
+ ],
103
+ ),
104
+ )
105
+ end
106
+
107
+ private
108
+
109
+ sig { params(range: Document::RangeShape, new_text: String).returns(Interface::TextEdit) }
110
+ def create_text_edit(range, new_text)
111
+ Interface::TextEdit.new(
112
+ range: Interface::Range.new(
113
+ start: Interface::Position.new(line: range.dig(:start, :line), character: range.dig(:start, :character)),
114
+ end: Interface::Position.new(line: range.dig(:end, :line), character: range.dig(:end, :character)),
115
+ ),
116
+ new_text: new_text,
117
+ )
118
+ end
119
+ end
120
+ end
121
+ end
@@ -23,7 +23,7 @@ module RubyLsp
23
23
  params(
24
24
  uri: String,
25
25
  document: Document,
26
- range: T::Range[Integer],
26
+ range: Document::RangeShape,
27
27
  context: T::Hash[Symbol, T.untyped],
28
28
  ).void
29
29
  end
@@ -38,9 +38,8 @@ module RubyLsp
38
38
  sig { override.returns(T.nilable(T.all(T::Array[Interface::CodeAction], Object))) }
39
39
  def run
40
40
  diagnostics = @context[:diagnostics]
41
- return if diagnostics.nil? || diagnostics.empty?
42
41
 
43
- diagnostics.filter_map do |diagnostic|
42
+ code_actions = diagnostics.filter_map do |diagnostic|
44
43
  code_action = diagnostic.dig(:data, :code_action)
45
44
  next if code_action.nil?
46
45
 
@@ -49,13 +48,30 @@ module RubyLsp
49
48
  range = code_action.dig(:edit, :documentChanges, 0, :edits, 0, :range)
50
49
  code_action if diagnostic.dig(:data, :correctable) && cover?(range)
51
50
  end
51
+
52
+ code_actions << refactor_code_action(@range, @uri)
52
53
  end
53
54
 
54
55
  private
55
56
 
56
57
  sig { params(range: T.nilable(Document::RangeShape)).returns(T::Boolean) }
57
58
  def cover?(range)
58
- range.nil? || @range.cover?(range.dig(:start, :line)..range.dig(:end, :line))
59
+ range.nil? ||
60
+ ((@range.dig(:start, :line))..(@range.dig(:end, :line))).cover?(
61
+ (range.dig(:start, :line))..(range.dig(:end, :line)),
62
+ )
63
+ end
64
+
65
+ sig { params(range: Document::RangeShape, uri: String).returns(Interface::CodeAction) }
66
+ def refactor_code_action(range, uri)
67
+ Interface::CodeAction.new(
68
+ title: "Refactor: Extract Variable",
69
+ kind: Constant::CodeActionKind::REFACTOR_EXTRACT,
70
+ data: {
71
+ range: range,
72
+ uri: uri,
73
+ },
74
+ )
59
75
  end
60
76
  end
61
77
  end
@@ -34,7 +34,7 @@ module RubyLsp
34
34
  return unless defined?(Support::RuboCopDiagnosticsRunner)
35
35
 
36
36
  # Don't try to run RuboCop diagnostics for files outside the current working directory
37
- return unless @uri.sub("file://", "").start_with?(Dir.pwd)
37
+ return unless @uri.start_with?(WORKSPACE_URI)
38
38
 
39
39
  Support::RuboCopDiagnosticsRunner.instance.run(@uri, @document)
40
40
  end
@@ -108,7 +108,7 @@ module RubyLsp
108
108
 
109
109
  sig { override.params(node: SyntaxTree::Command).void }
110
110
  def visit_command(node)
111
- return unless ATTR_ACCESSORS.include?(node.message.value)
111
+ return visit(node.arguments) unless ATTR_ACCESSORS.include?(node.message.value)
112
112
 
113
113
  node.arguments.parts.each do |argument|
114
114
  next unless argument.is_a?(SyntaxTree::SymbolLiteral)
@@ -24,7 +24,6 @@ module RubyLsp
24
24
  SyntaxTree::BlockNode,
25
25
  SyntaxTree::Case,
26
26
  SyntaxTree::ClassDeclaration,
27
- SyntaxTree::Command,
28
27
  SyntaxTree::For,
29
28
  SyntaxTree::HashLiteral,
30
29
  SyntaxTree::Heredoc,
@@ -100,6 +99,11 @@ module RubyLsp
100
99
  add_call_range(node)
101
100
  return
102
101
  end
102
+ when SyntaxTree::Command
103
+ unless same_lines_for_command_and_block?(node)
104
+ location = node.location
105
+ add_lines_range(location.start_line, location.end_line - 1)
106
+ end
103
107
  when SyntaxTree::DefNode
104
108
  add_def_range(node)
105
109
  when SyntaxTree::StringConcat
@@ -110,6 +114,17 @@ module RubyLsp
110
114
  super
111
115
  end
112
116
 
117
+ # This is to prevent duplicate ranges
118
+ sig { params(node: T.any(SyntaxTree::Command, SyntaxTree::CommandCall)).returns(T::Boolean) }
119
+ def same_lines_for_command_and_block?(node)
120
+ node_block = node.block
121
+ return false unless node_block
122
+
123
+ location = node.location
124
+ block_location = node_block.location
125
+ block_location.start_line == location.start_line && block_location.end_line == location.end_line
126
+ end
127
+
113
128
  class PartialRange
114
129
  extend T::Sig
115
130
 
@@ -223,9 +238,17 @@ module RubyLsp
223
238
  end
224
239
  end
225
240
 
226
- add_lines_range(receiver.location.start_line, node.location.end_line - 1) if receiver
241
+ if receiver
242
+ unless node.is_a?(SyntaxTree::CommandCall) && same_lines_for_command_and_block?(node)
243
+ add_lines_range(
244
+ receiver.location.start_line,
245
+ node.location.end_line - 1,
246
+ )
247
+ end
248
+ end
227
249
 
228
250
  visit(node.arguments)
251
+ visit(node.block) if node.is_a?(SyntaxTree::CommandCall)
229
252
  end
230
253
 
231
254
  sig { params(node: SyntaxTree::DefNode).void }
@@ -22,14 +22,23 @@ module RubyLsp
22
22
  # ```
23
23
  class Formatting < BaseRequest
24
24
  class Error < StandardError; end
25
+ class InvalidFormatter < StandardError; end
25
26
 
26
27
  extend T::Sig
27
28
 
28
- sig { params(uri: String, document: Document).void }
29
- def initialize(uri, document)
29
+ sig { params(uri: String, document: Document, formatter: String).void }
30
+ def initialize(uri, document, formatter: "auto")
30
31
  super(document)
31
32
 
32
33
  @uri = uri
34
+ @formatter = T.let(
35
+ if formatter == "auto"
36
+ defined?(Support::RuboCopFormattingRunner) ? "rubocop" : "syntax_tree"
37
+ else
38
+ formatter
39
+ end,
40
+ String,
41
+ )
33
42
  end
34
43
 
35
44
  sig { override.returns(T.nilable(T.all(T::Array[Interface::TextEdit], Object))) }
@@ -37,6 +46,8 @@ module RubyLsp
37
46
  # Don't try to format files outside the current working directory
38
47
  return unless @uri.sub("file://", "").start_with?(Dir.pwd)
39
48
 
49
+ return if @document.syntax_error?
50
+
40
51
  formatted_text = formatted_file
41
52
  return unless formatted_text
42
53
 
@@ -58,10 +69,13 @@ module RubyLsp
58
69
 
59
70
  sig { returns(T.nilable(String)) }
60
71
  def formatted_file
61
- if defined?(Support::RuboCopFormattingRunner)
72
+ case @formatter
73
+ when "rubocop"
62
74
  Support::RuboCopFormattingRunner.instance.run(@uri, @document)
63
- else
75
+ when "syntax_tree"
64
76
  SyntaxTree.format(@document.source)
77
+ else
78
+ raise InvalidFormatter, "Unknown formatter: #{@formatter}"
65
79
  end
66
80
  end
67
81
  end
@@ -21,17 +21,20 @@ module RubyLsp
21
21
  super(document)
22
22
 
23
23
  @tree = T.let(Support::PrefixTree.new(collect_load_path_files), Support::PrefixTree)
24
-
25
- char_position = document.create_scanner.find_char_position(position)
26
- @target = T.let(find(char_position), T.nilable(SyntaxTree::TStringContent))
24
+ @position = position
27
25
  end
28
26
 
29
- sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::CompletionItem], Object)) }
27
+ sig { override.returns(T.all(T::Array[Interface::CompletionItem], Object)) }
30
28
  def run
31
- # no @target means the we are not inside a `require` call
32
- return [] unless @target
29
+ # We can't verify if we're inside a require when there are syntax errors
30
+ return [] if @document.syntax_error?
31
+
32
+ char_position = @document.create_scanner.find_char_position(@position)
33
+ target = T.let(find(char_position), T.nilable(SyntaxTree::TStringContent))
34
+ # no target means the we are not inside a `require` call
35
+ return [] unless target
33
36
 
34
- text = @target.value
37
+ text = target.value
35
38
  @tree.search(text).sort.map! do |path|
36
39
  build_completion(path, path.delete_prefix(text))
37
40
  end
@@ -73,14 +76,18 @@ module RubyLsp
73
76
  end
74
77
  end
75
78
 
76
- sig do
77
- params(label: String, insert_text: String).returns(LanguageServer::Protocol::Interface::CompletionItem)
78
- end
79
+ sig { params(label: String, insert_text: String).returns(Interface::CompletionItem) }
79
80
  def build_completion(label, insert_text)
80
- LanguageServer::Protocol::Interface::CompletionItem.new(
81
+ Interface::CompletionItem.new(
81
82
  label: label,
82
- insert_text: insert_text,
83
- kind: LanguageServer::Protocol::Constant::CompletionItemKind::REFERENCE,
83
+ text_edit: Interface::TextEdit.new(
84
+ range: Interface::Range.new(
85
+ start: @position,
86
+ end: @position,
87
+ ),
88
+ new_text: insert_text,
89
+ ),
90
+ kind: Constant::CompletionItemKind::REFERENCE,
84
91
  )
85
92
  end
86
93
  end
@@ -160,6 +160,7 @@ module RubyLsp
160
160
  add_token(node.message.location, :method)
161
161
  end
162
162
  visit(node.arguments)
163
+ visit(node.block)
163
164
  end
164
165
 
165
166
  sig { override.params(node: SyntaxTree::CommandCall).void }
@@ -169,6 +170,7 @@ module RubyLsp
169
170
  visit(node.receiver)
170
171
  add_token(node.message.location, :method)
171
172
  visit(node.arguments)
173
+ visit(node.block)
172
174
  end
173
175
 
174
176
  sig { override.params(node: SyntaxTree::Const).void }
@@ -236,13 +238,12 @@ module RubyLsp
236
238
 
237
239
  value = node.value
238
240
 
239
- case value
240
- when SyntaxTree::Ident
241
+ if value.is_a?(SyntaxTree::Ident)
241
242
  type = type_for_local(value)
242
243
  add_token(value.location, type)
243
- else
244
- visit(value)
245
244
  end
245
+
246
+ super
246
247
  end
247
248
 
248
249
  sig { override.params(node: SyntaxTree::VarRef).void }
@@ -260,16 +261,65 @@ module RubyLsp
260
261
  end
261
262
  end
262
263
 
264
+ # All block locals are variables. E.g.: [].each do |x; block_local|
265
+ sig { override.params(node: SyntaxTree::BlockVar).void }
266
+ def visit_block_var(node)
267
+ node.locals.each { |local| add_token(local.location, :variable) }
268
+ super
269
+ end
270
+
271
+ # All lambda locals are variables. E.g.: ->(x; lambda_local) {}
272
+ sig { override.params(node: SyntaxTree::LambdaVar).void }
273
+ def visit_lambda_var(node)
274
+ node.locals.each { |local| add_token(local.location, :variable) }
275
+ super
276
+ end
277
+
263
278
  sig { override.params(node: SyntaxTree::VCall).void }
264
279
  def visit_vcall(node)
265
280
  return super unless visible?(node, @range)
266
281
 
267
- return if special_method?(node.value.value)
282
+ # A VCall may exist as a local in the current_scope. This happens when used named capture groups in a regexp
283
+ ident = node.value
284
+ value = ident.value
285
+ local = current_scope.find_local(value)
286
+ return if local.nil? && special_method?(value)
287
+
288
+ type = if local
289
+ :variable
290
+ elsif Support::Sorbet.annotation?(node)
291
+ :type
292
+ else
293
+ :method
294
+ end
268
295
 
269
- type = Support::Sorbet.annotation?(node) ? :type : :method
270
296
  add_token(node.value.location, type)
271
297
  end
272
298
 
299
+ sig { override.params(node: SyntaxTree::Binary).void }
300
+ def visit_binary(node)
301
+ # It's important to visit the regexp first in the WithScope module
302
+ super
303
+
304
+ # You can only capture local variables with regexp by using the =~ operator
305
+ return unless node.operator == :=~
306
+
307
+ left = node.left
308
+ parts = left.parts
309
+
310
+ if left.is_a?(SyntaxTree::RegexpLiteral) && parts.one? && parts.first.is_a?(SyntaxTree::TStringContent)
311
+ content = parts.first
312
+
313
+ # For each capture name we find in the regexp, look for a local in the current_scope
314
+ Regexp.new(content.value, Regexp::FIXEDENCODING).names.each do |name|
315
+ local = current_scope.find_local(name)
316
+ next unless local
317
+
318
+ local.definitions.each { |definition| add_token(definition, :variable) }
319
+ end
320
+ end
321
+ end
322
+
273
323
  sig { override.params(node: SyntaxTree::ClassDeclaration).void }
274
324
  def visit_class(node)
275
325
  return super unless visible?(node, @range)
@@ -14,6 +14,7 @@ module RubyLsp
14
14
  # - {RubyLsp::Requests::OnTypeFormatting}
15
15
  # - {RubyLsp::Requests::Diagnostics}
16
16
  # - {RubyLsp::Requests::CodeActions}
17
+ # - {RubyLsp::Requests::CodeActionResolve}
17
18
  # - {RubyLsp::Requests::DocumentHighlight}
18
19
  # - {RubyLsp::Requests::InlayHints}
19
20
  # - {RubyLsp::Requests::PathCompletion}
@@ -30,6 +31,7 @@ module RubyLsp
30
31
  autoload :OnTypeFormatting, "ruby_lsp/requests/on_type_formatting"
31
32
  autoload :Diagnostics, "ruby_lsp/requests/diagnostics"
32
33
  autoload :CodeActions, "ruby_lsp/requests/code_actions"
34
+ autoload :CodeActionResolve, "ruby_lsp/requests/code_action_resolve"
33
35
  autoload :DocumentHighlight, "ruby_lsp/requests/document_highlight"
34
36
  autoload :InlayHints, "ruby_lsp/requests/inlay_hints"
35
37
  autoload :PathCompletion, "ruby_lsp/requests/path_completion"
@@ -33,7 +33,7 @@ module RubyLsp
33
33
  # fall under the else branch which just pushes requests to the queue
34
34
  @reader.read do |request|
35
35
  case request[:method]
36
- when "initialize", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
36
+ when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
37
37
  result = Executor.new(@store).execute(request)
38
38
  finalize_request(result, request)
39
39
  when "$/cancelRequest"
@@ -56,6 +56,7 @@ module RubyLsp
56
56
  # We return zero if shutdown has already been received or one otherwise as per the recommendation in the spec
57
57
  # https://microsoft.github.io/language-server-protocol/specification/#exit
58
58
  status = @store.empty? ? 0 : 1
59
+ warn("Shutdown complete with status #{status}")
59
60
  exit(status)
60
61
  else
61
62
  # Default case: push the request to the queue to be executed by the worker
@@ -12,10 +12,14 @@ module RubyLsp
12
12
  sig { params(encoding: String).void }
13
13
  attr_writer :encoding
14
14
 
15
+ sig { returns(String) }
16
+ attr_accessor :formatter
17
+
15
18
  sig { void }
16
19
  def initialize
17
20
  @state = T.let({}, T::Hash[String, Document])
18
21
  @encoding = T.let("utf-8", String)
22
+ @formatter = T.let("auto", String)
19
23
  end
20
24
 
21
25
  sig { params(uri: String).returns(Document) }
@@ -5,6 +5,9 @@ module RubyLsp
5
5
  # Used to indicate that a request shouldn't return a response
6
6
  VOID = T.let(Object.new.freeze, Object)
7
7
 
8
+ # This freeze is not redundant since the interpolated string is mutable
9
+ WORKSPACE_URI = T.let("file://#{Dir.pwd}".freeze, String) # rubocop:disable Style/RedundantFreeze
10
+
8
11
  # A notification to be sent to the client
9
12
  class Notification
10
13
  extend T::Sig
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-15 00:00:00.000000000 Z
11
+ date: 2023-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -44,7 +44,7 @@ dependencies:
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '6'
47
+ version: 6.0.2
48
48
  - - "<"
49
49
  - !ruby/object:Gem::Version
50
50
  version: '7'
@@ -54,7 +54,7 @@ dependencies:
54
54
  requirements:
55
55
  - - ">="
56
56
  - !ruby/object:Gem::Version
57
- version: '6'
57
+ version: 6.0.2
58
58
  - - "<"
59
59
  - !ruby/object:Gem::Version
60
60
  version: '7'
@@ -76,6 +76,7 @@ files:
76
76
  - lib/ruby_lsp/internal.rb
77
77
  - lib/ruby_lsp/requests.rb
78
78
  - lib/ruby_lsp/requests/base_request.rb
79
+ - lib/ruby_lsp/requests/code_action_resolve.rb
79
80
  - lib/ruby_lsp/requests/code_actions.rb
80
81
  - lib/ruby_lsp/requests/diagnostics.rb
81
82
  - lib/ruby_lsp/requests/document_highlight.rb