ruby-lsp 0.3.7 → 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cddece941249b188843957c1c026e6e1e10f875c26a7c2f14f556a53556cc94f
4
- data.tar.gz: cdc7368658e3de269033b41bceb831becb68320cd1444ec9792380408ee2a4f2
3
+ metadata.gz: 5f1ed447e51909ada66f6bebceca82d807686a4889468838a797e2ed8afff50e
4
+ data.tar.gz: 7b429773bbdebc171debbe446adb868136bd15b5f493542898df0a9c82f3514e
5
5
  SHA512:
6
- metadata.gz: d28c2f96939d517cacae6b86407e5990ca1587f99848bc14ff98ea24b5421e2ad38ba0ee39a7040c2a4694623e349c907c6f2a764737e0cc9316d302804687b2
7
- data.tar.gz: a5e118206994b0e58ff9568043eb534360e591f931c0c8f078579283ab0519057a236606728c230e4be86f7405035a6e2bea1e52959af280f2067f0e52127278
6
+ metadata.gz: 62e8a6ee36c43dec6ec0af16fd19830b6369446455a5fc16871c368c8bb4ccdb47c0d9c5678990a43604bf03c622c2ef13bb82c6a6e9f26fdcde72b3b18ec99e
7
+ data.tar.gz: e441a670c22bcdbfdc3afe99743fe6e1c4aa5f28edaedcf09f47255372322c52ead68988bc6d7ce09170e368e4059c94dac73b395a640c86f20b2969ae8e8095
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Ruby LSP
4
4
 
