ruby-lsp 0.0.2 → 0.1.0

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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -1
  3. data/.github/workflows/publish_docs.yml +32 -0
  4. data/.rubocop.yml +25 -0
  5. data/CHANGELOG.md +23 -0
  6. data/Gemfile +8 -4
  7. data/Gemfile.lock +64 -13
  8. data/README.md +58 -1
  9. data/Rakefile +5 -0
  10. data/VERSION +1 -1
  11. data/bin/tapioca +29 -0
  12. data/dev.yml +3 -0
  13. data/exe/ruby-lsp +19 -3
  14. data/lib/ruby-lsp.rb +2 -0
  15. data/lib/ruby_lsp/cli.rb +23 -7
  16. data/lib/ruby_lsp/document.rb +98 -6
  17. data/lib/ruby_lsp/handler.rb +119 -18
  18. data/lib/ruby_lsp/internal.rb +7 -0
  19. data/lib/ruby_lsp/requests/base_request.rb +19 -5
  20. data/lib/ruby_lsp/requests/code_actions.rb +30 -9
  21. data/lib/ruby_lsp/requests/diagnostics.rb +29 -77
  22. data/lib/ruby_lsp/requests/document_highlight.rb +111 -0
  23. data/lib/ruby_lsp/requests/document_symbol.rb +75 -16
  24. data/lib/ruby_lsp/requests/folding_ranges.rb +63 -19
  25. data/lib/ruby_lsp/requests/formatting.rb +19 -2
  26. data/lib/ruby_lsp/requests/rubocop_request.rb +21 -8
  27. data/lib/ruby_lsp/requests/selection_ranges.rb +114 -0
  28. data/lib/ruby_lsp/requests/semantic_highlighting.rb +132 -61
  29. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +100 -0
  30. data/lib/ruby_lsp/requests/support/selection_range.rb +20 -0
  31. data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +70 -0
  32. data/lib/ruby_lsp/requests/support/syntax_error_diagnostic.rb +32 -0
  33. data/lib/ruby_lsp/requests.rb +10 -0
  34. data/lib/ruby_lsp/store.rb +23 -2
  35. data/rakelib/check_docs.rake +57 -0
  36. data/ruby-lsp.gemspec +2 -1
  37. data/sorbet/config +4 -0
  38. data/sorbet/rbi/.rubocop.yml +8 -0
  39. data/sorbet/rbi/gems/ansi@1.5.0.rbi +338 -0
  40. data/sorbet/rbi/gems/ast@2.4.2.rbi +522 -0
  41. data/sorbet/rbi/gems/builder@3.2.4.rbi +418 -0
  42. data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
  43. data/sorbet/rbi/gems/debug@1.5.0.rbi +1273 -0
  44. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +867 -0
  45. data/sorbet/rbi/gems/io-console@0.5.11.rbi +8 -0
  46. data/sorbet/rbi/gems/irb@1.4.1.rbi +376 -0
  47. data/sorbet/rbi/gems/language_server-protocol@3.16.0.3.rbi +7325 -0
  48. data/sorbet/rbi/gems/method_source@1.0.0.rbi +8 -0
  49. data/sorbet/rbi/gems/minitest-reporters@1.5.0.rbi +612 -0
  50. data/sorbet/rbi/gems/minitest@5.15.0.rbi +994 -0
  51. data/sorbet/rbi/gems/parallel@1.22.1.rbi +163 -0
  52. data/sorbet/rbi/gems/parser@3.1.2.0.rbi +3968 -0
  53. data/sorbet/rbi/gems/prettier_print@0.1.0.rbi +734 -0
  54. data/sorbet/rbi/gems/pry@0.14.1.rbi +8 -0
  55. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +227 -0
  56. data/sorbet/rbi/gems/rake@13.0.6.rbi +1853 -0
  57. data/sorbet/rbi/gems/rbi@0.0.14.rbi +2337 -0
  58. data/sorbet/rbi/gems/regexp_parser@2.5.0.rbi +1854 -0
  59. data/sorbet/rbi/gems/reline@0.3.1.rbi +1274 -0
  60. data/sorbet/rbi/gems/rexml@3.2.5.rbi +3852 -0
  61. data/sorbet/rbi/gems/rubocop-ast@1.18.0.rbi +4180 -0
  62. data/sorbet/rbi/gems/rubocop-minitest@0.20.0.rbi +1369 -0
  63. data/sorbet/rbi/gems/rubocop-rake@0.6.0.rbi +246 -0
  64. data/sorbet/rbi/gems/rubocop-shopify@2.6.0.rbi +8 -0
  65. data/sorbet/rbi/gems/rubocop-sorbet@0.6.8.rbi +652 -0
  66. data/sorbet/rbi/gems/rubocop@1.30.0.rbi +36729 -0
  67. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +732 -0
  68. data/sorbet/rbi/gems/spoom@1.1.11.rbi +1600 -0
  69. data/sorbet/rbi/gems/syntax_tree@2.7.1.rbi +6777 -0
  70. data/sorbet/rbi/gems/tapioca@0.8.1.rbi +1972 -0
  71. data/sorbet/rbi/gems/thor@1.2.1.rbi +2921 -0
  72. data/sorbet/rbi/gems/unicode-display_width@2.1.0.rbi +27 -0
  73. data/sorbet/rbi/gems/unparser@0.6.5.rbi +2789 -0
  74. data/sorbet/rbi/gems/webrick@1.7.0.rbi +1779 -0
  75. data/sorbet/rbi/gems/yard-sorbet@0.6.1.rbi +289 -0
  76. data/sorbet/rbi/gems/yard@0.9.27.rbi +13048 -0
  77. data/sorbet/rbi/shims/fiddle.rbi +4 -0
  78. data/sorbet/rbi/shims/hash.rbi +6 -0
  79. data/sorbet/rbi/shims/rdoc.rbi +4 -0
  80. data/sorbet/tapioca/config.yml +13 -0
  81. data/sorbet/tapioca/require.rb +7 -0
  82. metadata +74 -6
  83. data/shipit.production.yml +0 -1
