ruby-lsp 0.4.3 → 0.4.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/VERSION +1 -1
- data/lib/ruby_lsp/document.rb +62 -3
- data/lib/ruby_lsp/event_emitter.rb +52 -0
- data/lib/ruby_lsp/executor.rb +40 -2
- data/lib/ruby_lsp/internal.rb +2 -0
- data/lib/ruby_lsp/listener.rb +39 -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 +27 -32
- 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/store.rb +3 -3
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61c8fe2a55057823533a8a17b4472309e3d463ec3d63302fa3393f046ec4b339
|
4
|
+
data.tar.gz: fb9636b8aafac9d5002a872d0f34f7f9852e9348009cdc2934369637a4cadb4c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db51fd5526431b773125c0b27bfe8ca53b2861759457f9a36e7a681bcfbffb0f58026b4f8cc4dc991be2380309959aacde7c8cc2cd0a0f0373dfb313d0d28db9
|
7
|
+
data.tar.gz: 13cc3991bffd1a18dd1e2580c8d1051161b03a171a25e8ce681f46fb8d5baedb1de0f9f96304556e08f3db95c2262e29693d22bf3659ba45eff620157adc035c
|
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
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.4.
|
1
|
+
0.4.4
|
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,52 @@
|
|
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
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/ruby_lsp/executor.rb
CHANGED
@@ -123,6 +123,8 @@ module RubyLsp
|
|
123
123
|
end
|
124
124
|
when "textDocument/completion"
|
125
125
|
completion(uri, request.dig(:params, :position))
|
126
|
+
when "textDocument/codeLens"
|
127
|
+
code_lens(uri)
|
126
128
|
end
|
127
129
|
end
|
128
130
|
|
@@ -133,6 +135,13 @@ module RubyLsp
|
|
133
135
|
end
|
134
136
|
end
|
135
137
|
|
138
|
+
sig { params(uri: String).returns(T::Array[Interface::CodeLens]) }
|
139
|
+
def code_lens(uri)
|
140
|
+
@store.cache_fetch(uri, :code_lens) do |document|
|
141
|
+
Requests::CodeLens.new(document).run
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
136
145
|
sig do
|
137
146
|
params(
|
138
147
|
uri: String,
|
@@ -140,7 +149,20 @@ module RubyLsp
|
|
140
149
|
).returns(T.nilable(Interface::Hover))
|
141
150
|
end
|
142
151
|
def hover(uri, position)
|
143
|
-
|
152
|
+
document = @store.get(uri)
|
153
|
+
document.parse
|
154
|
+
return if document.syntax_error?
|
155
|
+
|
156
|
+
target, parent = document.locate_node(position)
|
157
|
+
|
158
|
+
if !Requests::Hover::ALLOWED_TARGETS.include?(target.class) &&
|
159
|
+
Requests::Hover::ALLOWED_TARGETS.include?(parent.class)
|
160
|
+
target = parent
|
161
|
+
end
|
162
|
+
|
163
|
+
listener = RubyLsp::Requests::Hover.new
|
164
|
+
EventEmitter.new(listener).emit_for_target(target)
|
165
|
+
listener.response
|
144
166
|
end
|
145
167
|
|
146
168
|
sig { params(uri: String).returns(T::Array[Interface::DocumentLink]) }
|
@@ -326,11 +348,21 @@ module RubyLsp
|
|
326
348
|
sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
|
327
349
|
def initialize_request(options)
|
328
350
|
@store.clear
|
329
|
-
|
351
|
+
|
352
|
+
encodings = options.dig(:capabilities, :general, :positionEncodings)
|
353
|
+
@store.encoding = if encodings.nil? || encodings.empty?
|
354
|
+
Constant::PositionEncodingKind::UTF16
|
355
|
+
elsif encodings.include?(Constant::PositionEncodingKind::UTF8)
|
356
|
+
Constant::PositionEncodingKind::UTF8
|
357
|
+
else
|
358
|
+
encodings.first
|
359
|
+
end
|
360
|
+
|
330
361
|
formatter = options.dig(:initializationOptions, :formatter)
|
331
362
|
@store.formatter = formatter unless formatter.nil?
|
332
363
|
|
333
364
|
configured_features = options.dig(:initializationOptions, :enabledFeatures)
|
365
|
+
experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
|
334
366
|
|
335
367
|
enabled_features = case configured_features
|
336
368
|
when Array
|
@@ -359,6 +391,10 @@ module RubyLsp
|
|
359
391
|
Interface::DocumentLinkOptions.new(resolve_provider: false)
|
360
392
|
end
|
361
393
|
|
394
|
+
code_lens_provider = if experimental_features
|
395
|
+
Interface::CodeLensOptions.new(resolve_provider: false)
|
396
|
+
end
|
397
|
+
|
362
398
|
hover_provider = if enabled_features["hover"]
|
363
399
|
Interface::HoverClientCapabilities.new(dynamic_registration: false)
|
364
400
|
end
|
@@ -414,6 +450,7 @@ module RubyLsp
|
|
414
450
|
change: Constant::TextDocumentSyncKind::INCREMENTAL,
|
415
451
|
open_close: true,
|
416
452
|
),
|
453
|
+
position_encoding: @store.encoding,
|
417
454
|
selection_range_provider: enabled_features["selectionRanges"],
|
418
455
|
hover_provider: hover_provider,
|
419
456
|
document_symbol_provider: document_symbol_provider,
|
@@ -427,6 +464,7 @@ module RubyLsp
|
|
427
464
|
diagnostic_provider: diagnostics_provider,
|
428
465
|
inlay_hint_provider: inlay_hint_provider,
|
429
466
|
completion_provider: completion_provider,
|
467
|
+
code_lens_provider: code_lens_provider,
|
430
468
|
),
|
431
469
|
)
|
432
470
|
end
|
data/lib/ruby_lsp/internal.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
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
|
+
# All listener events must be defined inside of a `listener_events` block. This is to ensure we know which events
|
24
|
+
# have been registered. Defining an event outside of this block will simply not register it and it'll never be
|
25
|
+
# invoked
|
26
|
+
sig { params(block: T.proc.void).void }
|
27
|
+
def listener_events(&block)
|
28
|
+
current_methods = instance_methods
|
29
|
+
block.call
|
30
|
+
@events = T.let(instance_methods - current_methods, T.nilable(T::Array[Symbol]))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Override this method with an attr_reader that returns the response of your listener. The listener should
|
35
|
+
# accumulate results in a @response variable and then provide the reader so that it is accessible
|
36
|
+
sig { abstract.returns(ResponseType) }
|
37
|
+
def response; end
|
38
|
+
end
|
39
|
+
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,45 @@ module RubyLsp
|
|
29
32
|
T::Array[T.class_of(SyntaxTree::Node)],
|
30
33
|
)
|
31
34
|
|
32
|
-
sig {
|
33
|
-
|
34
|
-
super(document)
|
35
|
+
sig { override.returns(ResponseType) }
|
36
|
+
attr_reader :response
|
35
37
|
|
36
|
-
|
38
|
+
sig { void }
|
39
|
+
def initialize
|
40
|
+
@response = T.let(nil, ResponseType)
|
41
|
+
super()
|
37
42
|
end
|
38
43
|
|
39
|
-
|
40
|
-
|
41
|
-
|
44
|
+
listener_events do
|
45
|
+
sig { params(node: SyntaxTree::Command).void }
|
46
|
+
def on_command(node)
|
47
|
+
message = node.message
|
48
|
+
@response = generate_rails_document_link_hover(message.value, message)
|
49
|
+
end
|
42
50
|
|
43
|
-
|
44
|
-
|
51
|
+
sig { params(node: SyntaxTree::ConstPathRef).void }
|
52
|
+
def on_const_path_ref(node)
|
53
|
+
@response = generate_rails_document_link_hover(full_constant_name(node), node)
|
54
|
+
end
|
45
55
|
|
46
|
-
|
47
|
-
|
48
|
-
message =
|
49
|
-
generate_rails_document_link_hover(message.value, message)
|
50
|
-
when SyntaxTree::CallNode
|
51
|
-
message = target.message
|
56
|
+
sig { params(node: SyntaxTree::CallNode).void }
|
57
|
+
def on_call(node)
|
58
|
+
message = node.message
|
52
59
|
return if message.is_a?(Symbol)
|
53
60
|
|
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)
|
61
|
+
@response = generate_rails_document_link_hover(message.value, message)
|
58
62
|
end
|
59
63
|
end
|
60
64
|
|
61
65
|
private
|
62
66
|
|
63
|
-
sig
|
64
|
-
params(name: String, node: SyntaxTree::Node).returns(T.nilable(Interface::Hover))
|
65
|
-
end
|
67
|
+
sig { params(name: String, node: SyntaxTree::Node).returns(T.nilable(Interface::Hover)) }
|
66
68
|
def generate_rails_document_link_hover(name, node)
|
67
69
|
urls = Support::RailsDocumentClient.generate_rails_document_urls(name)
|
68
|
-
|
69
70
|
return if urls.empty?
|
70
71
|
|
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
|
-
)
|
72
|
+
contents = Interface::MarkupContent.new(kind: "markdown", value: urls.join("\n\n"))
|
73
|
+
Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
|
79
74
|
end
|
80
75
|
end
|
81
76
|
end
|
@@ -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/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
|
|
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.4
|
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-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: language_server-protocol
|
@@ -73,12 +73,15 @@ 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
|
77
78
|
- lib/ruby_lsp/internal.rb
|
79
|
+
- lib/ruby_lsp/listener.rb
|
78
80
|
- lib/ruby_lsp/requests.rb
|
79
81
|
- lib/ruby_lsp/requests/base_request.rb
|
80
82
|
- lib/ruby_lsp/requests/code_action_resolve.rb
|
81
83
|
- lib/ruby_lsp/requests/code_actions.rb
|
84
|
+
- lib/ruby_lsp/requests/code_lens.rb
|
82
85
|
- lib/ruby_lsp/requests/diagnostics.rb
|
83
86
|
- lib/ruby_lsp/requests/document_highlight.rb
|
84
87
|
- lib/ruby_lsp/requests/document_link.rb
|
@@ -92,6 +95,7 @@ files:
|
|
92
95
|
- lib/ruby_lsp/requests/selection_ranges.rb
|
93
96
|
- lib/ruby_lsp/requests/semantic_highlighting.rb
|
94
97
|
- lib/ruby_lsp/requests/support/annotation.rb
|
98
|
+
- lib/ruby_lsp/requests/support/common.rb
|
95
99
|
- lib/ruby_lsp/requests/support/highlight_target.rb
|
96
100
|
- lib/ruby_lsp/requests/support/prefix_tree.rb
|
97
101
|
- lib/ruby_lsp/requests/support/rails_document_client.rb
|
@@ -127,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
131
|
- !ruby/object:Gem::Version
|
128
132
|
version: '0'
|
129
133
|
requirements: []
|
130
|
-
rubygems_version: 3.4.
|
134
|
+
rubygems_version: 3.4.10
|
131
135
|
signing_key:
|
132
136
|
specification_version: 4
|
133
137
|
summary: An opinionated language server for Ruby
|