-
Notifications
You must be signed in to change notification settings - Fork 193
/
Copy pathsemantic_highlighting.rb
129 lines (109 loc) · 5.74 KB
/
semantic_highlighting.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# typed: strict
# frozen_string_literal: true
require "ruby_lsp/listeners/semantic_highlighting"
module RubyLsp
module Requests
# The [semantic
# highlighting](https://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.
class SemanticHighlighting < Request
class << self
#: -> Interface::SemanticTokensRegistrationOptions
def provider
Interface::SemanticTokensRegistrationOptions.new(
document_selector: nil,
legend: Interface::SemanticTokensLegend.new(
token_types: ResponseBuilders::SemanticHighlighting::TOKEN_TYPES.keys,
token_modifiers: ResponseBuilders::SemanticHighlighting::TOKEN_MODIFIERS.keys,
),
range: true,
full: { delta: true },
)
end
# The compute_delta method receives the current semantic tokens and the previous semantic tokens and then tries
# to compute the smallest possible semantic token edit that will turn previous into current
#: (Array[Integer] current_tokens, Array[Integer] previous_tokens, String result_id) -> Interface::SemanticTokensDelta
def compute_delta(current_tokens, previous_tokens, result_id)
# Find the index of the first token that is different between the two sets of tokens
first_different_position = current_tokens.zip(previous_tokens).find_index { |new, old| new != old }
# When deleting a token from the end, the first_different_position will be nil, but since we're removing at
# the end, then we have to initialize it to the length of the current tokens after the deletion
if !first_different_position && current_tokens.length < previous_tokens.length
first_different_position = current_tokens.length
end
unless first_different_position
return Interface::SemanticTokensDelta.new(result_id: result_id, edits: [])
end
# Filter the tokens based on the first different position. This must happen at this stage, before we try to
# find the next position from the end or else we risk confusing sets of token that may have different lengths,
# but end with the exact same token
old_tokens = T.must(previous_tokens[first_different_position...])
new_tokens = T.must(current_tokens[first_different_position...])
# Then search from the end to find the first token that doesn't match. Since the user is normally editing the
# middle of the file, this will minimize the number of edits since the end of the token array has not changed
first_different_token_from_end = new_tokens.reverse.zip(old_tokens.reverse).find_index do |new, old|
new != old
end || 0
# Filter the old and new tokens to only the section that will be replaced/inserted/deleted
old_tokens = T.must(old_tokens[...old_tokens.length - first_different_token_from_end])
new_tokens = T.must(new_tokens[...new_tokens.length - first_different_token_from_end])
# And we send back a single edit, replacing an entire section for the new tokens
Interface::SemanticTokensDelta.new(
result_id: result_id,
edits: [{ start: first_different_position, deleteCount: old_tokens.length, data: new_tokens }],
)
end
#: -> Integer
def next_result_id
@mutex.synchronize do
@result_id += 1
end
end
end
@result_id = 0 #: Integer
@mutex = Mutex.new #: Mutex
#: (GlobalState global_state, Prism::Dispatcher dispatcher, (RubyDocument | ERBDocument) document, String? previous_result_id, ?range: T::Range[Integer]?) -> void
def initialize(global_state, dispatcher, document, previous_result_id, range: nil)
super()
@document = document
@previous_result_id = previous_result_id
@range = range
@result_id = SemanticHighlighting.next_result_id.to_s #: String
@response_builder = ResponseBuilders::SemanticHighlighting
.new(document.code_units_cache) #: ResponseBuilders::SemanticHighlighting
Listeners::SemanticHighlighting.new(dispatcher, @response_builder)
Addon.addons.each do |addon|
Addon.notify(addon, :create_semantic_highlighting_listener, @response_builder, dispatcher)
end
end
# @override
#: -> (Interface::SemanticTokens | Interface::SemanticTokensDelta)
def perform
previous_tokens = @document.semantic_tokens
tokens = @response_builder.response
encoded_tokens = ResponseBuilders::SemanticHighlighting::SemanticTokenEncoder.new.encode(tokens)
full_response = Interface::SemanticTokens.new(result_id: @result_id, data: encoded_tokens)
@document.semantic_tokens = full_response
if @range
tokens_within_range = tokens.select { |token| @range.cover?(token.start_line - 1) }
return Interface::SemanticTokens.new(
result_id: @result_id,
data: ResponseBuilders::SemanticHighlighting::SemanticTokenEncoder.new.encode(tokens_within_range),
)
end
# Semantic tokens full delta
if @previous_result_id
response = if previous_tokens.is_a?(Interface::SemanticTokens) &&
previous_tokens.result_id == @previous_result_id
Requests::SemanticHighlighting.compute_delta(encoded_tokens, previous_tokens.data, @result_id)
else
full_response
end
return response
end
# Semantic tokens full
full_response
end
end
end
end