ruby-lsp 0.4.3 → 0.4.5
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/README.md +2 -1
- data/VERSION +1 -1
- data/exe/ruby-lsp +10 -1
- data/lib/ruby_lsp/document.rb +62 -3
- data/lib/ruby_lsp/event_emitter.rb +54 -0
- data/lib/ruby_lsp/executor.rb +71 -10
- data/lib/ruby_lsp/extension.rb +104 -0
- data/lib/ruby_lsp/internal.rb +3 -0
- data/lib/ruby_lsp/listener.rb +49 -0
- data/lib/ruby_lsp/requests/base_request.rb +2 -85
- data/lib/ruby_lsp/requests/code_action_resolve.rb +46 -19
- data/lib/ruby_lsp/requests/code_lens.rb +135 -0
- data/lib/ruby_lsp/requests/document_highlight.rb +4 -6
- data/lib/ruby_lsp/requests/hover.rb +42 -32
- data/lib/ruby_lsp/requests/on_type_formatting.rb +4 -0
- data/lib/ruby_lsp/requests/path_completion.rb +7 -7
- data/lib/ruby_lsp/requests/support/common.rb +55 -0
- data/lib/ruby_lsp/requests.rb +3 -0
- data/lib/ruby_lsp/server.rb +14 -5
- data/lib/ruby_lsp/store.rb +3 -3
- data/lib/ruby_lsp/utils.rb +12 -6
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 437c2048862bed450fd70057489636117c99943dd7bb68df35c4e161a29f8385
|
4
|
+
data.tar.gz: 30d26e8455f9086bc934ffe23b5e37816f18f5904e3422495c316a980a860cfc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 366dd4b756ded58762b598545e398059fe0eae7578b13c92cc0a90dcc9aba2d58106dbf2dc0e9867a9db7de5007e6114ebbbf7cf191bcdebb843b077de945ba9
|
7
|
+
data.tar.gz: 1f798c1462825cccafd48bff23baecb8ba8ef36f9b5733502d79c5d8e6e52550c7ab722a99678eec04ce2a8b42c4d84aab912acb2100ff7d26e422b02f2c185b
|
data/README.md
CHANGED
@@ -44,6 +44,7 @@ See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth de
|
|
44
44
|
## Learn More
|
45
45
|
|
46
46
|
* [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
|
47
|
+
* [Remote Ruby: Ruby Language Server with Vinicius Stock](https://remoteruby.com/221)
|
47
48
|
|
48
49
|
## Contributing
|
49
50
|
|
@@ -122,7 +123,7 @@ To add a new expectations test runner for a new request handler:
|
|
122
123
|
3. [`vscode-ruby-lsp`] Select `Run Extension` and click the green triangle (or press F5).
|
123
124
|
4. [`vscode-ruby-lsp`] Now VS Code will:
|
124
125
|
- Open another workspace as the `Extension Development Host`.
|
125
|
-
- Run `vscode-ruby-lsp` extension in debug mode, which will start a new `ruby-lsp` process with the `--debug` flag.
|
126
|
+
- Run `vscode-ruby-lsp` extension in debug mode, which will start a new `ruby-lsp` process with the `--debug` flag. Note that debugging is not available on Windows.
|
126
127
|
5. Open `ruby-lsp` in VS Code.
|
127
128
|
6. [`ruby-lsp`] Run `bin/rdbg -A` to connect to the running `ruby-lsp` process.
|
128
129
|
7. [`ruby-lsp`] Use commands like `b <file>:<line>` or `b Class#method` to set breakpoints and type `c` to continue the process.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.4.
|
1
|
+
0.4.5
|
data/exe/ruby-lsp
CHANGED
@@ -21,6 +21,11 @@ end
|
|
21
21
|
require_relative "../lib/ruby_lsp/internal"
|
22
22
|
|
23
23
|
if ARGV.include?("--debug")
|
24
|
+
if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
|
25
|
+
puts "Debugging is not supported on Windows"
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
24
29
|
sockets_dir = "/tmp/ruby-lsp-debug-sockets"
|
25
30
|
Dir.mkdir(sockets_dir) unless Dir.exist?(sockets_dir)
|
26
31
|
# ruby-debug-ENV["USER"] is an implicit naming pattern in ruby/debug
|
@@ -28,7 +33,11 @@ if ARGV.include?("--debug")
|
|
28
33
|
socket_identifier = "ruby-debug-#{ENV["USER"]}-#{File.basename(Dir.pwd)}.sock"
|
29
34
|
ENV["RUBY_DEBUG_SOCK_PATH"] = "#{sockets_dir}/#{socket_identifier}"
|
30
35
|
|
31
|
-
|
36
|
+
begin
|
37
|
+
require "debug/open_nonstop"
|
38
|
+
rescue LoadError
|
39
|
+
warn("You need to install the debug gem to use the --debug flag")
|
40
|
+
end
|
32
41
|
end
|
33
42
|
|
34
43
|
RubyLsp::Server.new.start
|
data/lib/ruby_lsp/document.rb
CHANGED
@@ -22,7 +22,7 @@ module RubyLsp
|
|
22
22
|
attr_reader :uri
|
23
23
|
|
24
24
|
sig { params(source: String, version: Integer, uri: String, encoding: String).void }
|
25
|
-
def initialize(source:, version:, uri:, encoding:
|
25
|
+
def initialize(source:, version:, uri:, encoding: Constant::PositionEncodingKind::UTF8)
|
26
26
|
@cache = T.let({}, T::Hash[Symbol, T.untyped])
|
27
27
|
@encoding = T.let(encoding, String)
|
28
28
|
@source = T.let(source, String)
|
@@ -77,9 +77,9 @@ module RubyLsp
|
|
77
77
|
def parse
|
78
78
|
return if @unparsed_edits.empty?
|
79
79
|
|
80
|
+
@unparsed_edits.clear
|
80
81
|
@tree = SyntaxTree.parse(@source)
|
81
82
|
@syntax_error = false
|
82
|
-
@unparsed_edits.clear
|
83
83
|
rescue SyntaxTree::Parser::ParseError
|
84
84
|
@syntax_error = true
|
85
85
|
end
|
@@ -99,6 +99,61 @@ module RubyLsp
|
|
99
99
|
Scanner.new(@source, @encoding)
|
100
100
|
end
|
101
101
|
|
102
|
+
sig do
|
103
|
+
params(
|
104
|
+
position: PositionShape,
|
105
|
+
node_types: T::Array[T.class_of(SyntaxTree::Node)],
|
106
|
+
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
|
107
|
+
end
|
108
|
+
def locate_node(position, node_types: [])
|
109
|
+
return [nil, nil] unless parsed?
|
110
|
+
|
111
|
+
locate(T.must(@tree), create_scanner.find_char_position(position))
|
112
|
+
end
|
113
|
+
|
114
|
+
sig do
|
115
|
+
params(
|
116
|
+
node: SyntaxTree::Node,
|
117
|
+
char_position: Integer,
|
118
|
+
node_types: T::Array[T.class_of(SyntaxTree::Node)],
|
119
|
+
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
|
120
|
+
end
|
121
|
+
def locate(node, char_position, node_types: [])
|
122
|
+
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
|
123
|
+
closest = node
|
124
|
+
parent = T.let(nil, T.nilable(SyntaxTree::Node))
|
125
|
+
|
126
|
+
until queue.empty?
|
127
|
+
candidate = queue.shift
|
128
|
+
|
129
|
+
# Skip nil child nodes
|
130
|
+
next if candidate.nil?
|
131
|
+
|
132
|
+
# Add the next child_nodes to the queue to be processed
|
133
|
+
queue.concat(candidate.child_nodes)
|
134
|
+
|
135
|
+
# Skip if the current node doesn't cover the desired position
|
136
|
+
loc = candidate.location
|
137
|
+
next unless (loc.start_char...loc.end_char).cover?(char_position)
|
138
|
+
|
139
|
+
# If the node's start character is already past the position, then we should've found the closest node
|
140
|
+
# already
|
141
|
+
break if char_position < loc.start_char
|
142
|
+
|
143
|
+
# If there are node types to filter by, and the current node is not one of those types, then skip it
|
144
|
+
next if node_types.any? && node_types.none? { |type| candidate.class == type }
|
145
|
+
|
146
|
+
# If the current node is narrower than or equal to the previous closest node, then it is more precise
|
147
|
+
closest_loc = closest.location
|
148
|
+
if loc.end_char - loc.start_char <= closest_loc.end_char - closest_loc.start_char
|
149
|
+
parent = closest
|
150
|
+
closest = candidate
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
[closest, parent]
|
155
|
+
end
|
156
|
+
|
102
157
|
class Scanner
|
103
158
|
extend T::Sig
|
104
159
|
|
@@ -127,7 +182,11 @@ module RubyLsp
|
|
127
182
|
# The final position is the beginning of the line plus the requested column. If the encoding is UTF-16, we also
|
128
183
|
# need to adjust for surrogate pairs
|
129
184
|
requested_position = @pos + position[:character]
|
130
|
-
|
185
|
+
|
186
|
+
if @encoding == Constant::PositionEncodingKind::UTF16
|
187
|
+
requested_position -= utf_16_character_position_correction(@pos, requested_position)
|
188
|
+
end
|
189
|
+
|
131
190
|
requested_position
|
132
191
|
end
|
133
192
|
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
# EventEmitter is an intermediary between our requests and Syntax Tree visitors. It's used to visit the document's AST
|
6
|
+
# and emit events that the requests can listen to for providing functionality. Usages:
|
7
|
+
#
|
8
|
+
# - For positional requests, locate the target node and use `emit_for_target` to fire events for each listener
|
9
|
+
# - For nonpositional requests, use `visit` to go through the AST, which will fire events for each listener as nodes
|
10
|
+
# are found
|
11
|
+
#
|
12
|
+
# # Example
|
13
|
+
#
|
14
|
+
# ```ruby
|
15
|
+
# target_node = document.locate_node(position)
|
16
|
+
# listener = Requests::Hover.new
|
17
|
+
# EventEmitter.new(listener).emit_for_target(target_node)
|
18
|
+
# listener.response
|
19
|
+
# ```
|
20
|
+
class EventEmitter < SyntaxTree::Visitor
|
21
|
+
extend T::Sig
|
22
|
+
|
23
|
+
sig { params(listeners: Listener[T.untyped]).void }
|
24
|
+
def initialize(*listeners)
|
25
|
+
@listeners = listeners
|
26
|
+
|
27
|
+
# Create a map of event name to listeners that have registered it, so that we avoid unnecessary invocations
|
28
|
+
@event_to_listener_map = T.let(
|
29
|
+
listeners.each_with_object(Hash.new { |h, k| h[k] = [] }) do |listener, hash|
|
30
|
+
listener.class.events&.each { |event| hash[event] << listener }
|
31
|
+
end,
|
32
|
+
T::Hash[Symbol, T::Array[Listener[T.untyped]]],
|
33
|
+
)
|
34
|
+
|
35
|
+
super()
|
36
|
+
end
|
37
|
+
|
38
|
+
# Emit events for a specific node. This is similar to the regular `visit` method, but avoids going deeper into the
|
39
|
+
# tree for performance
|
40
|
+
sig { params(node: T.nilable(SyntaxTree::Node)).void }
|
41
|
+
def emit_for_target(node)
|
42
|
+
case node
|
43
|
+
when SyntaxTree::Command
|
44
|
+
@event_to_listener_map[:on_command]&.each { |listener| T.unsafe(listener).on_command(node) }
|
45
|
+
when SyntaxTree::CallNode
|
46
|
+
@event_to_listener_map[:on_call]&.each { |listener| T.unsafe(listener).on_call(node) }
|
47
|
+
when SyntaxTree::ConstPathRef
|
48
|
+
@event_to_listener_map[:on_const_path_ref]&.each { |listener| T.unsafe(listener).on_const_path_ref(node) }
|
49
|
+
when SyntaxTree::Const
|
50
|
+
@event_to_listener_map[:on_const]&.each { |listener| T.unsafe(listener).on_const(node) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/ruby_lsp/executor.rb
CHANGED
@@ -11,7 +11,7 @@ module RubyLsp
|
|
11
11
|
# Requests that mutate the store must be run sequentially! Parallel requests only receive a temporary copy of the
|
12
12
|
# store
|
13
13
|
@store = store
|
14
|
-
@
|
14
|
+
@messages = T.let([], T::Array[Message])
|
15
15
|
end
|
16
16
|
|
17
17
|
sig { params(request: T::Hash[Symbol, T.untyped]).returns(Result) }
|
@@ -25,7 +25,7 @@ module RubyLsp
|
|
25
25
|
error = e
|
26
26
|
end
|
27
27
|
|
28
|
-
Result.new(response: response, error: error, request_time: request_time,
|
28
|
+
Result.new(response: response, error: error, request_time: request_time, messages: @messages)
|
29
29
|
end
|
30
30
|
|
31
31
|
private
|
@@ -38,6 +38,22 @@ module RubyLsp
|
|
38
38
|
when "initialize"
|
39
39
|
initialize_request(request.dig(:params))
|
40
40
|
when "initialized"
|
41
|
+
Extension.load_extensions
|
42
|
+
|
43
|
+
errored_extensions = Extension.extensions.select(&:error?)
|
44
|
+
|
45
|
+
if errored_extensions.any?
|
46
|
+
@messages << Notification.new(
|
47
|
+
message: "window/showMessage",
|
48
|
+
params: Interface::ShowMessageParams.new(
|
49
|
+
type: Constant::MessageType::WARNING,
|
50
|
+
message: "Error loading extensions:\n\n#{errored_extensions.map(&:formatted_errors).join("\n\n")}",
|
51
|
+
),
|
52
|
+
)
|
53
|
+
|
54
|
+
warn(errored_extensions.map(&:backtraces).join("\n\n"))
|
55
|
+
end
|
56
|
+
|
41
57
|
warn("Ruby LSP is ready")
|
42
58
|
VOID
|
43
59
|
when "textDocument/didOpen"
|
@@ -47,7 +63,7 @@ module RubyLsp
|
|
47
63
|
request.dig(:params, :textDocument, :version),
|
48
64
|
)
|
49
65
|
when "textDocument/didClose"
|
50
|
-
@
|
66
|
+
@messages << Notification.new(
|
51
67
|
message: "textDocument/publishDiagnostics",
|
52
68
|
params: Interface::PublishDiagnosticsParams.new(uri: uri, diagnostics: []),
|
53
69
|
)
|
@@ -75,7 +91,7 @@ module RubyLsp
|
|
75
91
|
begin
|
76
92
|
formatting(uri)
|
77
93
|
rescue Requests::Formatting::InvalidFormatter => error
|
78
|
-
@
|
94
|
+
@messages << Notification.new(
|
79
95
|
message: "window/showMessage",
|
80
96
|
params: Interface::ShowMessageParams.new(
|
81
97
|
type: Constant::MessageType::ERROR,
|
@@ -85,7 +101,7 @@ module RubyLsp
|
|
85
101
|
|
86
102
|
nil
|
87
103
|
rescue StandardError => error
|
88
|
-
@
|
104
|
+
@messages << Notification.new(
|
89
105
|
message: "window/showMessage",
|
90
106
|
params: Interface::ShowMessageParams.new(
|
91
107
|
type: Constant::MessageType::ERROR,
|
@@ -111,7 +127,7 @@ module RubyLsp
|
|
111
127
|
begin
|
112
128
|
diagnostic(uri)
|
113
129
|
rescue StandardError => error
|
114
|
-
@
|
130
|
+
@messages << Notification.new(
|
115
131
|
message: "window/showMessage",
|
116
132
|
params: Interface::ShowMessageParams.new(
|
117
133
|
type: Constant::MessageType::ERROR,
|
@@ -123,6 +139,8 @@ module RubyLsp
|
|
123
139
|
end
|
124
140
|
when "textDocument/completion"
|
125
141
|
completion(uri, request.dig(:params, :position))
|
142
|
+
when "textDocument/codeLens"
|
143
|
+
code_lens(uri)
|
126
144
|
end
|
127
145
|
end
|
128
146
|
|
@@ -133,6 +151,13 @@ module RubyLsp
|
|
133
151
|
end
|
134
152
|
end
|
135
153
|
|
154
|
+
sig { params(uri: String).returns(T::Array[Interface::CodeLens]) }
|
155
|
+
def code_lens(uri)
|
156
|
+
@store.cache_fetch(uri, :code_lens) do |document|
|
157
|
+
Requests::CodeLens.new(document).run
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
136
161
|
sig do
|
137
162
|
params(
|
138
163
|
uri: String,
|
@@ -140,7 +165,27 @@ module RubyLsp
|
|
140
165
|
).returns(T.nilable(Interface::Hover))
|
141
166
|
end
|
142
167
|
def hover(uri, position)
|
143
|
-
|
168
|
+
document = @store.get(uri)
|
169
|
+
document.parse
|
170
|
+
return if document.syntax_error?
|
171
|
+
|
172
|
+
target, parent = document.locate_node(position)
|
173
|
+
|
174
|
+
if !Requests::Hover::ALLOWED_TARGETS.include?(target.class) &&
|
175
|
+
Requests::Hover::ALLOWED_TARGETS.include?(parent.class)
|
176
|
+
target = parent
|
177
|
+
end
|
178
|
+
|
179
|
+
# Instantiate all listeners
|
180
|
+
base_listener = Requests::Hover.new
|
181
|
+
listeners = Requests::Hover.listeners.map(&:new)
|
182
|
+
|
183
|
+
# Emit events for all listeners
|
184
|
+
T.unsafe(EventEmitter).new(base_listener, *listeners).emit_for_target(target)
|
185
|
+
|
186
|
+
# Merge all responses into a single hover
|
187
|
+
listeners.each { |ext| base_listener.merge_response!(ext) }
|
188
|
+
base_listener.response
|
144
189
|
end
|
145
190
|
|
146
191
|
sig { params(uri: String).returns(T::Array[Interface::DocumentLink]) }
|
@@ -269,7 +314,7 @@ module RubyLsp
|
|
269
314
|
|
270
315
|
case result
|
271
316
|
when Requests::CodeActionResolve::Error::EmptySelection
|
272
|
-
@
|
317
|
+
@messages << Notification.new(
|
273
318
|
message: "window/showMessage",
|
274
319
|
params: Interface::ShowMessageParams.new(
|
275
320
|
type: Constant::MessageType::ERROR,
|
@@ -278,7 +323,7 @@ module RubyLsp
|
|
278
323
|
)
|
279
324
|
raise Requests::CodeActionResolve::CodeActionError
|
280
325
|
when Requests::CodeActionResolve::Error::InvalidTargetRange
|
281
|
-
@
|
326
|
+
@messages << Notification.new(
|
282
327
|
message: "window/showMessage",
|
283
328
|
params: Interface::ShowMessageParams.new(
|
284
329
|
type: Constant::MessageType::ERROR,
|
@@ -326,11 +371,21 @@ module RubyLsp
|
|
326
371
|
sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
|
327
372
|
def initialize_request(options)
|
328
373
|
@store.clear
|
329
|
-
|
374
|
+
|
375
|
+
encodings = options.dig(:capabilities, :general, :positionEncodings)
|
376
|
+
@store.encoding = if encodings.nil? || encodings.empty?
|
377
|
+
Constant::PositionEncodingKind::UTF16
|
378
|
+
elsif encodings.include?(Constant::PositionEncodingKind::UTF8)
|
379
|
+
Constant::PositionEncodingKind::UTF8
|
380
|
+
else
|
381
|
+
encodings.first
|
382
|
+
end
|
383
|
+
|
330
384
|
formatter = options.dig(:initializationOptions, :formatter)
|
331
385
|
@store.formatter = formatter unless formatter.nil?
|
332
386
|
|
333
387
|
configured_features = options.dig(:initializationOptions, :enabledFeatures)
|
388
|
+
experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
|
334
389
|
|
335
390
|
enabled_features = case configured_features
|
336
391
|
when Array
|
@@ -359,6 +414,10 @@ module RubyLsp
|
|
359
414
|
Interface::DocumentLinkOptions.new(resolve_provider: false)
|
360
415
|
end
|
361
416
|
|
417
|
+
code_lens_provider = if experimental_features
|
418
|
+
Interface::CodeLensOptions.new(resolve_provider: false)
|
419
|
+
end
|
420
|
+
|
362
421
|
hover_provider = if enabled_features["hover"]
|
363
422
|
Interface::HoverClientCapabilities.new(dynamic_registration: false)
|
364
423
|
end
|
@@ -414,6 +473,7 @@ module RubyLsp
|
|
414
473
|
change: Constant::TextDocumentSyncKind::INCREMENTAL,
|
415
474
|
open_close: true,
|
416
475
|
),
|
476
|
+
position_encoding: @store.encoding,
|
417
477
|
selection_range_provider: enabled_features["selectionRanges"],
|
418
478
|
hover_provider: hover_provider,
|
419
479
|
document_symbol_provider: document_symbol_provider,
|
@@ -427,6 +487,7 @@ module RubyLsp
|
|
427
487
|
diagnostic_provider: diagnostics_provider,
|
428
488
|
inlay_hint_provider: inlay_hint_provider,
|
429
489
|
completion_provider: completion_provider,
|
490
|
+
code_lens_provider: code_lens_provider,
|
430
491
|
),
|
431
492
|
)
|
432
493
|
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
# To register an extension, inherit from this class and implement both `name` and `activate`
|
6
|
+
#
|
7
|
+
# # Example
|
8
|
+
#
|
9
|
+
# ```ruby
|
10
|
+
# module MyGem
|
11
|
+
# class MyExtension < Extension
|
12
|
+
# def activate
|
13
|
+
# # Perform any relevant initialization
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# def name
|
17
|
+
# "My extension name"
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# ```
|
22
|
+
class Extension
|
23
|
+
extend T::Sig
|
24
|
+
extend T::Helpers
|
25
|
+
|
26
|
+
abstract!
|
27
|
+
|
28
|
+
class << self
|
29
|
+
extend T::Sig
|
30
|
+
|
31
|
+
# Automatically track and instantiate extension classes
|
32
|
+
sig { params(child_class: T.class_of(Extension)).void }
|
33
|
+
def inherited(child_class)
|
34
|
+
extensions << child_class.new
|
35
|
+
super
|
36
|
+
end
|
37
|
+
|
38
|
+
sig { returns(T::Array[Extension]) }
|
39
|
+
def extensions
|
40
|
+
@extensions ||= T.let([], T.nilable(T::Array[Extension]))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Discovers and loads all extensions. Returns the list of activated extensions
|
44
|
+
sig { returns(T::Array[Extension]) }
|
45
|
+
def load_extensions
|
46
|
+
# Require all extensions entry points, which should be placed under
|
47
|
+
# `some_gem/lib/ruby_lsp/your_gem_name/extension.rb`
|
48
|
+
Gem.find_files("ruby_lsp/**/extension.rb").each do |extension|
|
49
|
+
require File.expand_path(extension)
|
50
|
+
rescue => e
|
51
|
+
warn(e.message)
|
52
|
+
warn(e.backtrace.to_s) # rubocop:disable Lint/RedundantStringCoercion
|
53
|
+
end
|
54
|
+
|
55
|
+
# Activate each one of the discovered extensions. If any problems occur in the extensions, we don't want to
|
56
|
+
# fail to boot the server
|
57
|
+
extensions.each do |extension|
|
58
|
+
extension.activate
|
59
|
+
nil
|
60
|
+
rescue => e
|
61
|
+
extension.add_error(e)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
sig { void }
|
67
|
+
def initialize
|
68
|
+
@errors = T.let([], T::Array[StandardError])
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { params(error: StandardError).returns(T.self_type) }
|
72
|
+
def add_error(error)
|
73
|
+
@errors << error
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { returns(T::Boolean) }
|
78
|
+
def error?
|
79
|
+
@errors.any?
|
80
|
+
end
|
81
|
+
|
82
|
+
sig { returns(String) }
|
83
|
+
def formatted_errors
|
84
|
+
<<~ERRORS
|
85
|
+
#{name}:
|
86
|
+
#{@errors.map(&:message).join("\n")}
|
87
|
+
ERRORS
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { returns(String) }
|
91
|
+
def backtraces
|
92
|
+
@errors.filter_map(&:backtrace).join("\n\n")
|
93
|
+
end
|
94
|
+
|
95
|
+
# Each extension should implement `MyExtension#activate` and use to perform any sort of initialization, such as
|
96
|
+
# reading information into memory or even spawning a separate process
|
97
|
+
sig { abstract.void }
|
98
|
+
def activate; end
|
99
|
+
|
100
|
+
# Extensions should override the `name` method to return the extension name
|
101
|
+
sig { abstract.returns(String) }
|
102
|
+
def name; end
|
103
|
+
end
|
104
|
+
end
|
data/lib/ruby_lsp/internal.rb
CHANGED
@@ -11,5 +11,8 @@ require "ruby-lsp"
|
|
11
11
|
require "ruby_lsp/utils"
|
12
12
|
require "ruby_lsp/server"
|
13
13
|
require "ruby_lsp/executor"
|
14
|
+
require "ruby_lsp/event_emitter"
|
14
15
|
require "ruby_lsp/requests"
|
16
|
+
require "ruby_lsp/listener"
|
15
17
|
require "ruby_lsp/store"
|
18
|
+
require "ruby_lsp/extension"
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
# Listener is an abstract class to be used by requests for listening to events emitted when visiting an AST using the
|
6
|
+
# EventEmitter.
|
7
|
+
class Listener
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
extend T::Generic
|
11
|
+
include Requests::Support::Common
|
12
|
+
|
13
|
+
ResponseType = type_member
|
14
|
+
|
15
|
+
abstract!
|
16
|
+
|
17
|
+
class << self
|
18
|
+
extend T::Sig
|
19
|
+
|
20
|
+
sig { returns(T.nilable(T::Array[Symbol])) }
|
21
|
+
attr_reader :events
|
22
|
+
|
23
|
+
sig { returns(T::Array[T.class_of(Listener)]) }
|
24
|
+
def listeners
|
25
|
+
@listeners ||= T.let([], T.nilable(T::Array[T.class_of(Listener)]))
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { params(listener: T.class_of(Listener)).void }
|
29
|
+
def add_listener(listener)
|
30
|
+
listeners << listener
|
31
|
+
end
|
32
|
+
|
33
|
+
# All listener events must be defined inside of a `listener_events` block. This is to ensure we know which events
|
34
|
+
# have been registered. Defining an event outside of this block will simply not register it and it'll never be
|
35
|
+
# invoked
|
36
|
+
sig { params(block: T.proc.void).void }
|
37
|
+
def listener_events(&block)
|
38
|
+
current_methods = instance_methods
|
39
|
+
block.call
|
40
|
+
@events = T.let(instance_methods - current_methods, T.nilable(T::Array[Symbol]))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Override this method with an attr_reader that returns the response of your listener. The listener should
|
45
|
+
# accumulate results in a @response variable and then provide the reader so that it is accessible
|
46
|
+
sig { abstract.returns(ResponseType) }
|
47
|
+
def response; end
|
48
|
+
end
|
49
|
+
end
|
@@ -7,6 +7,7 @@ module RubyLsp
|
|
7
7
|
class BaseRequest < SyntaxTree::Visitor
|
8
8
|
extend T::Sig
|
9
9
|
extend T::Helpers
|
10
|
+
include Support::Common
|
10
11
|
|
11
12
|
abstract!
|
12
13
|
|
@@ -31,94 +32,10 @@ module RubyLsp
|
|
31
32
|
# Syntax Tree implements `visit_all` using `map` instead of `each` for users who want to use the pattern
|
32
33
|
# `result = visitor.visit(tree)`. However, we don't use that pattern and should avoid producing a new array for
|
33
34
|
# every single node visited
|
34
|
-
sig { params(nodes: T::Array[SyntaxTree::Node]).void }
|
35
|
+
sig { params(nodes: T::Array[T.nilable(SyntaxTree::Node)]).void }
|
35
36
|
def visit_all(nodes)
|
36
37
|
nodes.each { |node| visit(node) }
|
37
38
|
end
|
38
|
-
|
39
|
-
sig { params(node: SyntaxTree::Node).returns(Interface::Range) }
|
40
|
-
def range_from_syntax_tree_node(node)
|
41
|
-
loc = node.location
|
42
|
-
|
43
|
-
Interface::Range.new(
|
44
|
-
start: Interface::Position.new(
|
45
|
-
line: loc.start_line - 1,
|
46
|
-
character: loc.start_column,
|
47
|
-
),
|
48
|
-
end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
|
49
|
-
)
|
50
|
-
end
|
51
|
-
|
52
|
-
sig do
|
53
|
-
params(node: T.any(SyntaxTree::ConstPathRef, SyntaxTree::ConstRef, SyntaxTree::TopConstRef)).returns(String)
|
54
|
-
end
|
55
|
-
def full_constant_name(node)
|
56
|
-
name = +node.constant.value
|
57
|
-
constant = T.let(node, SyntaxTree::Node)
|
58
|
-
|
59
|
-
while constant.is_a?(SyntaxTree::ConstPathRef)
|
60
|
-
constant = constant.parent
|
61
|
-
|
62
|
-
case constant
|
63
|
-
when SyntaxTree::ConstPathRef
|
64
|
-
name.prepend("#{constant.constant.value}::")
|
65
|
-
when SyntaxTree::VarRef
|
66
|
-
name.prepend("#{constant.value.value}::")
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
name
|
71
|
-
end
|
72
|
-
|
73
|
-
sig do
|
74
|
-
params(
|
75
|
-
node: SyntaxTree::Node,
|
76
|
-
position: Integer,
|
77
|
-
node_types: T::Array[T.class_of(SyntaxTree::Node)],
|
78
|
-
).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
|
79
|
-
end
|
80
|
-
def locate(node, position, node_types: [])
|
81
|
-
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
|
82
|
-
closest = node
|
83
|
-
|
84
|
-
until queue.empty?
|
85
|
-
candidate = queue.shift
|
86
|
-
|
87
|
-
# Skip nil child nodes
|
88
|
-
next if candidate.nil?
|
89
|
-
|
90
|
-
# Add the next child_nodes to the queue to be processed
|
91
|
-
queue.concat(candidate.child_nodes)
|
92
|
-
|
93
|
-
# Skip if the current node doesn't cover the desired position
|
94
|
-
loc = candidate.location
|
95
|
-
next unless (loc.start_char...loc.end_char).cover?(position)
|
96
|
-
|
97
|
-
# If the node's start character is already past the position, then we should've found the closest node already
|
98
|
-
break if position < loc.start_char
|
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
|
-
|
103
|
-
# If the current node is narrower than or equal to the previous closest node, then it is more precise
|
104
|
-
closest_loc = closest.location
|
105
|
-
if loc.end_char - loc.start_char <= closest_loc.end_char - closest_loc.start_char
|
106
|
-
parent = T.let(closest, SyntaxTree::Node)
|
107
|
-
closest = candidate
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
[closest, parent]
|
112
|
-
end
|
113
|
-
|
114
|
-
sig { params(node: T.nilable(SyntaxTree::Node), range: T.nilable(T::Range[Integer])).returns(T::Boolean) }
|
115
|
-
def visible?(node, range)
|
116
|
-
return true if range.nil?
|
117
|
-
return false if node.nil?
|
118
|
-
|
119
|
-
loc = node.location
|
120
|
-
range.cover?(loc.start_line - 1) && range.cover?(loc.end_line - 1)
|
121
|
-
end
|
122
39
|
end
|
123
40
|
end
|
124
41
|
end
|
@@ -54,7 +54,8 @@ module RubyLsp
|
|
54
54
|
extracted_source = T.must(@document.source[start_index...end_index])
|
55
55
|
|
56
56
|
# Find the closest statements node, so that we place the refactor in a valid position
|
57
|
-
closest_statements =
|
57
|
+
closest_statements, parent_statements = @document
|
58
|
+
.locate(T.must(@document.tree), start_index, node_types: [SyntaxTree::Statements])
|
58
59
|
return Error::InvalidTargetRange if closest_statements.nil?
|
59
60
|
|
60
61
|
# Find the node with the end line closest to the requested position, so that we can place the refactor
|
@@ -64,25 +65,51 @@ module RubyLsp
|
|
64
65
|
distance <= 0 ? Float::INFINITY : distance
|
65
66
|
end
|
66
67
|
|
67
|
-
#
|
68
|
-
|
69
|
-
|
68
|
+
# Find the parent expression of the closest node
|
69
|
+
parent_expression = parent_statements.child_nodes.compact.find do |node|
|
70
|
+
loc = node.location
|
71
|
+
loc.start_line - 1 <= source_range.dig(:start, :line) && loc.end_line - 1 >= source_range.dig(:end, :line)
|
72
|
+
end if parent_statements
|
73
|
+
|
74
|
+
closest_node_loc = closest_node.location
|
75
|
+
# If the parent expression is a single line block, then we have to extract it inside of the oneline block
|
76
|
+
if parent_expression.is_a?(SyntaxTree::MethodAddBlock) &&
|
77
|
+
parent_expression.location.start_line == parent_expression.location.end_line
|
78
|
+
|
79
|
+
variable_source = " #{NEW_VARIABLE_NAME} = #{extracted_source};"
|
80
|
+
character = source_range.dig(:start, :character) - 1
|
81
|
+
target_range = {
|
82
|
+
start: { line: closest_node_loc.end_line - 1, character: character },
|
83
|
+
end: { line: closest_node_loc.end_line - 1, character: character },
|
84
|
+
}
|
70
85
|
else
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
+
# If the closest node covers the requested location, then we're extracting a statement nested inside of it. In
|
87
|
+
# that case, we want to place the extraction at the start of the closest node (one line above). Otherwise, we
|
88
|
+
# want to place the extract right below the closest node
|
89
|
+
if closest_node_loc.start_line - 1 <= source_range.dig(
|
90
|
+
:start,
|
91
|
+
:line,
|
92
|
+
) && closest_node_loc.end_line - 1 >= source_range.dig(:end, :line)
|
93
|
+
indentation_line = closest_node_loc.start_line - 1
|
94
|
+
target_line = indentation_line
|
95
|
+
else
|
96
|
+
target_line = closest_node_loc.end_line
|
97
|
+
indentation_line = closest_node_loc.end_line - 1
|
98
|
+
end
|
99
|
+
|
100
|
+
lines = @document.source.lines
|
101
|
+
indentation = T.must(T.must(lines[indentation_line])[/\A */]).size
|
102
|
+
|
103
|
+
target_range = {
|
104
|
+
start: { line: target_line, character: indentation },
|
105
|
+
end: { line: target_line, character: indentation },
|
106
|
+
}
|
107
|
+
|
108
|
+
variable_source = if T.must(lines[target_line]).strip.empty?
|
109
|
+
"\n#{" " * indentation}#{NEW_VARIABLE_NAME} = #{extracted_source}"
|
110
|
+
else
|
111
|
+
"#{NEW_VARIABLE_NAME} = #{extracted_source}\n#{" " * indentation}"
|
112
|
+
end
|
86
113
|
end
|
87
114
|
|
88
115
|
Interface::CodeAction.new(
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Requests
|
6
|
+
# 
|
7
|
+
#
|
8
|
+
# This feature is currently experimental. Clients will need to pass `experimentalFeaturesEnabled`
|
9
|
+
# in the initialization options to enable it.
|
10
|
+
#
|
11
|
+
# The
|
12
|
+
# [code lens](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens)
|
13
|
+
# request informs the editor of runnable commands such as tests
|
14
|
+
#
|
15
|
+
# # Example
|
16
|
+
#
|
17
|
+
# ```ruby
|
18
|
+
# # Run
|
19
|
+
# class Test < Minitest::Test
|
20
|
+
# end
|
21
|
+
# ```
|
22
|
+
|
23
|
+
class CodeLens < BaseRequest
|
24
|
+
BASE_COMMAND = T.let((File.exist?("Gemfile.lock") ? "bundle exec ruby" : "ruby") + " -Itest ", String)
|
25
|
+
ACCESS_MODIFIERS = T.let(["public", "private", "protected"], T::Array[String])
|
26
|
+
|
27
|
+
sig do
|
28
|
+
params(
|
29
|
+
document: Document,
|
30
|
+
).void
|
31
|
+
end
|
32
|
+
def initialize(document)
|
33
|
+
super(document)
|
34
|
+
@results = T.let([], T::Array[Interface::CodeLens])
|
35
|
+
@path = T.let(document.uri.delete_prefix("file://"), String)
|
36
|
+
@modifier = T.let("public", String)
|
37
|
+
end
|
38
|
+
|
39
|
+
sig { override.returns(T.all(T::Array[Interface::CodeLens], Object)) }
|
40
|
+
def run
|
41
|
+
visit(@document.tree) if @document.parsed?
|
42
|
+
@results
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { override.params(node: SyntaxTree::ClassDeclaration).void }
|
46
|
+
def visit_class(node)
|
47
|
+
class_name = node.constant.constant.value
|
48
|
+
if class_name.end_with?("Test")
|
49
|
+
add_code_lens(node, name: class_name, command: BASE_COMMAND + @path)
|
50
|
+
end
|
51
|
+
visit(node.bodystmt)
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { override.params(node: SyntaxTree::DefNode).void }
|
55
|
+
def visit_def(node)
|
56
|
+
if @modifier == "public"
|
57
|
+
method_name = node.name.value
|
58
|
+
if method_name.start_with?("test_")
|
59
|
+
add_code_lens(
|
60
|
+
node,
|
61
|
+
name: method_name,
|
62
|
+
command: BASE_COMMAND + @path + " --name " + method_name,
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
sig { override.params(node: SyntaxTree::Command).void }
|
69
|
+
def visit_command(node)
|
70
|
+
if node.message.value == "public"
|
71
|
+
with_visiblity("public", node)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
sig { override.params(node: SyntaxTree::CallNode).void }
|
76
|
+
def visit_call(node)
|
77
|
+
ident = node.message if node.message.is_a?(SyntaxTree::Ident)
|
78
|
+
|
79
|
+
if ident
|
80
|
+
if T.cast(ident, SyntaxTree::Ident).value == "public"
|
81
|
+
with_visiblity("public", node)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
sig { override.params(node: SyntaxTree::VCall).void }
|
87
|
+
def visit_vcall(node)
|
88
|
+
vcall_value = node.value.value
|
89
|
+
|
90
|
+
if ACCESS_MODIFIERS.include?(vcall_value)
|
91
|
+
@modifier = vcall_value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
sig do
|
98
|
+
params(
|
99
|
+
visibility: String,
|
100
|
+
node: T.any(SyntaxTree::CallNode, SyntaxTree::Command),
|
101
|
+
).void
|
102
|
+
end
|
103
|
+
def with_visiblity(visibility, node)
|
104
|
+
current_visibility = @modifier
|
105
|
+
@modifier = visibility
|
106
|
+
visit(node.arguments)
|
107
|
+
ensure
|
108
|
+
@modifier = T.must(current_visibility)
|
109
|
+
end
|
110
|
+
|
111
|
+
sig { params(node: SyntaxTree::Node, name: String, command: String).void }
|
112
|
+
def add_code_lens(node, name:, command:)
|
113
|
+
@results << Interface::CodeLens.new(
|
114
|
+
range: range_from_syntax_tree_node(node),
|
115
|
+
command: Interface::Command.new(
|
116
|
+
title: "Run",
|
117
|
+
command: "rubyLsp.runTest",
|
118
|
+
arguments: [
|
119
|
+
@path,
|
120
|
+
name,
|
121
|
+
command,
|
122
|
+
{
|
123
|
+
start_line: node.location.start_line - 1,
|
124
|
+
start_column: node.location.start_column,
|
125
|
+
end_line: node.location.end_line - 1,
|
126
|
+
end_column: node.location.end_column,
|
127
|
+
},
|
128
|
+
],
|
129
|
+
),
|
130
|
+
data: { type: "test" },
|
131
|
+
)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -32,8 +32,7 @@ module RubyLsp
|
|
32
32
|
@highlights = T.let([], T::Array[Interface::DocumentHighlight])
|
33
33
|
return unless document.parsed?
|
34
34
|
|
35
|
-
|
36
|
-
@target = T.let(find(T.must(document.tree), position), T.nilable(Support::HighlightTarget))
|
35
|
+
@target = T.let(find(position), T.nilable(Support::HighlightTarget))
|
37
36
|
end
|
38
37
|
|
39
38
|
sig { override.returns(T.all(T::Array[Interface::DocumentHighlight], Object)) }
|
@@ -68,12 +67,11 @@ module RubyLsp
|
|
68
67
|
|
69
68
|
sig do
|
70
69
|
params(
|
71
|
-
|
72
|
-
position: Integer,
|
70
|
+
position: Document::PositionShape,
|
73
71
|
).returns(T.nilable(Support::HighlightTarget))
|
74
72
|
end
|
75
|
-
def find(
|
76
|
-
matched, parent =
|
73
|
+
def find(position)
|
74
|
+
matched, parent = @document.locate_node(position)
|
77
75
|
|
78
76
|
return unless matched && parent
|
79
77
|
return unless matched.is_a?(SyntaxTree::Ident) || DIRECT_HIGHLIGHTS.include?(matched.class)
|
@@ -17,8 +17,11 @@ module RubyLsp
|
|
17
17
|
# before_save :do_something # when hovering on before_save, the link will be rendered
|
18
18
|
# end
|
19
19
|
# ```
|
20
|
-
class Hover <
|
20
|
+
class Hover < Listener
|
21
21
|
extend T::Sig
|
22
|
+
extend T::Generic
|
23
|
+
|
24
|
+
ResponseType = type_member { { fixed: T.nilable(Interface::Hover) } }
|
22
25
|
|
23
26
|
ALLOWED_TARGETS = T.let(
|
24
27
|
[
|
@@ -29,53 +32,60 @@ module RubyLsp
|
|
29
32
|
T::Array[T.class_of(SyntaxTree::Node)],
|
30
33
|
)
|
31
34
|
|
32
|
-
sig {
|
33
|
-
|
34
|
-
|
35
|
+
sig { override.returns(ResponseType) }
|
36
|
+
attr_reader :response
|
37
|
+
|
38
|
+
sig { void }
|
39
|
+
def initialize
|
40
|
+
@response = T.let(nil, ResponseType)
|
41
|
+
super()
|
42
|
+
end
|
43
|
+
|
44
|
+
# Merges responses from other hover listeners
|
45
|
+
sig { params(other: Listener[ResponseType]).returns(T.self_type) }
|
46
|
+
def merge_response!(other)
|
47
|
+
other_response = other.response
|
48
|
+
return self unless other_response
|
49
|
+
|
50
|
+
if @response.nil?
|
51
|
+
@response = other.response
|
52
|
+
else
|
53
|
+
@response.contents.value << other_response.contents.value << "\n\n"
|
54
|
+
end
|
35
55
|
|
36
|
-
|
56
|
+
self
|
37
57
|
end
|
38
58
|
|
39
|
-
|
40
|
-
|
41
|
-
|
59
|
+
listener_events do
|
60
|
+
sig { params(node: SyntaxTree::Command).void }
|
61
|
+
def on_command(node)
|
62
|
+
message = node.message
|
63
|
+
@response = generate_rails_document_link_hover(message.value, message)
|
64
|
+
end
|
42
65
|
|
43
|
-
|
44
|
-
|
66
|
+
sig { params(node: SyntaxTree::ConstPathRef).void }
|
67
|
+
def on_const_path_ref(node)
|
68
|
+
@response = generate_rails_document_link_hover(full_constant_name(node), node)
|
69
|
+
end
|
45
70
|
|
46
|
-
|
47
|
-
|
48
|
-
message =
|
49
|
-
generate_rails_document_link_hover(message.value, message)
|
50
|
-
when SyntaxTree::CallNode
|
51
|
-
message = target.message
|
71
|
+
sig { params(node: SyntaxTree::CallNode).void }
|
72
|
+
def on_call(node)
|
73
|
+
message = node.message
|
52
74
|
return if message.is_a?(Symbol)
|
53
75
|
|
54
|
-
generate_rails_document_link_hover(message.value, message)
|
55
|
-
when SyntaxTree::ConstPathRef
|
56
|
-
constant_name = full_constant_name(target)
|
57
|
-
generate_rails_document_link_hover(constant_name, target)
|
76
|
+
@response = generate_rails_document_link_hover(message.value, message)
|
58
77
|
end
|
59
78
|
end
|
60
79
|
|
61
80
|
private
|
62
81
|
|
63
|
-
sig
|
64
|
-
params(name: String, node: SyntaxTree::Node).returns(T.nilable(Interface::Hover))
|
65
|
-
end
|
82
|
+
sig { params(name: String, node: SyntaxTree::Node).returns(T.nilable(Interface::Hover)) }
|
66
83
|
def generate_rails_document_link_hover(name, node)
|
67
84
|
urls = Support::RailsDocumentClient.generate_rails_document_urls(name)
|
68
|
-
|
69
85
|
return if urls.empty?
|
70
86
|
|
71
|
-
contents = Interface::MarkupContent.new(
|
72
|
-
|
73
|
-
value: urls.join("\n\n"),
|
74
|
-
)
|
75
|
-
Interface::Hover.new(
|
76
|
-
range: range_from_syntax_tree_node(node),
|
77
|
-
contents: contents,
|
78
|
-
)
|
87
|
+
contents = Interface::MarkupContent.new(kind: "markdown", value: urls.join("\n\n"))
|
88
|
+
Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
|
79
89
|
end
|
80
90
|
end
|
81
91
|
end
|
@@ -80,6 +80,10 @@ module RubyLsp
|
|
80
80
|
|
81
81
|
sig { void }
|
82
82
|
def handle_statement_end
|
83
|
+
# If a keyword occurs in a line which appears be a comment or a string, we will not try to format it, since
|
84
|
+
# it could be a coincidental match. This approach is not perfect, but it should cover most cases.
|
85
|
+
return if @previous_line.start_with?(/["'#]/)
|
86
|
+
|
83
87
|
return unless END_REGEXES.any? { |regex| regex.match?(@previous_line) }
|
84
88
|
|
85
89
|
indents = " " * @indentation
|
@@ -29,8 +29,7 @@ module RubyLsp
|
|
29
29
|
# We can't verify if we're inside a require when there are syntax errors
|
30
30
|
return [] if @document.syntax_error?
|
31
31
|
|
32
|
-
|
33
|
-
target = T.let(find(char_position), T.nilable(SyntaxTree::TStringContent))
|
32
|
+
target = T.let(find, T.nilable(SyntaxTree::TStringContent))
|
34
33
|
# no target means the we are not inside a `require` call
|
35
34
|
return [] unless target
|
36
35
|
|
@@ -51,11 +50,12 @@ module RubyLsp
|
|
51
50
|
end
|
52
51
|
end
|
53
52
|
|
54
|
-
sig {
|
55
|
-
def find
|
56
|
-
|
53
|
+
sig { returns(T.nilable(SyntaxTree::TStringContent)) }
|
54
|
+
def find
|
55
|
+
char_position = @document.create_scanner.find_char_position(@position)
|
56
|
+
matched, parent = @document.locate(
|
57
57
|
T.must(@document.tree),
|
58
|
-
|
58
|
+
char_position,
|
59
59
|
node_types: [SyntaxTree::Command, SyntaxTree::CommandCall, SyntaxTree::CallNode],
|
60
60
|
)
|
61
61
|
|
@@ -76,7 +76,7 @@ module RubyLsp
|
|
76
76
|
|
77
77
|
path_node = argument.parts.first
|
78
78
|
return unless path_node.is_a?(SyntaxTree::TStringContent)
|
79
|
-
return unless (path_node.location.start_char..path_node.location.end_char).cover?(
|
79
|
+
return unless (path_node.location.start_char..path_node.location.end_char).cover?(char_position)
|
80
80
|
|
81
81
|
path_node
|
82
82
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Requests
|
6
|
+
module Support
|
7
|
+
module Common
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { params(node: SyntaxTree::Node).returns(Interface::Range) }
|
11
|
+
def range_from_syntax_tree_node(node)
|
12
|
+
loc = node.location
|
13
|
+
|
14
|
+
Interface::Range.new(
|
15
|
+
start: Interface::Position.new(
|
16
|
+
line: loc.start_line - 1,
|
17
|
+
character: loc.start_column,
|
18
|
+
),
|
19
|
+
end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig do
|
24
|
+
params(node: T.any(SyntaxTree::ConstPathRef, SyntaxTree::ConstRef, SyntaxTree::TopConstRef)).returns(String)
|
25
|
+
end
|
26
|
+
def full_constant_name(node)
|
27
|
+
name = +node.constant.value
|
28
|
+
constant = T.let(node, SyntaxTree::Node)
|
29
|
+
|
30
|
+
while constant.is_a?(SyntaxTree::ConstPathRef)
|
31
|
+
constant = constant.parent
|
32
|
+
|
33
|
+
case constant
|
34
|
+
when SyntaxTree::ConstPathRef
|
35
|
+
name.prepend("#{constant.constant.value}::")
|
36
|
+
when SyntaxTree::VarRef
|
37
|
+
name.prepend("#{constant.value.value}::")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
name
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { params(node: T.nilable(SyntaxTree::Node), range: T.nilable(T::Range[Integer])).returns(T::Boolean) }
|
45
|
+
def visible?(node, range)
|
46
|
+
return true if range.nil?
|
47
|
+
return false if node.nil?
|
48
|
+
|
49
|
+
loc = node.location
|
50
|
+
range.cover?(loc.start_line - 1) && range.cover?(loc.end_line - 1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/ruby_lsp/requests.rb
CHANGED
@@ -18,6 +18,7 @@ module RubyLsp
|
|
18
18
|
# - {RubyLsp::Requests::DocumentHighlight}
|
19
19
|
# - {RubyLsp::Requests::InlayHints}
|
20
20
|
# - {RubyLsp::Requests::PathCompletion}
|
21
|
+
# - {RubyLsp::Requests::CodeLens}
|
21
22
|
|
22
23
|
module Requests
|
23
24
|
autoload :BaseRequest, "ruby_lsp/requests/base_request"
|
@@ -35,6 +36,7 @@ module RubyLsp
|
|
35
36
|
autoload :DocumentHighlight, "ruby_lsp/requests/document_highlight"
|
36
37
|
autoload :InlayHints, "ruby_lsp/requests/inlay_hints"
|
37
38
|
autoload :PathCompletion, "ruby_lsp/requests/path_completion"
|
39
|
+
autoload :CodeLens, "ruby_lsp/requests/code_lens"
|
38
40
|
|
39
41
|
# :nodoc:
|
40
42
|
module Support
|
@@ -46,6 +48,7 @@ module RubyLsp
|
|
46
48
|
autoload :HighlightTarget, "ruby_lsp/requests/support/highlight_target"
|
47
49
|
autoload :RailsDocumentClient, "ruby_lsp/requests/support/rails_document_client"
|
48
50
|
autoload :PrefixTree, "ruby_lsp/requests/support/prefix_tree"
|
51
|
+
autoload :Common, "ruby_lsp/requests/support/common"
|
49
52
|
end
|
50
53
|
end
|
51
54
|
end
|
data/lib/ruby_lsp/server.rb
CHANGED
@@ -23,6 +23,7 @@ module RubyLsp
|
|
23
23
|
@jobs = T.let({}, T::Hash[T.any(String, Integer), Job])
|
24
24
|
@mutex = T.let(Mutex.new, Mutex)
|
25
25
|
@worker = T.let(new_worker, Thread)
|
26
|
+
@current_request_id = T.let(1, Integer)
|
26
27
|
|
27
28
|
Thread.main.priority = 1
|
28
29
|
end
|
@@ -54,7 +55,7 @@ module RubyLsp
|
|
54
55
|
@worker.join
|
55
56
|
@store.clear
|
56
57
|
|
57
|
-
finalize_request(Result.new(response: nil,
|
58
|
+
finalize_request(Result.new(response: nil, messages: []), request)
|
58
59
|
when "exit"
|
59
60
|
# We return zero if shutdown has already been received or one otherwise as per the recommendation in the spec
|
60
61
|
# https://microsoft.github.io/language-server-protocol/specification/#exit
|
@@ -88,7 +89,7 @@ module RubyLsp
|
|
88
89
|
|
89
90
|
result = if job.cancelled
|
90
91
|
# We need to return nil to the client even if the request was cancelled
|
91
|
-
Result.new(response: nil,
|
92
|
+
Result.new(response: nil, messages: [])
|
92
93
|
else
|
93
94
|
Executor.new(@store).execute(request)
|
94
95
|
end
|
@@ -105,9 +106,6 @@ module RubyLsp
|
|
105
106
|
error = result.error
|
106
107
|
response = result.response
|
107
108
|
|
108
|
-
# If the response include any notifications, go through them and publish each one
|
109
|
-
result.notifications.each { |n| @writer.write(method: n.message, params: n.params) }
|
110
|
-
|
111
109
|
if error
|
112
110
|
@writer.write(
|
113
111
|
id: request[:id],
|
@@ -121,6 +119,17 @@ module RubyLsp
|
|
121
119
|
@writer.write(id: request[:id], result: response)
|
122
120
|
end
|
123
121
|
|
122
|
+
# If the response include any messages, go through them and publish each one
|
123
|
+
result.messages.each do |n|
|
124
|
+
case n
|
125
|
+
when Notification
|
126
|
+
@writer.write(method: n.message, params: n.params)
|
127
|
+
when Request
|
128
|
+
@writer.write(id: @current_request_id, method: n.message, params: n.params)
|
129
|
+
@current_request_id += 1
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
124
133
|
request_time = result.request_time
|
125
134
|
if request_time
|
126
135
|
@writer.write(method: "telemetry/event", params: telemetry_params(request, request_time, error))
|
data/lib/ruby_lsp/store.rb
CHANGED
@@ -9,8 +9,8 @@ module RubyLsp
|
|
9
9
|
class Store
|
10
10
|
extend T::Sig
|
11
11
|
|
12
|
-
sig {
|
13
|
-
|
12
|
+
sig { returns(String) }
|
13
|
+
attr_accessor :encoding
|
14
14
|
|
15
15
|
sig { returns(String) }
|
16
16
|
attr_accessor :formatter
|
@@ -18,7 +18,7 @@ module RubyLsp
|
|
18
18
|
sig { void }
|
19
19
|
def initialize
|
20
20
|
@state = T.let({}, T::Hash[String, Document])
|
21
|
-
@encoding = T.let(
|
21
|
+
@encoding = T.let(Constant::PositionEncodingKind::UTF8, String)
|
22
22
|
@formatter = T.let("auto", String)
|
23
23
|
end
|
24
24
|
|
data/lib/ruby_lsp/utils.rb
CHANGED
@@ -9,8 +9,11 @@ module RubyLsp
|
|
9
9
|
WORKSPACE_URI = T.let("file://#{Dir.pwd}".freeze, String) # rubocop:disable Style/RedundantFreeze
|
10
10
|
|
11
11
|
# A notification to be sent to the client
|
12
|
-
class
|
12
|
+
class Message
|
13
13
|
extend T::Sig
|
14
|
+
extend T::Helpers
|
15
|
+
|
16
|
+
abstract!
|
14
17
|
|
15
18
|
sig { returns(String) }
|
16
19
|
attr_reader :message
|
@@ -25,6 +28,9 @@ module RubyLsp
|
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
31
|
+
class Notification < Message; end
|
32
|
+
class Request < Message; end
|
33
|
+
|
28
34
|
# The final result of running a request before its IO is finalized
|
29
35
|
class Result
|
30
36
|
extend T::Sig
|
@@ -32,8 +38,8 @@ module RubyLsp
|
|
32
38
|
sig { returns(T.untyped) }
|
33
39
|
attr_reader :response
|
34
40
|
|
35
|
-
sig { returns(T::Array[
|
36
|
-
attr_reader :
|
41
|
+
sig { returns(T::Array[Message]) }
|
42
|
+
attr_reader :messages
|
37
43
|
|
38
44
|
sig { returns(T.nilable(Exception)) }
|
39
45
|
attr_reader :error
|
@@ -44,14 +50,14 @@ module RubyLsp
|
|
44
50
|
sig do
|
45
51
|
params(
|
46
52
|
response: T.untyped,
|
47
|
-
|
53
|
+
messages: T::Array[Message],
|
48
54
|
error: T.nilable(Exception),
|
49
55
|
request_time: T.nilable(Float),
|
50
56
|
).void
|
51
57
|
end
|
52
|
-
def initialize(response:,
|
58
|
+
def initialize(response:, messages:, error: nil, request_time: nil)
|
53
59
|
@response = response
|
54
|
-
@
|
60
|
+
@messages = messages
|
55
61
|
@error = error
|
56
62
|
@request_time = request_time
|
57
63
|
end
|
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.
|
4
|
+
version: 0.4.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-04-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: language_server-protocol
|
@@ -73,12 +73,16 @@ files:
|
|
73
73
|
- lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb
|
74
74
|
- lib/ruby-lsp.rb
|
75
75
|
- lib/ruby_lsp/document.rb
|
76
|
+
- lib/ruby_lsp/event_emitter.rb
|
76
77
|
- lib/ruby_lsp/executor.rb
|
78
|
+
- lib/ruby_lsp/extension.rb
|
77
79
|
- lib/ruby_lsp/internal.rb
|
80
|
+
- lib/ruby_lsp/listener.rb
|
78
81
|
- lib/ruby_lsp/requests.rb
|
79
82
|
- lib/ruby_lsp/requests/base_request.rb
|
80
83
|
- lib/ruby_lsp/requests/code_action_resolve.rb
|
81
84
|
- lib/ruby_lsp/requests/code_actions.rb
|
85
|
+
- lib/ruby_lsp/requests/code_lens.rb
|
82
86
|
- lib/ruby_lsp/requests/diagnostics.rb
|
83
87
|
- lib/ruby_lsp/requests/document_highlight.rb
|
84
88
|
- lib/ruby_lsp/requests/document_link.rb
|
@@ -92,6 +96,7 @@ files:
|
|
92
96
|
- lib/ruby_lsp/requests/selection_ranges.rb
|
93
97
|
- lib/ruby_lsp/requests/semantic_highlighting.rb
|
94
98
|
- lib/ruby_lsp/requests/support/annotation.rb
|
99
|
+
- lib/ruby_lsp/requests/support/common.rb
|
95
100
|
- lib/ruby_lsp/requests/support/highlight_target.rb
|
96
101
|
- lib/ruby_lsp/requests/support/prefix_tree.rb
|
97
102
|
- lib/ruby_lsp/requests/support/rails_document_client.rb
|
@@ -127,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
132
|
- !ruby/object:Gem::Version
|
128
133
|
version: '0'
|
129
134
|
requirements: []
|
130
|
-
rubygems_version: 3.4.
|
135
|
+
rubygems_version: 3.4.12
|
131
136
|
signing_key:
|
132
137
|
specification_version: 4
|
133
138
|
summary: An opinionated language server for Ruby
|