ruby-lsp 0.0.1 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +7 -16
  3. data/.github/pull_request_template.md +15 -0
  4. data/.github/workflows/ci.yml +31 -0
  5. data/.github/workflows/publish_docs.yml +32 -0
  6. data/.gitignore +9 -12
  7. data/.rubocop.yml +20 -2
  8. data/.vscode/settings.json +5 -0
  9. data/CHANGELOG.md +29 -0
  10. data/Gemfile +8 -4
  11. data/Gemfile.lock +76 -14
  12. data/README.md +69 -2
  13. data/Rakefile +5 -0
  14. data/VERSION +1 -1
  15. data/bin/tapioca +29 -0
  16. data/bin/test +7 -1
  17. data/dev.yml +7 -7
  18. data/exe/ruby-lsp +19 -2
  19. data/lib/internal.rb +7 -0
  20. data/lib/ruby-lsp.rb +4 -1
  21. data/lib/ruby_lsp/cli.rb +88 -0
  22. data/lib/ruby_lsp/document.rb +113 -0
  23. data/lib/ruby_lsp/handler.rb +236 -0
  24. data/lib/ruby_lsp/requests/base_request.rb +33 -0
  25. data/lib/ruby_lsp/requests/code_actions.rb +37 -0
  26. data/lib/ruby_lsp/requests/diagnostics.rb +37 -0
  27. data/lib/ruby_lsp/requests/document_highlight.rb +96 -0
  28. data/lib/ruby_lsp/requests/document_symbol.rb +216 -0
  29. data/lib/ruby_lsp/requests/folding_ranges.rb +213 -0
  30. data/lib/ruby_lsp/requests/formatting.rb +52 -0
  31. data/lib/ruby_lsp/requests/rubocop_request.rb +50 -0
  32. data/lib/ruby_lsp/requests/selection_ranges.rb +103 -0
  33. data/lib/ruby_lsp/requests/semantic_highlighting.rb +112 -0
  34. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +88 -0
  35. data/lib/ruby_lsp/requests/support/selection_range.rb +17 -0
  36. data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +60 -0
  37. data/lib/ruby_lsp/requests/support/syntax_error_diagnostic.rb +27 -0
  38. data/lib/ruby_lsp/requests.rb +24 -0
  39. data/lib/ruby_lsp/store.rb +59 -0
  40. data/rakelib/check_docs.rake +56 -0
  41. data/ruby-lsp.gemspec +5 -1
  42. data/{shipit.yml → shipit.production.yml} +0 -0
  43. data/sorbet/config +4 -0
  44. data/sorbet/rbi/.rubocop.yml +8 -0
  45. data/sorbet/rbi/gems/ansi@1.5.0.rbi +338 -0
  46. data/sorbet/rbi/gems/ast@2.4.2.rbi +522 -0
  47. data/sorbet/rbi/gems/builder@3.2.4.rbi +418 -0
  48. data/sorbet/rbi/gems/coderay@1.1.3.rbi +8 -0
  49. data/sorbet/rbi/gems/debug@1.5.0.rbi +1273 -0
  50. data/sorbet/rbi/gems/diff-lcs@1.5.0.rbi +867 -0
  51. data/sorbet/rbi/gems/io-console@0.5.11.rbi +8 -0
  52. data/sorbet/rbi/gems/irb@1.4.1.rbi +376 -0
  53. data/sorbet/rbi/gems/language_server-protocol@3.16.0.3.rbi +7325 -0
  54. data/sorbet/rbi/gems/method_source@1.0.0.rbi +8 -0
  55. data/sorbet/rbi/gems/minitest-reporters@1.5.0.rbi +612 -0
  56. data/sorbet/rbi/gems/minitest@5.15.0.rbi +994 -0
  57. data/sorbet/rbi/gems/parallel@1.22.1.rbi +163 -0
  58. data/sorbet/rbi/gems/parser@3.1.2.0.rbi +3968 -0
  59. data/sorbet/rbi/gems/prettier_print@0.1.0.rbi +734 -0
  60. data/sorbet/rbi/gems/pry@0.14.1.rbi +8 -0
  61. data/sorbet/rbi/gems/rainbow@3.1.1.rbi +227 -0
  62. data/sorbet/rbi/gems/rake@13.0.6.rbi +1853 -0
  63. data/sorbet/rbi/gems/rbi@0.0.14.rbi +2337 -0
  64. data/sorbet/rbi/gems/regexp_parser@2.5.0.rbi +1854 -0
  65. data/sorbet/rbi/gems/reline@0.3.1.rbi +1274 -0
  66. data/sorbet/rbi/gems/rexml@3.2.5.rbi +3852 -0
  67. data/sorbet/rbi/gems/rubocop-ast@1.18.0.rbi +4180 -0
  68. data/sorbet/rbi/gems/rubocop-minitest@0.20.0.rbi +1369 -0
  69. data/sorbet/rbi/gems/rubocop-rake@0.6.0.rbi +246 -0
  70. data/sorbet/rbi/gems/rubocop-shopify@2.6.0.rbi +8 -0
  71. data/sorbet/rbi/gems/rubocop-sorbet@0.6.8.rbi +652 -0
  72. data/sorbet/rbi/gems/rubocop@1.30.0.rbi +36729 -0
  73. data/sorbet/rbi/gems/ruby-progressbar@1.11.0.rbi +732 -0
  74. data/sorbet/rbi/gems/spoom@1.1.11.rbi +1600 -0
  75. data/sorbet/rbi/gems/syntax_tree@2.7.1.rbi +6777 -0
  76. data/sorbet/rbi/gems/tapioca@0.8.1.rbi +1972 -0
  77. data/sorbet/rbi/gems/thor@1.2.1.rbi +2921 -0
  78. data/sorbet/rbi/gems/unicode-display_width@2.1.0.rbi +27 -0
  79. data/sorbet/rbi/gems/unparser@0.6.5.rbi +2789 -0
  80. data/sorbet/rbi/gems/webrick@1.7.0.rbi +1779 -0
  81. data/sorbet/rbi/gems/yard-sorbet@0.6.1.rbi +289 -0
  82. data/sorbet/rbi/gems/yard@0.9.27.rbi +13048 -0
  83. data/sorbet/rbi/shims/fiddle.rbi +4 -0
  84. data/sorbet/rbi/shims/hash.rbi +6 -0
  85. data/sorbet/rbi/shims/rdoc.rbi +4 -0
  86. data/sorbet/tapioca/config.yml +13 -0
  87. data/sorbet/tapioca/require.rb +7 -0
  88. metadata +119 -9
  89. data/.vscode/launch.json +0 -19
  90. data/bin/package_extension +0 -5
  91. data/bin/style +0 -10
  92. data/lib/ruby/lsp/cli.rb +0 -37
  93. data/lib/ruby/lsp.rb +0 -3