@@ -1,50 +1,81 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require "ruby_lsp/requests"
4
5
  require "ruby_lsp/store"
6
+ require "benchmark"
5
7
 
6
8
  module RubyLsp
7
9
  class Handler
10
+ extend T::Sig
11
+ VOID = T.let(Object.new.freeze, Object)
12
+
13
+ sig { returns(Store) }
8
14
  attr_reader :store
9
15
 
10
16
  Interface = LanguageServer::Protocol::Interface
11
17
  Constant = LanguageServer::Protocol::Constant
12
18
  Transport = LanguageServer::Protocol::Transport
13
19
 
20
+ sig { void }
14
21
  def initialize
15
- @writer = Transport::Stdio::Writer.new
16
- @reader = Transport::Stdio::Reader.new
17
- @handlers = {}
18
- @store = Store.new
22
+ @writer = T.let(Transport::Stdio::Writer.new, Transport::Stdio::Writer)
23
+ @reader = T.let(Transport::Stdio::Reader.new, Transport::Stdio::Reader)
24
+ @handlers = T.let({}, T::Hash[String, T.proc.params(request: T::Hash[Symbol, T.untyped]).returns(T.untyped)])
25
+ @store = T.let(Store.new, Store)
19
26
  end
20
27
 
28
+ sig { void }
21
29
  def start
22
30
  $stderr.puts "Starting Ruby LSP..."
23
- @reader.read do |request|
24
- handle(request)
25
- end
31
+ @reader.read { |request| handle(request) }
26
32
  end
27
33
 
34
+ sig { params(blk: T.proc.bind(Handler).params(arg0: T.untyped).void).void }
28
35
  def config(&blk)
29
36
  instance_exec(&blk)
30
37
  end
31
38
 
32
39
  private
33
40
 
