ruby-lsp 0.4.0 → 0.4.2

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