@@ -0,0 +1,113 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ class Document
6
+ extend T::Sig
7
+
8
+ PositionShape = T.type_alias { { line: Integer, character: Integer } }
9
+ RangeShape = T.type_alias { { start: PositionShape, end: PositionShape } }
10
+ EditShape = T.type_alias { { range: RangeShape, text: String } }
11
+
12
+ sig { returns(SyntaxTree::Node) }
13
+ attr_reader :tree
14
+
15
+ sig { returns(String) }
16
+ attr_reader :source
17
+
18
+ sig { returns(T::Array[EditShape]) }
19
+ attr_reader :syntax_error_edits
20
+
21
+ sig { params(source: String).void }
22
+ def initialize(source)
23
+ @tree = T.let(SyntaxTree.parse(source), SyntaxTree::Node)
24
+ @cache = T.let({}, T::Hash[Symbol, T.untyped])
25
+ @syntax_error_edits = T.let([], T::Array[EditShape])
26
+ @source = source
27
+ @parsable_source = T.let(source.dup, String)
28
+ end
29
+
30
+ sig { params(other: Document).returns(T::Boolean) }
31
+ def ==(other)
32
+ @source == other.source
33
+ end
34
+
35
+ sig { params(request_name: Symbol, block: T.proc.params(document: Document).returns(T.untyped)).returns(T.untyped) }
36
+ def cache_fetch(request_name, &block)
37
+ cached = @cache[request_name]
38
+ return cached if cached
39
+
40
+ result = block.call(self)
41
+ @cache[request_name] = result
42
+ result
43
+ end
44
+
45
+ sig { params(edits: T::Array[EditShape]).void }
46
+ def push_edits(edits)
47
+ # Apply the edits on the real source
48
+ edits.each { |edit| apply_edit(@source, edit[:range], edit[:text]) }
49
+
50
+ @cache.clear
51
+ @tree = SyntaxTree.parse(@source)
52
+ @syntax_error_edits.clear
53
+ @parsable_source = @source.dup
54
+ nil
55
+ rescue SyntaxTree::Parser::ParseError
56
+ update_parsable_source(edits)
57
+ end
58
+
59
+ sig { returns(T::Boolean) }
60
+ def syntax_errors?
61
+ @syntax_error_edits.any?
62
+ end
63
+
64
+ private
65
+
66
+ sig { params(edits: T::Array[EditShape]).void }
67
+ def update_parsable_source(edits)
68
+ # If the new edits caused a syntax error, make all edits blank spaces and line breaks to adjust the line and
69
+ # column numbers. This is attempt to make the document parsable while partial edits are being applied
70
+ edits.each do |edit|
71
+ @syntax_error_edits << edit
72
+ next if edit[:text].empty? # skip deletions, since they may have caused the syntax error
73
+
74
+ apply_edit(@parsable_source, edit[:range], edit[:text].gsub(/[^\r\n]/, " "))
75
+ end
76
+
77
+ @tree = SyntaxTree.parse(@parsable_source)
78
+ rescue SyntaxTree::Parser::ParseError
79
+ # If we can't parse the source even after emptying the edits, then just fallback to the previous source
80
+ end
81
+
82
+ sig { params(source: String, range: RangeShape, text: String).void }
83
+ def apply_edit(source, range, text)
84
+ scanner = Scanner.new(source)
85
+ start_position = scanner.find_position(range[:start])
86
+ end_position = scanner.find_position(range[:end])
87
+
88
+ source[start_position...end_position] = text
89
+ end
90
+
91
+ class Scanner
92
+ extend T::Sig
93
+
94
+ sig { params(source: String).void }
95
+ def initialize(source)
96
+ @current_line = T.let(0, Integer)
97
+ @pos = T.let(0, Integer)
98
+ @source = source
99
+ end
100
+
101
+ sig { params(position: PositionShape).returns(Integer) }
102
+ def find_position(position)
103
+ until @current_line == position[:line]
104
+ @pos += 1 until /\R/.match?(@source[@pos])
105
+ @pos += 1
106
+ @current_line += 1
107
+ end
108
+
109
+ @pos + position[:character]
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,236 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "ruby_lsp/requests"
5
+ require "ruby_lsp/store"
6
+ require "benchmark"
7
+
8
+ module RubyLsp
9
+ class Handler
10
+ extend T::Sig
11
+ VOID = T.let(Object.new.freeze, Object)
12
+
13
+ sig { returns(Store) }
14
+ attr_reader :store
15
+
16
+ Interface = LanguageServer::Protocol::Interface
17
+ Constant = LanguageServer::Protocol::Constant
18
+ Transport = LanguageServer::Protocol::Transport
19
+
20
+ sig { void }
21
+ def initialize
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)
26
+ end
27
+
28
+ sig { void }
29
+ def start
30
+ $stderr.puts "Starting Ruby LSP..."
31
+ @reader.read do |request|
32
+ with_telemetry(request) { handle(request) }
33
+ end
34
+ end
35
+
36
+ sig { params(blk: T.proc.bind(Handler).params(arg0: T.untyped).void).void }
37
+ def config(&blk)
38
+ instance_exec(&blk)
39
+ end
40
+
41
+ private
42
+
43
+ sig do
44
+ params(
45
+ msg: String,
46
+ blk: T.proc.bind(Handler).params(request: T::Hash[Symbol, T.untyped]).returns(T.untyped)
47
+ ).void
48
+ end
49
+ def on(msg, &blk)
50
+ @handlers[msg] = blk
51
+ end
52
+
53
+ sig { params(request: T::Hash[Symbol, T.untyped]).void }
54
+ def handle(request)
55
+ handler = @handlers[request[:method]]
56
+ return unless handler
57
+
58
+ result = handler.call(request)
59
+ @writer.write(id: request[:id], result: result) unless result == VOID
60
+ end
61
+
62
+ sig { void }
63
+ def shutdown
64
+ $stderr.puts "Shutting down Ruby LSP..."
65
+ store.clear
66
+ end
67
+
68
+ sig { params(enabled_features: T::Array[String]).returns(Interface::InitializeResult) }
69
+ def respond_with_capabilities(enabled_features)
70
+ document_symbol_provider = if enabled_features.include?("documentSymbols")
71
+ Interface::DocumentSymbolClientCapabilities.new(
72
+ hierarchical_document_symbol_support: true,
73
+ symbol_kind: {
74
+ value_set: Requests::DocumentSymbol::SYMBOL_KIND.values,
75
+ }
76
+ )
77
+ end
78
+
79
+ folding_ranges_provider = if enabled_features.include?("foldingRanges")
80
+ Interface::FoldingRangeClientCapabilities.new(line_folding_only: true)
81
+ end
82
+
83
+ semantic_tokens_provider = if enabled_features.include?("semanticHighlighting")
84
+ Interface::SemanticTokensRegistrationOptions.new(
85
+ document_selector: { scheme: "file", language: "ruby" },
86
+ legend: Interface::SemanticTokensLegend.new(
87
+ token_types: Requests::SemanticHighlighting::TOKEN_TYPES,
88
+ token_modifiers: Requests::SemanticHighlighting::TOKEN_MODIFIERS.keys
89
+ ),
90
+ range: false,
91
+ full: {
92
+ delta: true,
93
+ }
94
+ )
95
+ end
96
+
97
+ Interface::InitializeResult.new(
98
+ capabilities: Interface::ServerCapabilities.new(
99
+ text_document_sync: Interface::TextDocumentSyncOptions.new(
100
+ change: Constant::TextDocumentSyncKind::INCREMENTAL,
101
+ open_close: true,
102
+ ),
103
+ selection_range_provider: enabled_features.include?("selectionRanges"),
104
+ document_symbol_provider: document_symbol_provider,
105
+ folding_range_provider: folding_ranges_provider,
106
+ semantic_tokens_provider: semantic_tokens_provider,
107
+ document_formatting_provider: enabled_features.include?("formatting"),
108
+ document_highlight_provider: enabled_features.include?("documentHighlights"),
109
+ code_action_provider: enabled_features.include?("codeActions")
110
+ )
111
+ )
112
+ end
113
+
114
+ sig { params(uri: String).returns(T::Array[LanguageServer::Protocol::Interface::DocumentSymbol]) }
115
+ def respond_with_document_symbol(uri)
116
+ store.cache_fetch(uri, :document_symbol) do |document|
117
+ RubyLsp::Requests::DocumentSymbol.run(document)
118
+ end
119
+ end
120
+
121
+ sig { params(uri: String).returns(T::Array[LanguageServer::Protocol::Interface::FoldingRange]) }
122
+ def respond_with_folding_ranges(uri)
123
+ store.cache_fetch(uri, :folding_ranges) do |document|
124
+ Requests::FoldingRanges.run(document)
125
+ end
126
+ end
127
+
128
+ sig do
129
+ params(
130
+ uri: String,
131
+ positions: T::Array[Document::PositionShape]
132
+ ).returns(T::Array[RubyLsp::Requests::Support::SelectionRange])
133
+ end
134
+ def respond_with_selection_ranges(uri, positions)
135
+ ranges = store.cache_fetch(uri, :selection_ranges) do |document|
136
+ Requests::SelectionRanges.run(document)
137
+ end
138
+
139
+ # Per the selection range request spec (https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange),
140
+ # every position in the positions array should have an element at the same index in the response
141
+ # array. For positions without a valid selection range, the corresponding element in the response
142
+ # array will be nil.
143
+ positions.map do |position|
144
+ ranges.find do |range|
145
+ range.cover?(position)
146
+ end
147
+ end
148
+ end
149
+
150
+ sig { params(uri: String).returns(LanguageServer::Protocol::Interface::SemanticTokens) }
151
+ def respond_with_semantic_highlighting(uri)
152
+ store.cache_fetch(uri, :semantic_highlighting) do |document|
153
+ Requests::SemanticHighlighting.new(document, encoder: Requests::Support::SemanticTokenEncoder.new).run
154
+ end
155
+ end
156
+
157
+ sig { params(uri: String).returns(T::Array[LanguageServer::Protocol::Interface::TextEdit]) }
158
+ def respond_with_formatting(uri)
159
+ Requests::Formatting.run(uri, store.get(uri))
160
+ end
161
+
162
+ sig { params(uri: String).void }
163
+ def send_diagnostics(uri)
164
+ response = store.cache_fetch(uri, :diagnostics) do |document|
165
+ Requests::Diagnostics.run(uri, document)
166
+ end
167
+
168
+ @writer.write(
169
+ method: "textDocument/publishDiagnostics",
170
+ params: Interface::PublishDiagnosticsParams.new(
171
+ uri: uri,
172
+ diagnostics: response.map(&:to_lsp_diagnostic)
173
+ )
174
+ )
175
+ end
176
+
177
+ sig do
178
+ params(uri: String, range: T::Range[Integer]).returns(T::Array[LanguageServer::Protocol::Interface::Diagnostic])
179
+ end
180
+ def respond_with_code_actions(uri, range)
181
+ store.cache_fetch(uri, :code_actions) do |document|
182
+ Requests::CodeActions.run(uri, document, range)
183
+ end
184
+ end
185
+
186
+ sig do
187
+ params(
188
+ uri: String,
189
+ position: Document::PositionShape
190
+ ).returns(T::Array[LanguageServer::Protocol::Interface::DocumentHighlight])
191
+ end
192
+ def respond_with_document_highlight(uri, position)
193
+ Requests::DocumentHighlight.run(store.get(uri), position)
194
+ end
195
+
196
+ sig { params(request: T::Hash[Symbol, T.untyped], block: T.proc.void).returns(T.untyped) }
197
+ def with_telemetry(request, &block)
198
+ result = T.let(nil, T.untyped)
199
+ error = T.let(nil, T.nilable(StandardError))
200
+
201
+ request_time = Benchmark.realtime do
202
+ result = block.call
203
+ rescue StandardError => e
204
+ error = e
205
+ end
206
+
207
+ @writer.write(method: "telemetry/event", params: telemetry_params(request, request_time, error))
208
+ result
209
+ end
210
+
211
+ sig do
212
+ params(
213
+ request: T::Hash[Symbol, T.untyped],
214
+ request_time: Float,
215
+ error: T.nilable(StandardError)
216
+ ).returns(T::Hash[Symbol, T.any(String, Float)])
217
+ end
218
+ def telemetry_params(request, request_time, error)
219
+ uri = request.dig(:params, :textDocument, :uri)
220
+
221
+ params = {
222
+ request: request[:method],
223
+ lspVersion: RubyLsp::VERSION,
224
+ requestTime: request_time,
225
+ }
226
+
227
+ if error
228
+ params[:errorClass] = error.class.name
229
+ params[:errorMessage] = error.message
230
+ end
231
+
232
+ params[:uri] = uri if uri
233
+ params
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,33 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # :nodoc:
7
+ class BaseRequest < SyntaxTree::Visitor
8
+ def self.run(document)
9
+ new(document).run
10
+ end
11
+
12
+ def initialize(document)
13
+ @document = document
14
+
15
+ super()
16
+ end
17
+
18
+ def run
19
+ raise NotImplementedError, "#{self.class}#run must be implemented"
20
+ end
21
+
22
+ def range_from_syntax_tree_node(node)
23
+ loc = node.location
24
+
25
+ LanguageServer::Protocol::Interface::Range.new(
26
+ start: LanguageServer::Protocol::Interface::Position.new(line: loc.start_line - 1,
27
+ character: loc.start_column),
28
+ end: LanguageServer::Protocol::Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
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
18
+ def self.run(uri, document, range)
19
+ new(uri, document, range).run
20
+ end
21
+
22
+ def initialize(uri, document, range)
23
+ @document = document
24
+ @uri = uri
25
+ @range = range
26
+ end
27
+
28
+ def run
29
+ diagnostics = Diagnostics.run(@uri, @document)
30
+ corrections = diagnostics.select { |diagnostic| diagnostic.correctable? && diagnostic.in_range?(@range) }
31
+ return [] if corrections.empty?
32
+
33
+ corrections.map!(&:to_lsp_code_action)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
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
+ # ```
17
+ class Diagnostics < RuboCopRequest
18
+ def run
19
+ return syntax_error_diagnostics if @document.syntax_errors?
20
+
21
+ super
22
+
23
+ @diagnostics
24
+ end
25
+
26
+ def file_finished(_file, offenses)
27
+ @diagnostics = offenses.map { |offense| Support::RuboCopDiagnostic.new(offense, @uri) }
28
+ end
29
+
30
+ private
31
+
32
+ def syntax_error_diagnostics
33
+ @document.syntax_error_edits.map { |e| Support::SyntaxErrorDiagnostic.new(e) }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,96 @@
1
+ # typed: true
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
+ def self.run(document, position)
25
+ new(document, position).run
26
+ end
27
+
28
+ def initialize(document, position)
29
+ @highlights = []
30
+ position = Document::Scanner.new(document.source).find_position(position)
31
+ @target = find(document.tree, position)
32
+
33
+ super(document)
34
+ end
35
+
36
+ def run
37
+ # no @target means the target is not highlightable
38
+ return [] unless @target
39
+
40
+ visit(@document.tree)
41
+ @highlights
42
+ end
43
+
44
+ def visit_var_field(node)
45
+ if matches_target?(node.value)
46
+ add_highlight(
47
+ node.value,
48
+ LanguageServer::Protocol::Constant::DocumentHighlightKind::WRITE
49
+ )
50
+ end
51
+
52
+ super
53
+ end
54
+
55
+ def visit_var_ref(node)
56
+ if matches_target?(node.value)
57
+ add_highlight(
58
+ node.value,
59
+ LanguageServer::Protocol::Constant::DocumentHighlightKind::READ
60
+ )
61
+ end
62
+
63
+ super
64
+ end
65
+
66
+ private
67
+
68
+ def find(node, position)
69
+ matched =
70
+ node.child_nodes.compact.bsearch do |child|
71
+ if (child.location.start_char...child.location.end_char).cover?(position)
72
+ 0
73
+ else
74
+ position <=> child.location.start_char
75
+ end
76
+ end
77
+
78
+ case matched
79
+ when SyntaxTree::GVar, SyntaxTree::Ident, SyntaxTree::IVar, SyntaxTree::Const, SyntaxTree::CVar
80
+ matched
81
+ when SyntaxTree::Node
82
+ find(matched, position)
83
+ end
84
+ end
85
+
86
+ def matches_target?(node)
87
+ node.is_a?(@target.class) && node.value == @target.value
88
+ end
89
+
90
+ def add_highlight(node, kind)
91
+ range = range_from_syntax_tree_node(node)
92
+ @highlights << LanguageServer::Protocol::Interface::DocumentHighlight.new(range: range, kind: kind)
93
+ end
94
+ end
95
+ end
96
+ end