41
+ sig do
42
+ params(
43
+ msg: String,
44
+ blk: T.proc.bind(Handler).params(request: T::Hash[Symbol, T.untyped]).returns(T.untyped)
45
+ ).void
46
+ end
34
47
  def on(msg, &blk)
35
- @handlers[msg.to_s] = blk
48
+ @handlers[msg] = blk
36
49
  end
37
50
 
51
+ sig { params(request: T::Hash[Symbol, T.untyped]).void }
38
52
  def handle(request)
39
- result = @handlers[request[:method]]&.call(request)
40
- @writer.write(id: request[:id], result: result) if result
53
+ result = T.let(nil, T.untyped)
54
+ error = T.let(nil, T.nilable(StandardError))
55
+ handler = @handlers[request[:method]]
56
+
57
+ request_time = Benchmark.realtime do
58
+ if handler
59
+ begin
60
+ result = handler.call(request)
61
+ rescue StandardError => e
62
+ error = e
63
+ end
64
+
65
+ @writer.write(id: request[:id], result: result) unless result == VOID
66
+ end
67
+ end
68
+
69
+ @writer.write(method: "telemetry/event", params: telemetry_params(request, request_time, error))
41
70
  end
42
71
 
72
+ sig { void }
43
73
  def shutdown
44
74
  $stderr.puts "Shutting down Ruby LSP..."
45
75
  store.clear
46
76
  end
47
77
 
78
+ sig { params(enabled_features: T::Array[String]).returns(Interface::InitializeResult) }
48
79
  def respond_with_capabilities(enabled_features)
49
80
  document_symbol_provider = if enabled_features.include?("documentSymbols")
