ruby-lsp 0.18.4 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -40,6 +40,8 @@ module RubyLsp
40
40
  # For example, forgetting to return the `insertText` included in the original item will make the editor use the
41
41
  # `label` for the text edit instead
42
42
  label = @item[:label].dup
43
+ return keyword_resolve(@item) if @item.dig(:data, :keyword)
44
+
43
45
  entries = @index[label] || []
44
46
 
45
47
  owner_name = @item.dig(:data, :owner_name)
@@ -72,6 +74,33 @@ module RubyLsp
72
74
 
73
75
  @item
74
76
  end
77
+
78
+ private
79
+
80
+ sig { params(item: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
81
+ def keyword_resolve(item)
82
+ keyword = item[:label]
83
+ content = KEYWORD_DOCS[keyword]
84
+
85
+ if content
86
+ doc_path = File.join(STATIC_DOCS_PATH, "#{keyword}.md")
87
+
88
+ @item[:documentation] = Interface::MarkupContent.new(
89
+ kind: "markdown",
90
+ value: <<~MARKDOWN.chomp,
91
+ ```ruby
92
+ #{keyword}
93
+ ```
94
+
95
+ [Read more](#{doc_path})
96
+
97
+ #{content}
98
+ MARKDOWN
99
+ )
100
+ end
101
+
102
+ item
103
+ end
75
104
  end
76
105
  end
77
106
  end
@@ -0,0 +1,189 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # The
7
+ # [rename](https://microsoft.github.io/language-server-protocol/specification#textDocument_rename)
8
+ # request renames all instances of a symbol in a document.
9
+ class Rename < Request
10
+ extend T::Sig
11
+ include Support::Common
12
+
13
+ class InvalidNameError < StandardError; end
14
+
15
+ sig do
16
+ params(
17
+ global_state: GlobalState,
18
+ store: Store,
19
+ document: T.any(RubyDocument, ERBDocument),
20
+ params: T::Hash[Symbol, T.untyped],
21
+ ).void
22
+ end
23
+ def initialize(global_state, store, document, params)
24
+ super()
25
+ @global_state = global_state
26
+ @store = store
27
+ @document = document
28
+ @position = T.let(params[:position], T::Hash[Symbol, Integer])
29
+ @new_name = T.let(params[:newName], String)
30
+ end
31
+
32
+ sig { override.returns(T.nilable(Interface::WorkspaceEdit)) }
33
+ def perform
34
+ char_position = @document.create_scanner.find_char_position(@position)
35
+
36
+ node_context = RubyDocument.locate(
37
+ @document.parse_result.value,
38
+ char_position,
39
+ node_types: [Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode],
40
+ )
41
+ target = node_context.node
42
+ parent = node_context.parent
43
+ return if !target || target.is_a?(Prism::ProgramNode)
44
+
45
+ if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
46
+ target = determine_target(
47
+ target,
48
+ parent,
49
+ @position,
50
+ )
51
+ end
52
+
53
+ target = T.cast(
54
+ target,
55
+ T.any(Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode),
56
+ )
57
+
58
+ name = constant_name(target)
59
+ return unless name
60
+
61
+ entries = @global_state.index.resolve(name, node_context.nesting)
62
+ return unless entries
63
+
64
+ if (conflict_entries = @global_state.index.resolve(@new_name, node_context.nesting))
65
+ raise InvalidNameError, "The new name is already in use by #{T.must(conflict_entries.first).name}"
66
+ end
67
+
68
+ fully_qualified_name = T.must(entries.first).name
69
+ changes = collect_text_edits(fully_qualified_name, name)
70
+
71
+ # If the client doesn't support resource operations, such as renaming files, then we can only return the basic
72
+ # text changes
73
+ unless @global_state.supported_resource_operations.include?("rename")
74
+ return Interface::WorkspaceEdit.new(changes: changes)
75
+ end
76
+
77
+ # Text edits must be applied before any resource operations, such as renaming files. Otherwise, the file is
78
+ # renamed and then the URI associated to the text edit no longer exists, causing it to be dropped
79
+ document_changes = changes.map do |uri, edits|
80
+ Interface::TextDocumentEdit.new(
81
+ text_document: Interface::VersionedTextDocumentIdentifier.new(uri: uri, version: nil),
82
+ edits: edits,
83
+ )
84
+ end
85
+
86
+ collect_file_renames(fully_qualified_name, document_changes)
87
+ Interface::WorkspaceEdit.new(document_changes: document_changes)
88
+ end
89
+
90
+ private
91
+
92
+ sig do
93
+ params(
94
+ fully_qualified_name: String,
95
+ document_changes: T::Array[T.any(Interface::RenameFile, Interface::TextDocumentEdit)],
96
+ ).void
97
+ end
98
+ def collect_file_renames(fully_qualified_name, document_changes)
99
+ # Check if the declarations of the symbol being renamed match the file name. In case they do, we automatically
100
+ # rename the files for the user.
101
+ #
102
+ # We also look for an associated test file and rename it too
103
+ short_name = T.must(fully_qualified_name.split("::").last)
104
+
105
+ T.must(@global_state.index[fully_qualified_name]).each do |entry|
106
+ # Do not rename files that are not part of the workspace
107
+ next unless entry.file_path.start_with?(@global_state.workspace_path)
108
+
109
+ case entry
110
+ when RubyIndexer::Entry::Class, RubyIndexer::Entry::Module, RubyIndexer::Entry::Constant,
111
+ RubyIndexer::Entry::ConstantAlias, RubyIndexer::Entry::UnresolvedConstantAlias
112
+
113
+ file_name = file_from_constant_name(short_name)
114
+
115
+ if "#{file_name}.rb" == entry.file_name
116
+ new_file_name = file_from_constant_name(T.must(@new_name.split("::").last))
117
+
118
+ old_uri = URI::Generic.from_path(path: entry.file_path).to_s
119
+ new_uri = URI::Generic.from_path(path: File.join(
120
+ File.dirname(entry.file_path),
121
+ "#{new_file_name}.rb",
122
+ )).to_s
123
+
124
+ document_changes << Interface::RenameFile.new(kind: "rename", old_uri: old_uri, new_uri: new_uri)
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ sig { params(fully_qualified_name: String, name: String).returns(T::Hash[String, T::Array[Interface::TextEdit]]) }
131
+ def collect_text_edits(fully_qualified_name, name)
132
+ changes = {}
133
+
134
+ Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
135
+ uri = URI::Generic.from_path(path: path)
136
+ # If the document is being managed by the client, then we should use whatever is present in the store instead
137
+ # of reading from disk
138
+ next if @store.key?(uri)
139
+
140
+ parse_result = Prism.parse_file(path)
141
+ edits = collect_changes(fully_qualified_name, parse_result, name, uri)
142
+ changes[uri.to_s] = edits unless edits.empty?
143
+ end
144
+
145
+ @store.each do |uri, document|
146
+ edits = collect_changes(fully_qualified_name, document.parse_result, name, document.uri)
147
+ changes[uri] = edits unless edits.empty?
148
+ end
149
+
150
+ changes
151
+ end
152
+
153
+ sig do
154
+ params(
155
+ fully_qualified_name: String,
156
+ parse_result: Prism::ParseResult,
157
+ name: String,
158
+ uri: URI::Generic,
159
+ ).returns(T::Array[Interface::TextEdit])
160
+ end
161
+ def collect_changes(fully_qualified_name, parse_result, name, uri)
162
+ dispatcher = Prism::Dispatcher.new
163
+ finder = RubyIndexer::ReferenceFinder.new(fully_qualified_name, @global_state.index, dispatcher)
164
+ dispatcher.visit(parse_result.value)
165
+
166
+ finder.references.uniq(&:location).map do |reference|
167
+ adjust_reference_for_edit(name, reference)
168
+ end
169
+ end
170
+
171
+ sig { params(name: String, reference: RubyIndexer::ReferenceFinder::Reference).returns(Interface::TextEdit) }
172
+ def adjust_reference_for_edit(name, reference)
173
+ # The reference may include a namespace in front. We need to check if the rename new name includes namespaces
174
+ # and then adjust both the text and the location to produce the correct edit
175
+ location = reference.location
176
+ new_text = reference.name.sub(name, @new_name)
177
+
178
+ Interface::TextEdit.new(range: range_from_location(location), new_text: new_text)
179
+ end
180
+
181
+ sig { params(constant_name: String).returns(String) }
182
+ def file_from_constant_name(constant_name)
183
+ constant_name
184
+ .gsub(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/, '\1\3_\2\4')
185
+ .downcase
186
+ end
187
+ end
188
+ end
189
+ end
@@ -19,6 +19,13 @@ module URI
19
19
  T::Array[Symbol],
20
20
  )
21
21
 
22
+ # `uri` for Ruby 3.4 switched the default parser from RFC2396 to RFC3986. The new parser emits a deprecation
23
+ # warning on a few methods and delegates them to RFC2396, namely `extract`/`make_regexp`/`escape`/`unescape`.
24
+ # On earlier versions of the uri gem, the RFC2396_PARSER constant doesn't exist, so it needs some special
25
+ # handling to select a parser that doesn't emit deprecations. While it was backported to Ruby 3.1, users may
26
+ # have the uri gem in their own bundle and thus not use a compatible version.
27
+ PARSER = T.let(const_defined?(:RFC2396_PARSER) ? RFC2396_PARSER : DEFAULT_PARSER, RFC2396_Parser)
28
+
22
29
  T.unsafe(self).alias_method(:gem_name, :host)
23
30
  T.unsafe(self).alias_method(:line_number, :fragment)
24
31
 
@@ -41,7 +48,7 @@ module URI
41
48
  {
42
49
  scheme: "source",
43
50
  host: gem_name,
44
- path: DEFAULT_PARSER.escape("/#{gem_version}/#{path}"),
51
+ path: PARSER.escape("/#{gem_version}/#{path}"),
45
52
  fragment: line_number,
46
53
  }
47
54
  )
@@ -67,6 +67,8 @@ module RubyLsp
67
67
  text_document_definition(message)
68
68
  when "textDocument/prepareTypeHierarchy"
69
69
  text_document_prepare_type_hierarchy(message)
70
+ when "textDocument/rename"
71
+ text_document_rename(message)
70
72
  when "typeHierarchy/supertypes"
71
73
  type_hierarchy_supertypes(message)
72
74
  when "typeHierarchy/subtypes"
@@ -123,9 +125,9 @@ module RubyLsp
123
125
  send_log_message("Error processing #{message[:method]}: #{e.full_message}", type: Constant::MessageType::ERROR)
124
126
  end
125
127
 
126
- sig { void }
127
- def load_addons
128
- errors = Addon.load_addons(@global_state, @outgoing_queue)
128
+ sig { params(include_project_addons: T::Boolean).void }
129
+ def load_addons(include_project_addons: true)
130
+ errors = Addon.load_addons(@global_state, @outgoing_queue, include_project_addons: include_project_addons)
129
131
 
130
132
  if errors.any?
131
133
  send_log_message(
@@ -227,6 +229,7 @@ module RubyLsp
227
229
  workspace_symbol_provider: enabled_features["workspaceSymbol"] && !@global_state.has_type_checker,
228
230
  signature_help_provider: signature_help_provider,
229
231
  type_hierarchy_provider: type_hierarchy_provider,
232
+ rename_provider: !@global_state.has_type_checker,
230
233
  experimental: {
231
234
  addon_detection: true,
232
235
  },
@@ -320,14 +323,18 @@ module RubyLsp
320
323
  language_id: language_id,
321
324
  )
322
325
 
323
- if document.past_expensive_limit?
326
+ if document.past_expensive_limit? && text_document[:uri].scheme == "file"
327
+ log_message = <<~MESSAGE
328
+ The file #{text_document[:uri].path} is too long. For performance reasons, semantic highlighting and
329
+ diagnostics will be disabled.
330
+ MESSAGE
331
+
324
332
  send_message(
325
333
  Notification.new(
326
- method: "window/showMessage",
327
- params: Interface::ShowMessageParams.new(
334
+ method: "window/logMessage",
335
+ params: Interface::LogMessageParams.new(
328
336
  type: Constant::MessageType::WARNING,
329
- message: "This file is too long. For performance reasons, semantic highlighting and " \
330
- "diagnostics will be disabled",
337
+ message: log_message,
331
338
  ),
332
339
  ),
333
340
  )
@@ -609,6 +616,26 @@ module RubyLsp
609
616
  )
610
617
  end
611
618
 
619
+ sig { params(message: T::Hash[Symbol, T.untyped]).void }
620
+ def text_document_rename(message)
621
+ params = message[:params]
622
+ document = @store.get(params.dig(:textDocument, :uri))
623
+
624
+ unless document.is_a?(RubyDocument)
625
+ send_empty_response(message[:id])
626
+ return
627
+ end
628
+
629
+ send_message(
630
+ Result.new(
631
+ id: message[:id],
632
+ response: Requests::Rename.new(@global_state, @store, document, params).perform,
633
+ ),
634
+ )
635
+ rescue Requests::Rename::InvalidNameError => e
636
+ send_message(Error.new(id: message[:id], code: Constant::ErrorCodes::REQUEST_FAILED, message: e.message))
637
+ end
638
+
612
639
  sig { params(document: Document[T.untyped]).returns(RubyDocument::SorbetLevel) }
613
640
  def sorbet_level(document)
614
641
  return RubyDocument::SorbetLevel::Ignore unless @global_state.has_type_checker
@@ -0,0 +1,15 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ # The path to the `static_docs` directory, where we keep long-form static documentation
6
+ STATIC_DOCS_PATH = T.let(File.join(File.dirname(File.dirname(T.must(__dir__))), "static_docs"), String)
7
+
8
+ # A map of keyword => short documentation to be displayed on hover or completion
9
+ KEYWORD_DOCS = T.let(
10
+ {
11
+ "yield" => "Invokes the passed block with the given arguments",
12
+ }.freeze,
13
+ T::Hash[String, String],
14
+ )
15
+ end
@@ -99,6 +99,18 @@ module RubyLsp
99
99
  @state.delete(uri.to_s)
100
100
  end
101
101
 
102
+ sig { params(uri: URI::Generic).returns(T::Boolean) }
103
+ def key?(uri)
104
+ @state.key?(uri.to_s)
105
+ end
106
+
107
+ sig { params(block: T.proc.params(uri: String, document: Document[T.untyped]).void).void }
108
+ def each(&block)
109
+ @state.each do |uri, document|
110
+ block.call(uri, document)
111
+ end
112
+ end
113
+
102
114
  sig do
103
115
  type_parameters(:T)
104
116
  .params(
@@ -42,7 +42,7 @@ module RubyLsp
42
42
  RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)),