5
- This gem is an implementation of the language server protocol specification for Ruby, used to improve editor features.
5
+ This gem is an implementation of the [language server protocol specification](https://microsoft.github.io/language-server-protocol/) for Ruby, used to improve editor features.
6
6
 
7
7
  ## Usage
8
8
 
@@ -14,7 +14,7 @@ group :development do
14
14
  end
15
15
  ```
16
16
 
17
- If using VS Code, install the [Ruby LSP plugin](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features in
17
+ If using VS Code, install the [Ruby LSP extension](https://github.com/Shopify/vscode-ruby-lsp) to get the extra features in
18
18
  the editor.
19
19
 
20
20
  See the [documentation](https://shopify.github.io/ruby-lsp) for
@@ -36,10 +36,17 @@ For more visibility into which tests are running, use the `SpecReporter`:
36
36
 
37
37
  `SPEC_REPORTER=1 bin/test`
38
38
 
39
+ By default the tests run with warnings disabled to reduce noise. To enable warnings, pass `VERBOSE=1`.
40
+ Warnings are always shown when running in CI.
41
+
39
42
  ### Expectation testing
40
43
 
41
44
  To simplify the way we run tests over different pieces of Ruby code, we use a custom expectations test framework against a set of Ruby fixtures.
42
45
 
46
+ We define expectations as `.exp` files, of which there are two variants:
47
+ * `.exp.rb`, to indicate the resulting code after an operation.
48
+ * `.exp.json`, consisting of a `result`, and an optional set of input `params`.
49
+
43
50
  To add a new fixture to the expectations test suite:
44
51
 
45
52
  1. Add a new fixture `my_fixture.rb` file under `test/fixtures`
@@ -86,6 +93,18 @@ Possible values are:
86
93
  * `messages`: display requests and responses notifications
87
94
  * `verbose`: display each request and response as JSON
88
95
 
96
+ ### Debugging using VS Code
97
+
98
+ The `launch.json` contains a 'Minitest - current file' configuration for the debugger.
99
+
100
+ 1. Add a breakpoint using the VS Code UI.
101
+ 1. Open the relevant test file.
102
+ 1. Open the **Run and Debug** panel on the sidebar.
103
+ 1. Ensure `Minitest - current file` is selected in the top dropdown.
104
+ 1. Press `F5` OR click the green triangle next to the top dropdown. VS Code will then run the test file with debugger activated.
105
+ 1. When the breakpoint is triggered, the process will pause and VS Code will connect to the debugger and activate the debugger UI.
106
+ 1. Open the Debug Console view to use the debugger's REPL.
107
+
89
108
  ## License
90
109
 
91
110
  The gem is available as open source under the terms of the
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.7
1
+ 0.3.8
@@ -15,16 +15,11 @@ module RubyLsp
15
15
  sig { returns(String) }
16
16
  attr_reader :source
17
17
 
18
- sig { returns(T::Array[EditShape]) }
19
- attr_reader :syntax_error_edits
20
-
21
18
  sig { params(source: String, encoding: String).void }
22
19
  def initialize(source, encoding = "utf-8")
23
20
  @cache = T.let({}, T::Hash[Symbol, T.untyped])
24
- @syntax_error_edits = T.let([], T::Array[EditShape])
25
21
  @encoding = T.let(encoding, String)
26
22
  @source = T.let(source, String)
27
- @parsable_source = T.let(source.dup, String)
28
23
  @unparsed_edits = T.let([], T::Array[EditShape])
29
24
  @tree = T.let(SyntaxTree.parse(@source), T.nilable(SyntaxTree::Node))
30
25
  rescue SyntaxTree::Parser::ParseError
@@ -54,8 +49,15 @@ module RubyLsp
54
49
 
55
50
  sig { params(edits: T::Array[EditShape]).void }
56
51
  def push_edits(edits)
57
- # Apply the edits on the real source
58
- edits.each { |edit| apply_edit(@source, edit[:range], edit[:text]) }
52
+ edits.each do |edit|
53
+ range = edit[:range]
54
+ scanner = create_scanner
55
+
56
+ start_position = scanner.find_char_position(range[:start])
57
+ end_position = scanner.find_char_position(range[:end])
58
+
59
+ @source[start_position...end_position] = edit[:text]
60
+ end
59
61
 
60
62
  @unparsed_edits.concat(edits)
61
63
  @cache.clear
@@ -66,17 +68,14 @@ module RubyLsp
66
68
  return if @unparsed_edits.empty?
67
69
 
68
70
  @tree = SyntaxTree.parse(@source)
69
- @syntax_error_edits.clear
70
71
  @unparsed_edits.clear
71
- @parsable_source = @source.dup
72
72
  rescue SyntaxTree::Parser::ParseError
73
- @syntax_error_edits = @unparsed_edits
74
- update_parsable_source(@unparsed_edits)
73
+ # Do nothing if we fail parse
75
74
  end
76
75
 
77
76
  sig { returns(T::Boolean) }
78
- def syntax_errors?
79
- @syntax_error_edits.any?
77
+ def syntax_error?
78
+ @unparsed_edits.any?
80
79
  end
81
80
 
82
81
  sig { returns(T::Boolean) }
@@ -89,33 +88,6 @@ module RubyLsp
89
88
  Scanner.new(@source, @encoding)
90
89
  end
91
90
 
92
- private
93
-
94
- sig { params(edits: T::Array[EditShape]).void }
95
- def update_parsable_source(edits)
96
- # If the new edits caused a syntax error, make all edits blank spaces and line breaks to adjust the line and
97
- # column numbers. This is attempt to make the document parsable while partial edits are being applied
98
- edits.each do |edit|
99
- next if edit[:text].empty? # skip deletions, since they may have caused the syntax error
100
-
101
- apply_edit(@parsable_source, edit[:range], edit[:text].gsub(/[^\r\n]/, " "))
102
- end
103
-
104
- @tree = SyntaxTree.parse(@parsable_source)
105
- rescue StandardError
106
- # Trying to maintain a parsable source when there are syntax errors is a best effort. If we fail to apply edits or
107
- # parse, just ignore it
108
- end
109
-
110
- sig { params(source: String, range: RangeShape, text: String).void }
111
- def apply_edit(source, range, text)
112
- scanner = Scanner.new(source, @encoding)
113
- start_position = scanner.find_char_position(range[:start])
114
- end_position = scanner.find_char_position(range[:end])
115
-
116
- source[start_position...end_position] = text
117
- end
118
-
119
91
  class Scanner
120
92
  extend T::Sig
121
93
 
@@ -59,7 +59,7 @@ module RubyLsp
59
59
 
60
60
  sig { void }
61
61
  def start
62
- $stderr.puts "Starting Ruby LSP..."
62
+ warn("Starting Ruby LSP...")
63
63
 
64
64
  @reader.read do |request|
65
65
  handler = @handlers[request[:method]]
@@ -94,7 +94,7 @@ module RubyLsp
94
94
 
95
95
  sig { void }
96
96
  def shutdown
97
- $stderr.puts "Shutting down Ruby LSP..."
97
+ warn("Shutting down Ruby LSP...")
98
98
  @queue.shutdown
99
99
  store.clear
100
100
  end
@@ -29,13 +29,17 @@ module RubyLsp
29
29
  loc = node.location
30
30
 
31
31
  LanguageServer::Protocol::Interface::Range.new(
32
- start: LanguageServer::Protocol::Interface::Position.new(line: loc.start_line - 1,
33
- character: loc.start_column),
32
+ start: LanguageServer::Protocol::Interface::Position.new(
33
+ line: loc.start_line - 1,
34
+ character: loc.start_column,
35
+ ),
34
36
  end: LanguageServer::Protocol::Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
35
37
  )
36
38
  end
37
39
 
38
- sig { params(node: SyntaxTree::ConstPathRef).returns(String) }
40
+ sig do
41
+ params(node: T.any(SyntaxTree::ConstPathRef, SyntaxTree::ConstRef, SyntaxTree::TopConstRef)).returns(String)
42
+ end
39
43
  def full_constant_name(node)
40
44
  name = +node.constant.value
41
45
  constant = T.let(node, SyntaxTree::Node)
@@ -56,28 +60,39 @@ module RubyLsp
56
60
 
57
61
  sig do
58
62
  params(
59
- parent: SyntaxTree::Node,
60
- target_nodes: T::Array[T.class_of(SyntaxTree::Node)],
63
+ node: SyntaxTree::Node,
61
64
  position: Integer,
62
- ).returns(T::Array[SyntaxTree::Node])
65
+ ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
63
66
  end
64
- def locate_node_and_parent(parent, target_nodes, position)
65
- matched = parent.child_nodes.compact.bsearch do |child|
66
- if (child.location.start_char...child.location.end_char).cover?(position)
67
- 0
68
- else
69
- position <=> child.location.start_char
67
+ def locate(node, position)
68
+ queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
69
+ closest = node
70
+
71
+ until queue.empty?
72
+ candidate = queue.shift
73
+
74
+ # Skip nil child nodes
75
+ next if candidate.nil?
76
+
77
+ # Add the next child_nodes to the queue to be processed
78
+ queue.concat(candidate.child_nodes)
79
+
80
+ # Skip if the current node doesn't cover the desired position
81
+ loc = candidate.location
82
+ next unless (loc.start_char...loc.end_char).cover?(position)
83
+
84
+ # If the node's start character is already past the position, then we should've found the closest node already
85
+ break if position < loc.start_char
86
+
87
+ # If the current node is narrower than or equal to the previous closest node, then it is more precise
88
+ closest_loc = closest.location
89
+ if loc.end_char - loc.start_char <= closest_loc.end_char - closest_loc.start_char
90
+ parent = T.let(closest, SyntaxTree::Node)
91
+ closest = candidate
70
92
  end
71
93
  end
72
94
 
73
- case matched
74
- when *target_nodes
75
- [matched, parent]
76
- when SyntaxTree::Node
77
- locate_node_and_parent(matched, target_nodes, position)
78
- else
79
- []
80
- end
95
+ [closest, parent]
81
96
  end
82
97
 
83
98
  sig { params(node: T.nilable(SyntaxTree::Node), range: T.nilable(T::Range[Integer])).returns(T::Boolean) }
@@ -6,7 +6,7 @@ module RubyLsp
6
6
  # ![Code actions demo](../../misc/code_actions.gif)
7
7
  #
8
8
  # The [code actions](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction)
9
- # request informs the editor of RuboCop quick fixes that can be applied. These are accesible by hovering over a
9
+ # request informs the editor of RuboCop quick fixes that can be applied. These are accessible by hovering over a
10
10
  # specific diagnostic.
11
11
  #
12
12
  # # Example
@@ -37,18 +37,11 @@ module RubyLsp
37
37
  )
38
38
  end
39
39
  def run
40
- return syntax_error_diagnostics if @document.syntax_errors?
40
+ return [] if @document.syntax_error?
41
41
  return [] unless defined?(Support::RuboCopDiagnosticsRunner)
42
42
 
43
43
  Support::RuboCopDiagnosticsRunner.instance.run(@uri, @document)
44
44
  end
45
-
46
- private
47
-
48
- sig { returns(T::Array[Support::SyntaxErrorDiagnostic]) }
49
- def syntax_error_diagnostics
50
- @document.syntax_error_edits.map { |e| Support::SyntaxErrorDiagnostic.new(e) }
51
- end
52
45
  end
53
46
  end
54
47
  end
@@ -7,7 +7,7 @@ module RubyLsp
7
7
  #
8
8
  # The [document highlight](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentHighlight)
9
9
  # informs the editor all relevant elements of the currently pointed item for highlighting. For example, when
10
- # the cursor is on the `F` of the constant `FOO`, the editor should identify other occurences of `FOO`
10
+ # the cursor is on the `F` of the constant `FOO`, the editor should identify other occurrences of `FOO`
11
11
  # and highlight them.
12
12
  #
13
13
  # For writable elements like constants or variables, their read/write occurrences should be highlighted differently.
@@ -55,13 +55,16 @@ module RubyLsp
55
55
 
56
56
  private
57
57
 
58
- DIRECT_HIGHLIGHTS = T.let([
59
- SyntaxTree::GVar,
60
- SyntaxTree::IVar,
61
- SyntaxTree::Const,
62
- SyntaxTree::CVar,
63
- SyntaxTree::VarField,
64
- ], T::Array[T.class_of(SyntaxTree::Node)])
58
+ DIRECT_HIGHLIGHTS = T.let(
59
+ [
60
+ SyntaxTree::GVar,
61
+ SyntaxTree::IVar,
62
+ SyntaxTree::Const,
63
+ SyntaxTree::CVar,
64
+ SyntaxTree::VarField,
65
+ ],
66
+ T::Array[T.class_of(SyntaxTree::Node)],
67
+ )
65
68
 
66
69
  sig do
67
70
  params(
@@ -70,9 +73,10 @@ module RubyLsp
70
73
  ).returns(T.nilable(Support::HighlightTarget))
71
74
  end
72
75
  def find(node, position)
73
- matched, parent = locate_node_and_parent(node, DIRECT_HIGHLIGHTS + [SyntaxTree::Ident], position)
76
+ matched, parent = locate(node, position)
74
77
 
75
78
  return unless matched && parent
79
+ return unless matched.is_a?(SyntaxTree::Ident) || DIRECT_HIGHLIGHTS.include?(matched.class)
76
80
 
77
81
  case matched
78
82
  when *DIRECT_HIGHLIGHTS
@@ -33,34 +33,39 @@ module RubyLsp
33
33
 
34
34
  sig { returns(T::Hash[String, T::Hash[String, T::Hash[String, String]]]) }
35
35
  def gem_paths
36
- @gem_paths ||= T.let(begin
37
- lookup = {}
38
-
39
- Gem::Specification.stubs.each do |stub|
40
- spec = stub.to_spec
41
- lookup[spec.name] = {}
42
- lookup[spec.name][spec.version.to_s] = {}
43
-
44
- Dir.glob("**/*.rb", base: "#{spec.full_gem_path}/").each do |path|
45
- lookup[spec.name][spec.version.to_s][path] = "#{spec.full_gem_path}/#{path}"
36
+ @gem_paths ||= T.let(
37
+ begin
38
+ lookup = {}
39
+
40
+ Gem::Specification.stubs.each do |stub|
41
+ spec = stub.to_spec
42
+ lookup[spec.name] = {}
43
+ lookup[spec.name][spec.version.to_s] = {}
44
+
45
+ Dir.glob("**/*.rb", base: "#{spec.full_gem_path}/").each do |path|
46
+ lookup[spec.name][spec.version.to_s][path] = "#{spec.full_gem_path}/#{path}"
47
+ end
46
48
  end
47
- end
48
-
49
- Gem::Specification.default_stubs.each do |stub|
50
- spec = stub.to_spec
51
- lookup[spec.name] = {}
52
- lookup[spec.name][spec.version.to_s] = {}
53
- prefix_matchers = Regexp.union(spec.require_paths.map { |rp| Regexp.new("^#{rp}/") })
54
- prefix_matcher = Regexp.union(prefix_matchers, //)
55
-
56
- spec.files.each do |file|
57
- path = file.sub(prefix_matcher, "")
58
- lookup[spec.name][spec.version.to_s][path] = "#{RbConfig::CONFIG["rubylibdir"]}/#{path}"
49
+
50
+ Gem::Specification.default_stubs.each do |stub|
51
+ spec = stub.to_spec
52
+ lookup[spec.name] = {}
53
+ lookup[spec.name][spec.version.to_s] = {}
54
+ prefix_matchers = Regexp.union(spec.require_paths.map do |rp|
55
+ Regexp.new("^#{rp}/")
56
+ end)
57
+ prefix_matcher = Regexp.union(prefix_matchers, //)
58
+
59
+ spec.files.each do |file|
60
+ path = file.sub(prefix_matcher, "")
61
+ lookup[spec.name][spec.version.to_s][path] = "#{RbConfig::CONFIG["rubylibdir"]}/#{path}"
62
+ end
59
63
  end
60
- end
61
64
 
62
- lookup
63
- end, T.nilable(T::Hash[String, T::Hash[String, T::Hash[String, String]]]))
65
+ lookup
66
+ end,
67
+ T.nilable(T::Hash[String, T::Hash[String, T::Hash[String, String]]]),
68
+ )
64
69
  end
65
70
  end
66
71
 
@@ -10,7 +10,7 @@ module RubyLsp
10
10
  # informs the editor of all the important symbols, such as classes, variables, and methods, defined in a file. With
11
11
  # this information, the editor can populate breadcrumbs, file outline and allow for fuzzy symbol searches.
12
12
  #
13
- # In VS Code, fuzzy symbol search can be accessed by opened the command palette and inserting an `@` symbol.
13
+ # In VS Code, fuzzy symbol search can be accessed by opening the command palette and inserting an `@` symbol.
14
14
  #
15
15
  # # Example
16
16
  #
@@ -29,34 +29,37 @@ module RubyLsp
29
29
  class DocumentSymbol < BaseRequest
30
30
  extend T::Sig
31
31
 
32
- SYMBOL_KIND = T.let({
33
- file: 1,
34
- module: 2,
35
- namespace: 3,
36
- package: 4,
37
- class: 5,
38
- method: 6,
39
- property: 7,
40
- field: 8,
41
- constructor: 9,
42
- enum: 10,
43
- interface: 11,
44
- function: 12,
45
- variable: 13,
46
- constant: 14,
47
- string: 15,
48
- number: 16,
49
- boolean: 17,
50
- array: 18,
51
- object: 19,
52
- key: 20,
53
- null: 21,
54
- enummember: 22,
55
- struct: 23,
56
- event: 24,
57
- operator: 25,
58
- typeparameter: 26,
59
- }.freeze, T::Hash[Symbol, Integer])
32
+ SYMBOL_KIND = T.let(
33
+ {
34
+ file: 1,
35
+ module: 2,
36
+ namespace: 3,
37
+ package: 4,
38
+ class: 5,
39
+ method: 6,
40
+ property: 7,
41
+ field: 8,
42
+ constructor: 9,
43
+ enum: 10,
44
+ interface: 11,
45
+ function: 12,
46
+ variable: 13,
47
+ constant: 14,
48
+ string: 15,
49
+ number: 16,
50
+ boolean: 17,
51
+ array: 18,
52
+ object: 19,
53
+ key: 20,
54
+ null: 21,
55
+ enummember: 22,
56
+ struct: 23,
57
+ event: 24,
58
+ operator: 25,
59
+ typeparameter: 26,
60
+ }.freeze,
61
+ T::Hash[Symbol, Integer],
62
+ )
60
63
 
61
64
  ATTR_ACCESSORS = T.let(["attr_reader", "attr_writer", "attr_accessor"].freeze, T::Array[String])
62
65
 
@@ -92,7 +95,7 @@ module RubyLsp
92
95
  sig { override.params(node: SyntaxTree::ClassDeclaration).void }
93
96
  def visit_class(node)
94
97
  symbol = create_document_symbol(
95
- name: fully_qualified_name(node),
98
+ name: full_constant_name(node.constant),
96
99
  kind: :class,
97
100
  range_node: node,
98
101
  selection_range_node: node.constant,
@@ -129,43 +132,19 @@ module RubyLsp
129
132
  )
130
133
  end
131
134
 
132
- sig { override.params(node: SyntaxTree::Def).void }
135
+ sig { override.params(node: SyntaxTree::DefNode).void }
133
136
  def visit_def(node)
134
- name = node.name.value
135
-
136
- symbol = create_document_symbol(
137
- name: name,
138
- kind: name == "initialize" ? :constructor : :method,
139
- range_node: node,
140
- selection_range_node: node.name,
141
- )
142
-
143
- @stack << symbol
144
- visit(node.bodystmt)
145
- @stack.pop
146
- end
147
-
148
- sig { override.params(node: SyntaxTree::DefEndless).void }
149
- def visit_def_endless(node)
150
- name = node.name.value
137
+ if node.target&.value&.value == "self"
138
+ name = "self.#{node.name.value}"
139
+ kind = :method
140
+ else
141
+ name = node.name.value
142
+ kind = name == "initialize" ? :constructor : :method
143
+ end
151
144
 
152
145
  symbol = create_document_symbol(
153
146
  name: name,
154
- kind: name == "initialize" ? :constructor : :method,
155
- range_node: node,
156
- selection_range_node: node.name,
157
- )
158
-
159
- @stack << symbol
160
- visit(node.statement)
161
- @stack.pop
162
- end
163
-
164
- sig { override.params(node: SyntaxTree::Defs).void }
165
- def visit_defs(node)
166
- symbol = create_document_symbol(
167
- name: "self.#{node.name.value}",
168
- kind: :method,
147
+ kind: kind,
169
148
  range_node: node,
170
149
  selection_range_node: node.name,
171
150
  )
@@ -178,7 +157,7 @@ module RubyLsp
178
157
  sig { override.params(node: SyntaxTree::ModuleDeclaration).void }
179
158
  def visit_module(node)
180
159
  symbol = create_document_symbol(
181
- name: fully_qualified_name(node),
160
+ name: full_constant_name(node.constant),
182
161
  kind: :module,
183
162
  range_node: node,
184
163
  selection_range_node: node.constant,
@@ -241,25 +220,6 @@ module RubyLsp
241
220
 
242
221
  symbol
243
222
  end
244
-
245
- sig { params(node: T.any(SyntaxTree::ClassDeclaration, SyntaxTree::ModuleDeclaration)).returns(String) }
246
- def fully_qualified_name(node)
247
- constant = T.let(node.constant, SyntaxTree::Node)
248
- name = +node.constant.constant.value
249
-
250
- while constant.is_a?(SyntaxTree::ConstPathRef)
251
- constant = constant.parent
252
-
253
- case constant
254
- when SyntaxTree::ConstPathRef
255
- name.prepend("#{constant.constant.value}::")
256
- when SyntaxTree::VarRef
257
- name.prepend("#{constant.value.value}::")
258
- end
259
- end
260
-
261
- name
262
- end
263
223
  end
264
224
  end
265
225
  end