50
81
  Interface::DocumentSymbolClientCapabilities.new(
@@ -64,7 +95,7 @@ module RubyLsp
64
95
  document_selector: { scheme: "file", language: "ruby" },
65
96
  legend: Interface::SemanticTokensLegend.new(
66
97
  token_types: Requests::SemanticHighlighting::TOKEN_TYPES,
67
- token_modifiers: Requests::SemanticHighlighting::TOKEN_MODIFIERS
98
+ token_modifiers: Requests::SemanticHighlighting::TOKEN_MODIFIERS.keys
68
99
  ),
69
100
  range: false,
70
101
  full: {
@@ -76,43 +107,75 @@ module RubyLsp
76
107
  Interface::InitializeResult.new(
77
108
  capabilities: Interface::ServerCapabilities.new(
78
109
  text_document_sync: Interface::TextDocumentSyncOptions.new(
79
- change: Constant::TextDocumentSyncKind::FULL,
110
+ change: Constant::TextDocumentSyncKind::INCREMENTAL,
80
111
  open_close: true,
81
112
  ),
113
+ selection_range_provider: enabled_features.include?("selectionRanges"),
82
114
  document_symbol_provider: document_symbol_provider,
83
115
  folding_range_provider: folding_ranges_provider,
84
116
  semantic_tokens_provider: semantic_tokens_provider,
85
117
  document_formatting_provider: enabled_features.include?("formatting"),
118
+ document_highlight_provider: enabled_features.include?("documentHighlights"),
86
119
  code_action_provider: enabled_features.include?("codeActions")
87
120
  )
88
121
  )
89
122
  end
90
123
 
124
+ sig { params(uri: String).returns(T::Array[LanguageServer::Protocol::Interface::DocumentSymbol]) }
91
125
  def respond_with_document_symbol(uri)
92
126
  store.cache_fetch(uri, :document_symbol) do |document|
93
- RubyLsp::Requests::DocumentSymbol.run(document)
127
+ RubyLsp::Requests::DocumentSymbol.new(document).run
94
128
  end
95
129
  end
96
130
 
131
+ sig { params(uri: String).returns(T::Array[LanguageServer::Protocol::Interface::FoldingRange]) }
97
132
  def respond_with_folding_ranges(uri)
98
133
  store.cache_fetch(uri, :folding_ranges) do |document|
99
- Requests::FoldingRanges.run(document)
134
+ Requests::FoldingRanges.new(document).run
135
+ end
136
+ end
137
+
138
+ sig do
139
+ params(
140
+ uri: String,
141
+ positions: T::Array[Document::PositionShape]
142
+ ).returns(T::Array[T.nilable(RubyLsp::Requests::Support::SelectionRange)])
143
+ end
144
+ def respond_with_selection_ranges(uri, positions)
145
+ ranges = store.cache_fetch(uri, :selection_ranges) do |document|
146
+ Requests::SelectionRanges.new(document).run
147
+ end
148
+
149
+ # Per the selection range request spec (https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange),
150
+ # every position in the positions array should have an element at the same index in the response
151
+ # array. For positions without a valid selection range, the corresponding element in the response
152
+ # array will be nil.
153
+ positions.map do |position|
154
+ ranges.find do |range|
155
+ range.cover?(position)
156
+ end
100
157
  end
101
158
  end
102
159
 
160
+ sig { params(uri: String).returns(LanguageServer::Protocol::Interface::SemanticTokens) }
103
161
  def respond_with_semantic_highlighting(uri)
104
162
  store.cache_fetch(uri, :semantic_highlighting) do |document|
105
- Requests::SemanticHighlighting.run(document)
163
+ T.cast(
164
+ Requests::SemanticHighlighting.new(document, encoder: Requests::Support::SemanticTokenEncoder.new).run,
165
+ LanguageServer::Protocol::Interface::SemanticTokens
166
+ )
106
167
  end
107
168
  end
108
169
 
170
+ sig { params(uri: String).returns(T.nilable(T::Array[LanguageServer::Protocol::Interface::TextEdit])) }
109
171
  def respond_with_formatting(uri)
110
- Requests::Formatting.run(uri, store.get(uri))
172
+ Requests::Formatting.new(uri, store.get(uri)).run
111
173
  end
112
174
 
175
+ sig { params(uri: String).void }
113
176
  def send_diagnostics(uri)
114
177
  response = store.cache_fetch(uri, :diagnostics) do |document|
115
- Requests::Diagnostics.run(uri, document)
178
+ Requests::Diagnostics.new(uri, document).run
116
179
  end
117
180
 
118
181
  @writer.write(
@@ -124,10 +187,48 @@ module RubyLsp
124
187
  )
125
188
  end
126
189
 
190
+ sig do
191
+ params(uri: String, range: T::Range[Integer]).returns(T::Array[LanguageServer::Protocol::Interface::CodeAction])
192
+ end
127
193
  def respond_with_code_actions(uri, range)
128
194
  store.cache_fetch(uri, :code_actions) do |document|
129
- Requests::CodeActions.run(uri, document, range)
195
+ Requests::CodeActions.new(uri, document, range).run
196
+ end
197
+ end
198
+
199
+ sig do
200
+ params(
201
+ uri: String,
202
+ position: Document::PositionShape
203
+ ).returns(T::Array[LanguageServer::Protocol::Interface::DocumentHighlight])
204
+ end
205
+ def respond_with_document_highlight(uri, position)
206
+ Requests::DocumentHighlight.new(store.get(uri), position).run
207
+ end
208
+
209
+ sig do
210
+ params(
211
+ request: T::Hash[Symbol, T.untyped],
212
+ request_time: Float,
213
+ error: T.nilable(StandardError)
214
+ ).returns(T::Hash[Symbol, T.any(String, Float)])
215
+ end
216
+ def telemetry_params(request, request_time, error)
217
+ uri = request.dig(:params, :textDocument, :uri)
218
+
219
+ params = {
220
+ request: request[:method],
221
+ lspVersion: RubyLsp::VERSION,
222
+ requestTime: request_time,
223
+ }
224
+
225
+ if error
226
+ params[:errorClass] = error.class.name
227
+ params[:errorMessage] = error.message
130
228
  end
229
+
230
+ params[:uri] = uri if uri
231
+ params
131
232
  end
132
233
  end
133
234
  end
@@ -0,0 +1,7 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "syntax_tree"
6
+ require "ruby-lsp"
7
+ require "ruby_lsp/cli"
@@ -1,20 +1,34 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module RubyLsp
4
5
  module Requests
6
+ # :nodoc:
5
7
  class BaseRequest < SyntaxTree::Visitor
6
- def self.run(document)
7
- new(document).run
8
- end
8
+ extend T::Sig
9
+ extend T::Helpers
10
+
11
+ abstract!
9
12
 
13
+ sig { params(document: Document).void }
10
14
  def initialize(document)
11
15
  @document = document
12
16
 
13
17
  super()
14
18
  end
15
19
 
16
- def run
17
- raise NotImplementedError, "#{self.class}#run must be implemented"
20
+ sig { abstract.returns(Object) }
21
+ def run; end
22
+
23
+ sig { params(node: SyntaxTree::Node).returns(LanguageServer::Protocol::Interface::Range) }
24
+ def range_from_syntax_tree_node(node)
25
+ loc = node.location
26
+
27
+ LanguageServer::Protocol::Interface::Range.new(
28
+ start: LanguageServer::Protocol::Interface::Position.new(line: loc.start_line - 1,
29
+ character: loc.start_column),
30
+ end: LanguageServer::Protocol::Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
31
+ )
18
32
  end
19
33
  end
20
34
  end
@@ -1,24 +1,45 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module RubyLsp
4
5
  module Requests
5
- class CodeActions
6
- def self.run(uri, document, range)
7
- new(uri, document, range).run
8
- end
6
+ # The [code actions](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction)
7
+ # request informs the editor of RuboCop quick fixes that can be applied. These are accesible by hovering over a
8
+ # specific diagnostic.
9
+ #
10
+ # # Example
11
+ #
12
+ # ```ruby
13
+ # def say_hello
14
+ # puts "Hello" # --> code action: quick fix indentation
15
+ # end
16
+ # ```
17
+ class CodeActions < BaseRequest
18
+ extend T::Sig
9
19
 
20
+ sig do
21
+ params(
22
+ uri: String,
23
+ document: Document,
24
+ range: T::Range[Integer]
25
+ ).void
26
+ end
10
27
  def initialize(uri, document, range)
11
- @document = document
28
+ super(document)
29
+
12
30
  @uri = uri
13
31
  @range = range
14
32
  end
15
33
 
34
+ sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::CodeAction], Object)) }
16
35
  def run
