Class: RubyLsp::Requests::SemanticHighlighting

Inherits:
Listener
  • Object
show all
Extended by:
T::Generic, T::Sig
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

ResponseType =
type_member { { fixed: T::Array[SemanticToken] } }
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 Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from Listener

add_listener, listeners

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

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

Constructor Details

#initialize(emitter, message_queue, range: nil) ⇒ SemanticHighlighting

Returns a new instance of SemanticHighlighting.



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 117

def initialize(emitter, message_queue, range: nil)
  super(emitter, message_queue)

  @response = T.let([], ResponseType)
  @range = range
  @special_methods = T.let(nil, T.nilable(T::Array[String]))

  emitter.register(
    self,
    :after_binary,
    :on_block_var,
    :on_call,
    :on_class,
    :on_command,
    :on_command_call,
    :on_const,
    :on_def,
    :on_field,
    :on_kw,
    :on_lambda_var,
    :on_module,
    :on_params,
    :on_var_field,
    :on_var_ref,
    :on_vcall,
  )
end

Instance Attribute Details

#responseObject (readonly)

Returns the value of attribute response.



108
109
110
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 108

def response
  @response
end

Instance Method Details

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



327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 327

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

#after_binary(node) ⇒ Object



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

def after_binary(node)
  # 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 = @emitter.current_scope.find_local(name)
    next unless local

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

#on_block_var(node) ⇒ Object



254
255
256
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 254

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

#on_call(node) ⇒ Object



146
147
148
149
150
151
152
153
154
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 146

def on_call(node)
  return unless visible?(node, @range)

  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
end

#on_class(node) ⇒ Object



310
311
312
313
314
315
316
317
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 310

def on_class(node)
  return unless visible?(node, @range)

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

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

#on_command(node) ⇒ Object



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

def on_command(node)
  return unless visible?(node, @range)

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

#on_command_call(node) ⇒ Object



164
165
166
167
168
169
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 164

def on_command_call(node)
  return unless visible?(node, @range)

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

#on_const(node) ⇒ Object



172
173
174
175
176
177
178
179
180
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 172

def on_const(node)
  return unless visible?(node, @range)
  # When finding a module or class definition, we will have already pushed a token related to this constant. We
  # need to look at the previous two tokens and if they match this locatione exactly, avoid pushing another token
  # on top of the previous one
  return if @response.last(2).any? { |token| token.location == node.location }

  add_token(node.location, :namespace)
end

#on_def(node) ⇒ Object



183
184
185
186
187
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 183

def on_def(node)
  return unless visible?(node, @range)

  add_token(node.name.location, :method, [:declaration])
end

#on_field(node) ⇒ Object



220
221
222
223
224
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 220

def on_field(node)
  return unless visible?(node, @range)

  add_token(node.name.location, :method)
end

#on_kw(node) ⇒ Object



190
191
192
193
194
195
196
197
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 190

def on_kw(node)
  return unless visible?(node, @range)

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

#on_lambda_var(node) ⇒ Object



260
261
262
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 260

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

#on_module(node) ⇒ Object



320
321
322
323
324
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 320

def on_module(node)
  return unless visible?(node, @range)

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

#on_params(node) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 200

def on_params(node)
  return 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
end

#on_var_field(node) ⇒ Object



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

def on_var_field(node)
  return unless visible?(node, @range)

  value = node.value

  case value
  when SyntaxTree::Ident
    type = type_for_local(value)
    add_token(value.location, type)
  end
end

#on_var_ref(node) ⇒ Object



240
241
242
243
244
245
246
247
248
249
250
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 240

def on_var_ref(node)
  return unless visible?(node, @range)

  value = node.value

  case value
  when SyntaxTree::Ident
    type = type_for_local(value)
    add_token(value.location, type)
  end
end

#on_vcall(node) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/ruby_lsp/requests/semantic_highlighting.rb', line 265

def on_vcall(node)
  return 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 = @emitter.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