ruby-lsp 0.0.1 → 0.0.4

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 (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