17
- diagnostics = Diagnostics.run(@uri, @document)
18
- corrections = diagnostics.select { |diagnostic| diagnostic.correctable? && diagnostic.in_range?(@range) }
19
- return if corrections.empty?
36
+ diagnostics = Diagnostics.new(@uri, @document).run
37
+ corrections = diagnostics.select do |diagnostic|
38
+ diagnostic.correctable? && T.cast(diagnostic, Support::RuboCopDiagnostic).in_range?(@range)
39
+ end
40
+ return [] if corrections.empty?
20
41
 
21
- corrections.map!(&:to_lsp_code_action)
42
+ T.cast(corrections, T::Array[Support::RuboCopDiagnostic]).map!(&:to_lsp_code_action)
22
43
  end
23
44
  end
24
45
  end
@@ -1,96 +1,48 @@
1
+ # typed: strict
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module RubyLsp
4
5
  module Requests
6
+ # The
7
+ # [diagnostics](https://microsoft.github.io/language-server-protocol/specification#textDocument_publishDiagnostics)
8
+ # request informs the editor of RuboCop offenses for a given file.
9
+ #
10
+ # # Example
11
+ #
12
+ # ```ruby
13
+ # def say_hello
14
+ # puts "Hello" # --> diagnostics: incorrect indentantion
15
+ # end
16
+ # ```
5
17
  class Diagnostics < RuboCopRequest
