ruby-lsp 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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