ruby-lsp 0.4.3 → 0.4.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
# ![Code lens demo](../../misc/code_lens.gif)
|
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
|