6
- RUBOCOP_TO_LSP_SEVERITY = {
7
- convention: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
8
- info: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
9
- refactor: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION,
10
- warning: LanguageServer::Protocol::Constant::DiagnosticSeverity::WARNING,
11
- error: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
12
- fatal: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR,
13
- }.freeze
18
+ extend T::Sig
14
19
 
20
+ sig do
21
+ override.returns(
22
+ T.any(
23
+ T.all(T::Array[Support::RuboCopDiagnostic], Object),
24
+ T.all(T::Array[Support::SyntaxErrorDiagnostic], Object),
25
+ )
26
+ )
27
+ end
15
28
  def run
29
+ return syntax_error_diagnostics if @document.syntax_errors?
30
+
16
31
  super
17
32
 
18
33
  @diagnostics
19
34
  end
20
35
 
36
+ sig { params(_file: String, offenses: T::Array[RuboCop::Cop::Offense]).void }
21
37
  def file_finished(_file, offenses)
22
- @diagnostics = offenses.map { |offense| Diagnostic.new(offense, @uri) }
38
+ @diagnostics = offenses.map { |offense| Support::RuboCopDiagnostic.new(offense, @uri) }
23
39
  end
24
40
 
25
- class Diagnostic
26
- attr_reader :replacements
27
-
28
- def initialize(offense, uri)
29
- @offense = offense
30
- @uri = uri
31
- @replacements = offense.correctable? ? offense_replacements : []
32
- end
33
-
34
- def correctable?
35
- @offense.correctable?
36
- end
37
-
38
- def in_range?(range)
39
- range.cover?(@offense.line - 1)
40
- end
41
-
42
- def to_lsp_code_action
43
- LanguageServer::Protocol::Interface::CodeAction.new(
44
- title: "Autocorrect #{@offense.cop_name}",
45
- kind: LanguageServer::Protocol::Constant::CodeActionKind::QUICK_FIX,
46
- edit: LanguageServer::Protocol::Interface::WorkspaceEdit.new(
47
- document_changes: [
48
- LanguageServer::Protocol::Interface::TextDocumentEdit.new(
49
- text_document: LanguageServer::Protocol::Interface::OptionalVersionedTextDocumentIdentifier.new(
50
- uri: @uri,
51
- version: nil
52
- ),
53
- edits: @replacements
54
- ),
55
- ]
56
- ),
57
- is_preferred: true,
58
- )
59
- end
60
-
61
- def to_lsp_diagnostic
62
- LanguageServer::Protocol::Interface::Diagnostic.new(
63
- message: @offense.message,
64
- source: "RuboCop",
65
- code: @offense.cop_name,
66
- severity: RUBOCOP_TO_LSP_SEVERITY[@offense.severity.name],
67
- range: LanguageServer::Protocol::Interface::Range.new(
68
- start: LanguageServer::Protocol::Interface::Position.new(
69
- line: @offense.line - 1,
70
- character: @offense.column
71
- ),
72
- end: LanguageServer::Protocol::Interface::Position.new(
73
- line: @offense.last_line - 1,
74
- character: @offense.last_column
75
- )
76
- )
77
- )
78
- end
79
-
80
- private
41
+ private
81
42
 
82
- def offense_replacements
83
- @offense.corrector.as_replacements.map do |range, replacement|
84
- LanguageServer::Protocol::Interface::TextEdit.new(
85
- range: LanguageServer::Protocol::Interface::Range.new(
86
- start: LanguageServer::Protocol::Interface::Position.new(line: range.line - 1, character: range.column),
87
- end: LanguageServer::Protocol::Interface::Position.new(line: range.last_line - 1,
88
- character: range.last_column)
89
- ),
90
- new_text: replacement
91
- )
92
- end
93
- end
43
+ sig { returns(T::Array[Support::SyntaxErrorDiagnostic]) }
44
+ def syntax_error_diagnostics
45
+ @document.syntax_error_edits.map { |e| Support::SyntaxErrorDiagnostic.new(e) }
94
46
  end
95
47
  end
96
48
  end
