Class: RubyLsp::Requests::SemanticHighlighting

Inherits:
BaseRequest
  • Object
show all
Extended by:
T::Sig
Includes:
SyntaxTree::WithScope
Defined in:
lib/ruby_lsp/requests/semantic_highlighting.rb

Overview

![Semantic highlighting demo](../../semantic_highlighting.gif)

The [semantic highlighting](microsoft.github.io/language-server-protocol/specification#textDocument_semanticTokens) request informs the editor of the correct token types to provide consistent and accurate highlighting for themes.

# Example

“‘ruby def foo

var = 1 # --> semantic highlighting: local variable
some_invocation # --> semantic highlighting: method invocation
var # --> semantic highlighting: local variable

end “‘

Defined Under Namespace

Classes: SemanticToken

Constant Summary collapse

TOKEN_TYPES =
T.let(
  {
    namespace: 0,
    type: 1,
    class: 2,
    enum: 3,
    interface: 4,
    struct: 5,
    typeParameter: 6,
    parameter: 7,
    variable: 8,
    property: 9,
    enumMember: 10,
    event: 11,
    function: 12,
    method: 13,
    macro: 14,
    keyword: 15,
    modifier: 16,
    comment: 17,
    string: 18,
    number: 19,
    regexp: 20,
    operator: 21,
    decorator: 22,
  }.freeze,
  T::Hash[Symbol, Integer],
)
TOKEN_MODIFIERS =
T.let(
  {
    declaration: 0,
    definition: 1,
    readonly: 2,
    static: 3,
    deprecated: 4,
    abstract: 5,
    async: 6,
    modification: 7,
    documentation: 8,
    default_library: 9,
  }.freeze,
  T::Hash[Symbol, Integer],
)
SPECIAL_RUBY_METHODS =
T.let(
  [
    Module.instance_methods(false),
    Kernel.instance_methods(false),
    Kernel.methods(false),
    Bundler::Dsl.instance_methods(false),
    Module.private_instance_methods(false),
  ].flatten.map(&:to_s),
  T::Array[String],
)

Instance Method Summary collapse

Methods inherited from BaseRequest

#visit_all

Methods included from RubyLsp::Requests::Support::Common

#create_code_lens, #full_constant_name, #range_from_syntax_tree_node, #visible?

Constructor Details

#initialize(document, range: nil, encoder: nil) ⇒ SemanticHighlighting



112
113
114
115
116
117
118
119
120
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 112

def initialize(document, range: nil, encoder: nil)
  super(document)

  @encoder = encoder
  @tokens = T.let([], T::Array[SemanticToken])
  @tree = T.let(T.must(document.tree), SyntaxTree::Node)
  @range = range
  @special_methods = T.let(nil, T.nilable(T::Array[String]))
end

Instance Method Details

#add_token(location, type, modifiers = []) ⇒ Object



355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 355

def add_token(location, type, modifiers = [])
  length = location.end_char - location.start_char
  modifiers_indices = modifiers.filter_map { |modifier| TOKEN_MODIFIERS[modifier] }
  @tokens.push(
    SemanticToken.new(
      location: location,
      length: length,
      type: T.must(TOKEN_TYPES[type]),
      modifier: modifiers_indices,
    ),
  )
end

#runObject



130
131
132
133
134
135
136
137
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 130

def run
  return @tokens unless @document.parsed?

  visit(@tree)
  return @tokens unless @encoder

  @encoder.encode(@tokens)
end

#visit_binary(node) ⇒ Object



308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 308

def visit_binary(node)
  # It's important to visit the regexp first in the WithScope module
  super

  # You can only capture local variables with regexp by using the =~ operator
  return unless node.operator == :=~

  left = node.left
  # The regexp needs to be on the left hand side of the =~ for local variable capture
  return unless left.is_a?(SyntaxTree::RegexpLiteral)

  parts = left.parts
  return unless parts.one?

  content = parts.first
  return unless content.is_a?(SyntaxTree::TStringContent)

  # For each capture name we find in the regexp, look for a local in the current_scope
  Regexp.new(content.value, Regexp::FIXEDENCODING).names.each do |name|
    local = current_scope.find_local(name)
    next unless local

    local.definitions.each { |definition| add_token(definition, :variable) }
  end
end

#visit_block_var(node) ⇒ Object



274
275
276
277
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 274

def visit_block_var(node)
  node.locals.each { |local| add_token(local.location, :variable) }
  super
end

#visit_call(node) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 140

def visit_call(node)
  return super unless visible?(node, @range)

  visit(node.receiver)

  message = node.message
  if !message.is_a?(Symbol) && !special_method?(message.value)
    type = Support::Sorbet.annotation?(node) ? :type : :method

    add_token(message.location, type)
  end

  visit(node.arguments)
end

#visit_class(node) ⇒ Object



335
336
337
338
339
340
341
342
343
344
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 335

def visit_class(node)
  return super unless visible?(node, @range)

  add_token(node.constant.location, :class, [:declaration])

  superclass = node.superclass
  add_token(superclass.location, :class) if superclass

  visit(node.bodystmt)
end

#visit_command(node) ⇒ Object



156
157
158
159
160
161
162
163
164
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 156

def visit_command(node)
  return super unless visible?(node, @range)

  unless special_method?(node.message.value)
    add_token(node.message.location, :method)
  end
  visit(node.arguments)
  visit(node.block)
end

#visit_command_call(node) ⇒ Object



167
168
169
170
171
172
173
174
175
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 167

def visit_command_call(node)
  return super unless visible?(node, @range)

  visit(node.receiver)
  message = node.message
  add_token(message.location, :method) unless message.is_a?(Symbol)
  visit(node.arguments)
  visit(node.block)
end

#visit_const(node) ⇒ Object



178
179
180
181
182
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 178

def visit_const(node)
  return super unless visible?(node, @range)

  add_token(node.location, :namespace)
end

#visit_def(node) ⇒ Object



185
186
187
188
189
190
191
192
193
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 185

def visit_def(node)
  return super unless visible?(node, @range)

  add_token(node.name.location, :method, [:declaration])
  visit(node.params)
  visit(node.bodystmt)
  visit(node.target) if node.target
  visit(node.operator) if node.operator
end

#visit_field(node) ⇒ Object



228
229
230
231
232
233
234
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 228

def visit_field(node)
  return super unless visible?(node, @range)

  add_token(node.name.location, :method)

  super
end

#visit_kw(node) ⇒ Object



196
197
198
199
200
201
202
203
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 196

def visit_kw(node)
  return super unless visible?(node, @range)

  case node.value
  when "self"
    add_token(node.location, :variable, [:default_library])
  end
end

#visit_lambda_var(node) ⇒ Object



281
282
283
284
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 281

def visit_lambda_var(node)
  node.locals.each { |local| add_token(local.location, :variable) }
  super
end

#visit_module(node) ⇒ Object



347
348
349
350
351
352
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 347

def visit_module(node)
  return super unless visible?(node, @range)

  add_token(node.constant.location, :class, [:declaration])
  visit(node.bodystmt)
end

#visit_params(node) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 206

def visit_params(node)
  return super unless visible?(node, @range)

  node.keywords.each do |keyword, *|
    location = keyword.location
    add_token(location_without_colon(location), :parameter)
  end

  node.requireds.each do |required|
    add_token(required.location, :parameter)
  end

  rest = node.keyword_rest
  if rest && !rest.is_a?(SyntaxTree::ArgsForward) && !rest.is_a?(Symbol)
    name = rest.name
    add_token(name.location, :parameter) if name
  end

  super
end

#visit_var_field(node) ⇒ Object



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 237

def visit_var_field(node)
  return super unless visible?(node, @range)

  value = node.value

  case value
  when SyntaxTree::Ident
    type = type_for_local(value)
    add_token(value.location, type)
  when Symbol
    # do nothing
  else
    visit(value)
  end

  super
end

#visit_var_ref(node) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 256

def visit_var_ref(node)
  return super unless visible?(node, @range)

  value = node.value

  case value
  when SyntaxTree::Ident
    type = type_for_local(value)
    add_token(value.location, type)
  when Symbol
    # do nothing
  else
    visit(value)
  end
end

#visit_vcall(node) ⇒ Object



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 287

def visit_vcall(node)
  return super unless visible?(node, @range)

  # A VCall may exist as a local in the current_scope. This happens when used named capture groups in a regexp
  ident = node.value
  value = ident.value
  local = current_scope.find_local(value)
  return if local.nil? && special_method?(value)

  type = if local
    :variable
  elsif Support::Sorbet.annotation?(node)
    :type
  else
    :method
  end

  add_token(node.value.location, type)
end