43
43
  source,
44
44
  )
45
- server.load_addons if load_addons
45
+ server.load_addons(include_project_addons: false) if load_addons
46
46
  block.call(server, uri)
47
47
  ensure
48
48
  if load_addons
@@ -89,7 +89,12 @@ module RubyLsp
89
89
 
90
90
  Type.new("#{parts.join("::")}::#{last}::<Class:#{last}>")
91
91
  else
92
- raw_receiver = node.receiver&.slice
92
+
93
+ raw_receiver = if receiver.is_a?(Prism::CallNode)
94
+ receiver.message
95
+ else
96
+ receiver&.slice
97
+ end
93
98
 
94
99
  if raw_receiver
95
100
  guessed_name = raw_receiver
@@ -0,0 +1,81 @@
1
+ # Yield
2
+
3
+ In Ruby, every method implicitly accepts a block, even when not included in the parameters list.
4
+
5
+ ```ruby
6
+ def foo
7
+ end
8
+
9
+ foo { 123 } # works!
10
+ ```
11
+
12
+ The `yield` keyword is used to invoke the block that was passed with arguments.
13
+
14
+ ```ruby
15
+ # Consider this method call. The block being passed to the method `foo` accepts an argument called `a`.
16
+ # It then takes whatever argument was passed and multiplies it by 2
17
+ foo do |a|
18
+ a * 2
19
+ end
20
+
21
+ # In the `foo` method declaration, we can use `yield` to invoke the block that was passed and provide the block
22
+ # with the value for the `a` argument
23
+ def foo
24
+ # Invoke the block passed to `foo` with the number 10 as the argument `a`
25
+ result = yield(10)
26
+ puts result # Will print 20
27
+ end
28
+ ```
29
+
30
+ If `yield` is used to invoke the block, but no block was passed, that will result in a local jump error.
31
+
32
+ ```ruby
33
+ # If we invoke `foo` without a block, trying to `yield` will fail
34
+ foo
35
+
36
+ # `foo': no block given (yield) (LocalJumpError)
37
+ ```
38
+
39
+ We can decide to use `yield` conditionally by using Ruby's `block_given?` method, which will return `true` if a block
40
+ was passed to the method.
41
+
42
+ ```ruby
43
+ def foo
44
+ # If a block is passed when invoking `foo`, call the block with argument 10 and print the result.
45
+ # Otherwise, just print that no block was passed
46
+ if block_given?
47
+ result = yield(10)
48
+ puts result
49
+ else
50
+ puts "No block passed!"
51
+ end
52
+ end
53
+
54
+ foo do |a|
55
+ a * 2
56
+ end
57
+ # => 20
58
+
59
+ foo
60
+ # => No block passed!
61
+ ```
62
+
63
+ ## Block parameter
64
+
65
+ In addition to implicit blocks, Ruby also allows developers to use explicit block parameters as part of the method's
66
+ signature. In this scenario, we can use the reference to the block directly instead of relying on the `yield` keyword.
67
+
68
+ ```ruby
69
+ # Block parameters are prefixed with & and a name
70
+ def foo(&my_block_param)
71
+ # If a block was passed to `foo`, `my_block_param` will be a `Proc` object. Otherwise, it will be `nil`. We can use
72
+ # that to check for its presence
73
+ if my_block_param
74
+ # Explicit block parameters are invoked using the method `call`, which is present in all `Proc` objects
75
+ result = my_block_param.call(10)
76
+ puts result
77
+ else
78
+ puts "No block passed!"
79
+ end
80
+ end
81
+ ```
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.18.4
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-30 00:00:00.000000000 Z
11
+ date: 2024-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -28,16 +28,22 @@ dependencies:
28
28
  name: prism
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.1'
34
+ - - "<"
32
35
  - !ruby/object:Gem::Version
33
- version: '1.0'
36
+ version: '2.0'
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
- - - "~>"
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '1.1'
44
+ - - "<"
39
45
  - !ruby/object:Gem::Version
40
- version: '1.0'
46
+ version: '2.0'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: rbs
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -99,6 +105,7 @@ files:
99
105
  - lib/ruby_indexer/lib/ruby_indexer/location.rb
100
106
  - lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb
101
107
  - lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb
108
+ - lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb
102
109
  - lib/ruby_indexer/ruby_indexer.rb
103
110
  - lib/ruby_indexer/test/classes_and_modules_test.rb
104
111
  - lib/ruby_indexer/test/configuration_test.rb
@@ -109,6 +116,7 @@ files:
109
116
  - lib/ruby_indexer/test/method_test.rb
110
117
  - lib/ruby_indexer/test/prefix_tree_test.rb
111
118
  - lib/ruby_indexer/test/rbs_indexer_test.rb
119
+ - lib/ruby_indexer/test/reference_finder_test.rb
112
120
  - lib/ruby_indexer/test/test_case.rb
113
121
  - lib/ruby_lsp/addon.rb
114
122
  - lib/ruby_lsp/base_server.rb
@@ -146,6 +154,7 @@ files:
146
154
  - lib/ruby_lsp/requests/inlay_hints.rb
147
155
  - lib/ruby_lsp/requests/on_type_formatting.rb
148
156
  - lib/ruby_lsp/requests/prepare_type_hierarchy.rb
157
+ - lib/ruby_lsp/requests/rename.rb
149
158
  - lib/ruby_lsp/requests/request.rb
150
159
  - lib/ruby_lsp/requests/selection_ranges.rb
151
160
  - lib/ruby_lsp/requests/semantic_highlighting.rb
@@ -173,10 +182,12 @@ files:
173
182
  - lib/ruby_lsp/scope.rb
174
183
  - lib/ruby_lsp/server.rb
175
184
  - lib/ruby_lsp/setup_bundler.rb
185
+ - lib/ruby_lsp/static_docs.rb
176
186
  - lib/ruby_lsp/store.rb
177
187
  - lib/ruby_lsp/test_helper.rb
178
188
  - lib/ruby_lsp/type_inferrer.rb
179
189
  - lib/ruby_lsp/utils.rb
190
+ - static_docs/yield.md
180
191
  homepage: https://github.com/Shopify/ruby-lsp
181
192
  licenses:
182
193
  - MIT
@@ -198,7 +209,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
198
209
  - !ruby/object:Gem::Version
199
210
  version: '0'
200
211
  requirements: []
201
- rubygems_version: 3.5.18
212
+ rubygems_version: 3.5.20
202
213
  signing_key:
203
214
  specification_version: 4
204
215
  summary: An opinionated language server for Ruby