@@ -0,0 +1,111 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # The [document highlight](https://microsoft.github.io/language-server-protocol/specification#textDocument_documentHighlight)
7
+ # informs the editor all relevant elements of the currently pointed item for highlighting. For example, when
8
+ # the cursor is on the `F` of the constant `FOO`, the editor should identify other occurences of `FOO`
9
+ # and highlight them.
10
+ #
11
+ # For writable elements like constants or variables, their read/write occurrences should be highlighted differently.
12
+ # This is achieved by sending different "kind" attributes to the editor (2 for read and 3 for write).
13
+ #
14
+ # # Example
15
+ #
16
+ # ```ruby
17
+ # FOO = 1 # should be highlighted as "write"
18
+ #
19
+ # def foo
20
+ # FOO # should be highlighted as "read"
21
+ # end
22
+ # ```
23
+ class DocumentHighlight < BaseRequest
24
+ extend T::Sig
25
+
26
+ VarNodes = T.type_alias do
27
+ T.any(
28
+ SyntaxTree::GVar,
29
+ SyntaxTree::Ident,
30
+ SyntaxTree::IVar,
31
+ SyntaxTree::Const,
32
+ SyntaxTree::CVar
33
+ )
34
+ end
35
+
36
+ sig { params(document: Document, position: Document::PositionShape).void }
37
+ def initialize(document, position)
38
+ @highlights = T.let([], T::Array[LanguageServer::Protocol::Interface::DocumentHighlight])
39
+ position = Document::Scanner.new(document.source).find_position(position)
40
+ @target = T.let(find(document.tree, position), T.nilable(VarNodes))
41
+
42
+ super(document)
43
+ end
44
+
45
+ sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::DocumentHighlight], Object)) }
46
+ def run
47
+ # no @target means the target is not highlightable
48
+ return [] unless @target
49
+
50
+ visit(@document.tree)
51
+ @highlights
52
+ end
53
+
54
+ sig { params(node: SyntaxTree::VarField).void }
55
+ def visit_var_field(node)
56
+ if matches_target?(node.value)
57
+ add_highlight(
58
+ node.value,
59
+ LanguageServer::Protocol::Constant::DocumentHighlightKind::WRITE
60
+ )
61
+ end
62
+
63
+ super
64
+ end
65
+
66
+ sig { params(node: SyntaxTree::VarRef).void }
67
+ def visit_var_ref(node)
68
+ if matches_target?(node.value)
69
+ add_highlight(
70
+ node.value,
71
+ LanguageServer::Protocol::Constant::DocumentHighlightKind::READ
72
+ )
73
+ end
74
+
75
+ super
76
+ end
77
+
78
+ private
79
+
80
+ sig { params(node: SyntaxTree::Node, position: Integer).returns(T.nilable(VarNodes)) }
81
+ def find(node, position)
82
+ matched =
83
+ node.child_nodes.compact.bsearch do |child|
84
+ if (child.location.start_char...child.location.end_char).cover?(position)
85
+ 0
86
+ else
87
+ position <=> child.location.start_char
88
+ end
89
+ end
90
+
91
+ case matched
92
+ when SyntaxTree::GVar, SyntaxTree::Ident, SyntaxTree::IVar, SyntaxTree::Const, SyntaxTree::CVar
93
+ matched
94
+ when SyntaxTree::Node
95
+ find(matched, position)
96
+ end
97
+ end
98
+
99
+ sig { params(node: SyntaxTree::Node).returns(T::Boolean) }
100
+ def matches_target?(node)
101
+ node.is_a?(@target.class) && T.cast(node, VarNodes).value == T.must(@target).value
102
+ end
103
+
104
+ sig { params(node: SyntaxTree::Node, kind: Integer).void }
105
+ def add_highlight(node, kind)
106
+ range = range_from_syntax_tree_node(node)
107
+ @highlights << LanguageServer::Protocol::Interface::DocumentHighlight.new(range: range, kind: kind)
108
+ end
109
+ end
110
+ end
111
+ end