ruby-lsp 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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