ruby-lsp 0.18.4 → 0.19.1
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 +4 -4
- data/VERSION +1 -1
- data/exe/ruby-lsp-check +1 -1
- data/lib/core_ext/uri.rb +9 -4
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +6 -0
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +66 -8
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +63 -32
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +8 -5
- data/lib/ruby_indexer/lib/ruby_indexer/rbs_indexer.rb +38 -4
- data/lib/ruby_indexer/lib/ruby_indexer/reference_finder.rb +324 -0
- data/lib/ruby_indexer/ruby_indexer.rb +1 -0
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +23 -0
- data/lib/ruby_indexer/test/constant_test.rb +8 -0
- data/lib/ruby_indexer/test/enhancements_test.rb +2 -0
- data/lib/ruby_indexer/test/index_test.rb +3 -0
- data/lib/ruby_indexer/test/instance_variables_test.rb +12 -0
- data/lib/ruby_indexer/test/method_test.rb +10 -0
- data/lib/ruby_indexer/test/rbs_indexer_test.rb +14 -0
- data/lib/ruby_indexer/test/reference_finder_test.rb +242 -0
- data/lib/ruby_lsp/addon.rb +69 -7
- data/lib/ruby_lsp/erb_document.rb +9 -3
- data/lib/ruby_lsp/global_state.rb +8 -0
- data/lib/ruby_lsp/internal.rb +4 -0
- data/lib/ruby_lsp/listeners/completion.rb +1 -1
- data/lib/ruby_lsp/listeners/hover.rb +19 -0
- data/lib/ruby_lsp/requests/code_action_resolve.rb +9 -3
- data/lib/ruby_lsp/requests/completion.rb +1 -0
- data/lib/ruby_lsp/requests/completion_resolve.rb +29 -0
- data/lib/ruby_lsp/requests/definition.rb +1 -0
- data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
- data/lib/ruby_lsp/requests/hover.rb +1 -0
- data/lib/ruby_lsp/requests/range_formatting.rb +55 -0
- data/lib/ruby_lsp/requests/references.rb +146 -0
- data/lib/ruby_lsp/requests/rename.rb +196 -0
- data/lib/ruby_lsp/requests/signature_help.rb +6 -1
- data/lib/ruby_lsp/requests/support/formatter.rb +3 -0
- data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +6 -0
- data/lib/ruby_lsp/requests/support/source_uri.rb +8 -1
- data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +8 -0
- data/lib/ruby_lsp/ruby_document.rb +23 -8
- data/lib/ruby_lsp/server.rb +98 -10
- data/lib/ruby_lsp/static_docs.rb +15 -0
- data/lib/ruby_lsp/store.rb +12 -0
- data/lib/ruby_lsp/test_helper.rb +1 -1
- data/lib/ruby_lsp/type_inferrer.rb +6 -1
- data/static_docs/yield.md +81 -0
- metadata +20 -7
@@ -0,0 +1,196 @@
|
|
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
|
+
encoding: @global_state.encoding,
|
41
|
+
)
|
42
|
+
target = node_context.node
|
43
|
+
parent = node_context.parent
|
44
|
+
return if !target || target.is_a?(Prism::ProgramNode)
|
45
|
+
|
46
|
+
if target.is_a?(Prism::ConstantReadNode) && parent.is_a?(Prism::ConstantPathNode)
|
47
|
+
target = determine_target(
|
48
|
+
target,
|
49
|
+
parent,
|
50
|
+
@position,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
target = T.cast(
|
55
|
+
target,
|
56
|
+
T.any(Prism::ConstantReadNode, Prism::ConstantPathNode, Prism::ConstantPathTargetNode),
|
57
|
+
)
|
58
|
+
|
59
|
+
name = constant_name(target)
|
60
|
+
return unless name
|
61
|
+
|
62
|
+
entries = @global_state.index.resolve(name, node_context.nesting)
|
63
|
+
return unless entries
|
64
|
+
|
65
|
+
if (conflict_entries = @global_state.index.resolve(@new_name, node_context.nesting))
|
66
|
+
raise InvalidNameError, "The new name is already in use by #{T.must(conflict_entries.first).name}"
|
67
|
+
end
|
68
|
+
|
69
|
+
fully_qualified_name = T.must(entries.first).name
|
70
|
+
reference_target = RubyIndexer::ReferenceFinder::ConstTarget.new(fully_qualified_name)
|
71
|
+
changes = collect_text_edits(reference_target, name)
|
72
|
+
|
73
|
+
# If the client doesn't support resource operations, such as renaming files, then we can only return the basic
|
74
|
+
# text changes
|
75
|
+
unless @global_state.supported_resource_operations.include?("rename")
|
76
|
+
return Interface::WorkspaceEdit.new(changes: changes)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Text edits must be applied before any resource operations, such as renaming files. Otherwise, the file is
|
80
|
+
# renamed and then the URI associated to the text edit no longer exists, causing it to be dropped
|
81
|
+
document_changes = changes.map do |uri, edits|
|
82
|
+
Interface::TextDocumentEdit.new(
|
83
|
+
text_document: Interface::VersionedTextDocumentIdentifier.new(uri: uri, version: nil),
|
84
|
+
edits: edits,
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
collect_file_renames(fully_qualified_name, document_changes)
|
89
|
+
Interface::WorkspaceEdit.new(document_changes: document_changes)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
sig do
|
95
|
+
params(
|
96
|
+
fully_qualified_name: String,
|
97
|
+
document_changes: T::Array[T.any(Interface::RenameFile, Interface::TextDocumentEdit)],
|
98
|
+
).void
|
99
|
+
end
|
100
|
+
def collect_file_renames(fully_qualified_name, document_changes)
|
101
|
+
# Check if the declarations of the symbol being renamed match the file name. In case they do, we automatically
|
102
|
+
# rename the files for the user.
|
103
|
+
#
|
104
|
+
# We also look for an associated test file and rename it too
|
105
|
+
short_name = T.must(fully_qualified_name.split("::").last)
|
106
|
+
|
107
|
+
T.must(@global_state.index[fully_qualified_name]).each do |entry|
|
108
|
+
# Do not rename files that are not part of the workspace
|
109
|
+
next unless entry.file_path.start_with?(@global_state.workspace_path)
|
110
|
+
|
111
|
+
case entry
|
112
|
+
when RubyIndexer::Entry::Class, RubyIndexer::Entry::Module, RubyIndexer::Entry::Constant,
|
113
|
+
RubyIndexer::Entry::ConstantAlias, RubyIndexer::Entry::UnresolvedConstantAlias
|
114
|
+
|
115
|
+
file_name = file_from_constant_name(short_name)
|
116
|
+
|
117
|
+
if "#{file_name}.rb" == entry.file_name
|
118
|
+
new_file_name = file_from_constant_name(T.must(@new_name.split("::").last))
|
119
|
+
|
120
|
+
old_uri = URI::Generic.from_path(path: entry.file_path).to_s
|
121
|
+
new_uri = URI::Generic.from_path(path: File.join(
|
122
|
+
File.dirname(entry.file_path),
|
123
|
+
"#{new_file_name}.rb",
|
124
|
+
)).to_s
|
125
|
+
|
126
|
+
document_changes << Interface::RenameFile.new(kind: "rename", old_uri: old_uri, new_uri: new_uri)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
sig do
|
133
|
+
params(
|
134
|
+
target: RubyIndexer::ReferenceFinder::Target,
|
135
|
+
name: String,
|
136
|
+
).returns(T::Hash[String, T::Array[Interface::TextEdit]])
|
137
|
+
end
|
138
|
+
def collect_text_edits(target, name)
|
139
|
+
changes = {}
|
140
|
+
|
141
|
+
Dir.glob(File.join(@global_state.workspace_path, "**/*.rb")).each do |path|
|
142
|
+
uri = URI::Generic.from_path(path: path)
|
143
|
+
# If the document is being managed by the client, then we should use whatever is present in the store instead
|
144
|
+
# of reading from disk
|
145
|
+
next if @store.key?(uri)
|
146
|
+
|
147
|
+
parse_result = Prism.parse_file(path)
|
148
|
+
edits = collect_changes(target, parse_result, name, uri)
|
149
|
+
changes[uri.to_s] = edits unless edits.empty?
|
150
|
+
end
|
151
|
+
|
152
|
+
@store.each do |uri, document|
|
153
|
+
edits = collect_changes(target, document.parse_result, name, document.uri)
|
154
|
+
changes[uri] = edits unless edits.empty?
|
155
|
+
end
|
156
|
+
|
157
|
+
changes
|
158
|
+
end
|
159
|
+
|
160
|
+
sig do
|
161
|
+
params(
|
162
|
+
target: RubyIndexer::ReferenceFinder::Target,
|
163
|
+
parse_result: Prism::ParseResult,
|
164
|
+
name: String,
|
165
|
+
uri: URI::Generic,
|
166
|
+
).returns(T::Array[Interface::TextEdit])
|
167
|
+
end
|
168
|
+
def collect_changes(target, parse_result, name, uri)
|
169
|
+
dispatcher = Prism::Dispatcher.new
|
170
|
+
finder = RubyIndexer::ReferenceFinder.new(target, @global_state.index, dispatcher)
|
171
|
+
dispatcher.visit(parse_result.value)
|
172
|
+
|
173
|
+
finder.references.map do |reference|
|
174
|
+
adjust_reference_for_edit(name, reference)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
sig { params(name: String, reference: RubyIndexer::ReferenceFinder::Reference).returns(Interface::TextEdit) }
|
179
|
+
def adjust_reference_for_edit(name, reference)
|
180
|
+
# The reference may include a namespace in front. We need to check if the rename new name includes namespaces
|
181
|
+
# and then adjust both the text and the location to produce the correct edit
|
182
|
+
location = reference.location
|
183
|
+
new_text = reference.name.sub(name, @new_name)
|
184
|
+
|
185
|
+
Interface::TextEdit.new(range: range_from_location(location), new_text: new_text)
|
186
|
+
end
|
187
|
+
|
188
|
+
sig { params(constant_name: String).returns(String) }
|
189
|
+
def file_from_constant_name(constant_name)
|
190
|
+
constant_name
|
191
|
+
.gsub(/([a-z])([A-Z])|([A-Z])([A-Z][a-z])/, '\1\3_\2\4')
|
192
|
+
.downcase
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -39,7 +39,12 @@ module RubyLsp
|
|
39
39
|
char_position = document.create_scanner.find_char_position(position)
|
40
40
|
delegate_request_if_needed!(global_state, document, char_position)
|
41
41
|
|
42
|
-
node_context = RubyDocument.locate(
|
42
|
+
node_context = RubyDocument.locate(
|
43
|
+
document.parse_result.value,
|
44
|
+
char_position,
|
45
|
+
node_types: [Prism::CallNode],
|
46
|
+
encoding: global_state.encoding,
|
47
|
+
)
|
43
48
|
|
44
49
|
target = adjust_for_nested_target(node_context.node, node_context.parent, position)
|
45
50
|
|
@@ -13,6 +13,9 @@ module RubyLsp
|
|
13
13
|
sig { abstract.params(uri: URI::Generic, document: RubyDocument).returns(T.nilable(String)) }
|
14
14
|
def run_formatting(uri, document); end
|
15
15
|
|
16
|
+
sig { abstract.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
|
17
|
+
def run_range_formatting(uri, source, base_indentation); end
|
18
|
+
|
16
19
|
sig do
|
17
20
|
abstract.params(
|
18
21
|
uri: URI::Generic,
|
@@ -28,6 +28,12 @@ module RubyLsp
|
|
28
28
|
@format_runner.formatted_source
|
29
29
|
end
|
30
30
|
|
31
|
+
# RuboCop does not support range formatting
|
32
|
+
sig { override.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
|
33
|
+
def run_range_formatting(uri, source, base_indentation)
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
31
37
|
sig do
|
32
38
|
override.params(
|
33
39
|
uri: URI::Generic,
|
@@ -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:
|
51
|
+
path: PARSER.escape("/#{gem_version}/#{path}"),
|
45
52
|
fragment: line_number,
|
46
53
|
}
|
47
54
|
)
|
@@ -37,6 +37,14 @@ module RubyLsp
|
|
37
37
|
SyntaxTree.format(document.source, @options.print_width, options: @options.formatter_options)
|
38
38
|
end
|
39
39
|
|
40
|
+
sig { override.params(uri: URI::Generic, source: String, base_indentation: Integer).returns(T.nilable(String)) }
|
41
|
+
def run_range_formatting(uri, source, base_indentation)
|
42
|
+
path = uri.to_standardized_path
|
43
|
+
return if path && @options.ignore_files.any? { |pattern| File.fnmatch?("*/#{pattern}", path) }
|
44
|
+
|
45
|
+
SyntaxTree.format(source, @options.print_width, base_indentation, options: @options.formatter_options)
|
46
|
+
end
|
47
|
+
|
40
48
|
sig do
|
41
49
|
override.params(
|
42
50
|
uri: URI::Generic,
|
@@ -26,9 +26,10 @@ module RubyLsp
|
|
26
26
|
node: Prism::Node,
|
27
27
|
char_position: Integer,
|
28
28
|
node_types: T::Array[T.class_of(Prism::Node)],
|
29
|
+
encoding: Encoding,
|
29
30
|
).returns(NodeContext)
|
30
31
|
end
|
31
|
-
def locate(node, char_position, node_types: [])
|
32
|
+
def locate(node, char_position, node_types: [], encoding: Encoding::UTF_8)
|
32
33
|
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
|
33
34
|
closest = node
|
34
35
|
parent = T.let(nil, T.nilable(Prism::Node))
|
@@ -61,16 +62,21 @@ module RubyLsp
|
|
61
62
|
|
62
63
|
# Skip if the current node doesn't cover the desired position
|
63
64
|
loc = candidate.location
|
64
|
-
|
65
|
+
loc_start_offset = loc.start_code_units_offset(encoding)
|
66
|
+
loc_end_offset = loc.end_code_units_offset(encoding)
|
67
|
+
next unless (loc_start_offset...loc_end_offset).cover?(char_position)
|
65
68
|
|
66
69
|
# If the node's start character is already past the position, then we should've found the closest node
|
67
70
|
# already
|
68
|
-
break if char_position <
|
71
|
+
break if char_position < loc_start_offset
|
69
72
|
|
70
73
|
# If the candidate starts after the end of the previous nesting level, then we've exited that nesting level
|
71
74
|
# and need to pop the stack
|
72
75
|
previous_level = nesting_nodes.last
|
73
|
-
|
76
|
+
if previous_level &&
|
77
|
+
(loc_start_offset > previous_level.location.end_code_units_offset(encoding))
|
78
|
+
nesting_nodes.pop
|
79
|
+
end
|
74
80
|
|
75
81
|
# Keep track of the nesting where we found the target. This is used to determine the fully qualified name of
|
76
82
|
# the target when it is a constant
|
@@ -83,8 +89,10 @@ module RubyLsp
|
|
83
89
|
if candidate.is_a?(Prism::CallNode)
|
84
90
|
arg_loc = candidate.arguments&.location
|
85
91
|
blk_loc = candidate.block&.location
|
86
|
-
if (arg_loc && (arg_loc.
|
87
|
-
|
92
|
+
if (arg_loc && (arg_loc.start_code_units_offset(encoding)...
|
93
|
+
arg_loc.end_code_units_offset(encoding)).cover?(char_position)) ||
|
94
|
+
(blk_loc && (blk_loc.start_code_units_offset(encoding)...
|
95
|
+
blk_loc.end_code_units_offset(encoding)).cover?(char_position))
|
88
96
|
call_node = candidate
|
89
97
|
end
|
90
98
|
end
|
@@ -94,7 +102,9 @@ module RubyLsp
|
|
94
102
|
|
95
103
|
# If the current node is narrower than or equal to the previous closest node, then it is more precise
|
96
104
|
closest_loc = closest.location
|
97
|
-
|
105
|
+
closest_node_start_offset = closest_loc.start_code_units_offset(encoding)
|
106
|
+
closest_node_end_offset = closest_loc.end_code_units_offset(encoding)
|
107
|
+
if loc_end_offset - loc_start_offset <= closest_node_end_offset - closest_node_start_offset
|
98
108
|
parent = closest
|
99
109
|
closest = candidate
|
100
110
|
end
|
@@ -201,7 +211,12 @@ module RubyLsp
|
|
201
211
|
).returns(NodeContext)
|
202
212
|
end
|
203
213
|
def locate_node(position, node_types: [])
|
204
|
-
RubyDocument.locate(
|
214
|
+
RubyDocument.locate(
|
215
|
+
@parse_result.value,
|
216
|
+
create_scanner.find_char_position(position),
|
217
|
+
node_types: node_types,
|
218
|
+
encoding: @encoding,
|
219
|
+
)
|
205
220
|
end
|
206
221
|
end
|
207
222
|
end
|
data/lib/ruby_lsp/server.rb
CHANGED
@@ -43,6 +43,8 @@ module RubyLsp
|
|
43
43
|
text_document_semantic_tokens_range(message)
|
44
44
|
when "textDocument/formatting"
|
45
45
|
text_document_formatting(message)
|
46
|
+
when "textDocument/rangeFormatting"
|
47
|
+
text_document_range_formatting(message)
|
46
48
|
when "textDocument/documentHighlight"
|
47
49
|
text_document_document_highlight(message)
|
48
50
|
when "textDocument/onTypeFormatting"
|
@@ -67,6 +69,10 @@ module RubyLsp
|
|
67
69
|
text_document_definition(message)
|
68
70
|
when "textDocument/prepareTypeHierarchy"
|
69
71
|
text_document_prepare_type_hierarchy(message)
|
72
|
+
when "textDocument/rename"
|
73
|
+
text_document_rename(message)
|
74
|
+
when "textDocument/references"
|
75
|
+
text_document_references(message)
|
70
76
|
when "typeHierarchy/supertypes"
|
71
77
|
type_hierarchy_supertypes(message)
|
72
78
|
when "typeHierarchy/subtypes"
|
@@ -85,7 +91,16 @@ module RubyLsp
|
|
85
91
|
id: message[:id],
|
86
92
|
response:
|
87
93
|
Addon.addons.map do |addon|
|
88
|
-
|
94
|
+
version_method = addon.method(:version)
|
95
|
+
|
96
|
+
# If the add-on doesn't define a `version` method, we'd be calling the abstract method defined by
|
97
|
+
# Sorbet, which would raise an error.
|
98
|
+
# Therefore, we only call the method if it's defined by the add-on itself
|
99
|
+
if version_method.owner != Addon
|
100
|
+
version = addon.version
|
101
|
+
end
|
102
|
+
|
103
|
+
{ name: addon.name, version: version, errored: addon.error? }
|
89
104
|
end,
|
90
105
|
),
|
91
106
|
)
|
@@ -123,9 +138,9 @@ module RubyLsp
|
|
123
138
|
send_log_message("Error processing #{message[:method]}: #{e.full_message}", type: Constant::MessageType::ERROR)
|
124
139
|
end
|
125
140
|
|
126
|
-
sig { void }
|
127
|
-
def load_addons
|
128
|
-
errors = Addon.load_addons(@global_state, @outgoing_queue)
|
141
|
+
sig { params(include_project_addons: T::Boolean).void }
|
142
|
+
def load_addons(include_project_addons: true)
|
143
|
+
errors = Addon.load_addons(@global_state, @outgoing_queue, include_project_addons: include_project_addons)
|
129
144
|
|
130
145
|
if errors.any?
|
131
146
|
send_log_message(
|
@@ -227,6 +242,9 @@ module RubyLsp
|
|
227
242
|
workspace_symbol_provider: enabled_features["workspaceSymbol"] && !@global_state.has_type_checker,
|
228
243
|
signature_help_provider: signature_help_provider,
|
229
244
|
type_hierarchy_provider: type_hierarchy_provider,
|
245
|
+
rename_provider: !@global_state.has_type_checker,
|
246
|
+
references_provider: !@global_state.has_type_checker,
|
247
|
+
document_range_formatting_provider: true,
|
230
248
|
experimental: {
|
231
249
|
addon_detection: true,
|
232
250
|
},
|
@@ -320,14 +338,18 @@ module RubyLsp
|
|
320
338
|
language_id: language_id,
|
321
339
|
)
|
322
340
|
|
323
|
-
if document.past_expensive_limit?
|
341
|
+
if document.past_expensive_limit? && text_document[:uri].scheme == "file"
|
342
|
+
log_message = <<~MESSAGE
|
343
|
+
The file #{text_document[:uri].path} is too long. For performance reasons, semantic highlighting and
|
344
|
+
diagnostics will be disabled.
|
345
|
+
MESSAGE
|
346
|
+
|
324
347
|
send_message(
|
325
348
|
Notification.new(
|
326
|
-
method: "window/
|
327
|
-
params: Interface::
|
349
|
+
method: "window/logMessage",
|
350
|
+
params: Interface::LogMessageParams.new(
|
328
351
|
type: Constant::MessageType::WARNING,
|
329
|
-
message:
|
330
|
-
"diagnostics will be disabled",
|
352
|
+
message: log_message,
|
331
353
|
),
|
332
354
|
),
|
333
355
|
)
|
@@ -506,6 +528,34 @@ module RubyLsp
|
|
506
528
|
send_message(Result.new(id: message[:id], response: request.perform))
|
507
529
|
end
|
508
530
|
|
531
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
532
|
+
def text_document_range_formatting(message)
|
533
|
+
# If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
|
534
|
+
if @global_state.formatter == "none"
|
535
|
+
send_empty_response(message[:id])
|
536
|
+
return
|
537
|
+
end
|
538
|
+
|
539
|
+
params = message[:params]
|
540
|
+
uri = params.dig(:textDocument, :uri)
|
541
|
+
# Do not format files outside of the workspace. For example, if someone is looking at a gem's source code, we
|
542
|
+
# don't want to format it
|
543
|
+
path = uri.to_standardized_path
|
544
|
+
unless path.nil? || path.start_with?(@global_state.workspace_path)
|
545
|
+
send_empty_response(message[:id])
|
546
|
+
return
|
547
|
+
end
|
548
|
+
|
549
|
+
document = @store.get(uri)
|
550
|
+
unless document.is_a?(RubyDocument)
|
551
|
+
send_empty_response(message[:id])
|
552
|
+
return
|
553
|
+
end
|
554
|
+
|
555
|
+
response = Requests::RangeFormatting.new(@global_state, document, params).perform
|
556
|
+
send_message(Result.new(id: message[:id], response: response))
|
557
|
+
end
|
558
|
+
|
509
559
|
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
510
560
|
def text_document_formatting(message)
|
511
561
|
# If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
|
@@ -609,6 +659,44 @@ module RubyLsp
|
|
609
659
|
)
|
610
660
|
end
|
611
661
|
|
662
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
663
|
+
def text_document_rename(message)
|
664
|
+
params = message[:params]
|
665
|
+
document = @store.get(params.dig(:textDocument, :uri))
|
666
|
+
|
667
|
+
unless document.is_a?(RubyDocument)
|
668
|
+
send_empty_response(message[:id])
|
669
|
+
return
|
670
|
+
end
|
671
|
+
|
672
|
+
send_message(
|
673
|
+
Result.new(
|
674
|
+
id: message[:id],
|
675
|
+
response: Requests::Rename.new(@global_state, @store, document, params).perform,
|
676
|
+
),
|
677
|
+
)
|
678
|
+
rescue Requests::Rename::InvalidNameError => e
|
679
|
+
send_message(Error.new(id: message[:id], code: Constant::ErrorCodes::REQUEST_FAILED, message: e.message))
|
680
|
+
end
|
681
|
+
|
682
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
683
|
+
def text_document_references(message)
|
684
|
+
params = message[:params]
|
685
|
+
document = @store.get(params.dig(:textDocument, :uri))
|
686
|
+
|
687
|
+
unless document.is_a?(RubyDocument)
|
688
|
+
send_empty_response(message[:id])
|
689
|
+
return
|
690
|
+
end
|
691
|
+
|
692
|
+
send_message(
|
693
|
+
Result.new(
|
694
|
+
id: message[:id],
|
695
|
+
response: Requests::References.new(@global_state, @store, document, params).perform,
|
696
|
+
),
|
697
|
+
)
|
698
|
+
end
|
699
|
+
|
612
700
|
sig { params(document: Document[T.untyped]).returns(RubyDocument::SorbetLevel) }
|
613
701
|
def sorbet_level(document)
|
614
702
|
return RubyDocument::SorbetLevel::Ignore unless @global_state.has_type_checker
|
@@ -684,7 +772,7 @@ module RubyLsp
|
|
684
772
|
return
|
685
773
|
end
|
686
774
|
|
687
|
-
result = Requests::CodeActionResolve.new(document, params).perform
|
775
|
+
result = Requests::CodeActionResolve.new(document, @global_state, params).perform
|
688
776
|
|
689
777
|
case result
|
690
778
|
when Requests::CodeActionResolve::Error::EmptySelection
|
@@ -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
|
data/lib/ruby_lsp/store.rb
CHANGED
@@ -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(
|
data/lib/ruby_lsp/test_helper.rb
CHANGED
@@ -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
|
-
|
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
|
+
```
|