Class: Ibex::LSP::SymbolIndex

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/lsp/symbol_index.rb,
sig/ibex/lsp/symbol_index.rbs

Overview

Answers source navigation and guarded workspace rename operations.

Constant Summary collapse

IDENTIFIER =

Signature:

  • Regexp

Returns:

  • (Regexp)
/\A[A-Za-z_][A-Za-z0-9_]*\z/
RESERVED =

Returns:

  • (Array[String])
%w[
  class fragment include token prechigh preclow left right nonassoc options expect start
  recover sync on_error_reduce convert display type pragma extended rule end
  separated_list separated_nonempty_list
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(store, path) ⇒ SymbolIndex

Returns a new instance of SymbolIndex.

RBS:

  • (DocumentStore store, String path) -> void

Parameters:



15
16
17
18
19
20
21
22
23
24
# File 'lib/ibex/lsp/symbol_index.rb', line 15

def initialize(store, path)
  @store = store
  @path = path
  files = store.files_for(path)
  @occurrences, @documents = SymbolIndexBuilder.new(store, files).build
  @snapshot_state = files.to_h do |file|
    snapshot = store.snapshot_for(file)
    [file, snapshot && [snapshot.fetch(:version), snapshot.fetch(:source)]]
  end #: Hash[String, Array[untyped]?]
end

Instance Method Details

#changed_sources(entries, new_name) ⇒ Hash[String, String]

RBS:

  • (Array[SymbolOccurrence] entries, String new_name) -> Hash[String, String]

Parameters:

Returns:

  • (Hash[String, String])


195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/ibex/lsp/symbol_index.rb', line 195

def changed_sources(entries, new_name)
  entries.group_by(&:path).to_h do |path, path_entries|
    document = @documents.fetch(path)
    source = document.source.dup
    path_entries.sort_by { |entry| -entry.span.start_byte }.each do |entry|
      prefix = source.byteslice(0, entry.span.start_byte) || ""
      suffix = source.byteslice(entry.span.end_byte, source.bytesize - entry.span.end_byte) || ""
      source = prefix + new_name + suffix
    end
    [path, source]
  end
end

#collision_scope?(candidate, occurrence) ⇒ Boolean

RBS:

  • (SymbolOccurrence candidate, SymbolOccurrence occurrence) -> bool

Parameters:

Returns:

  • (Boolean)


160
161
162
163
164
# File 'lib/ibex/lsp/symbol_index.rb', line 160

def collision_scope?(candidate, occurrence)
  return candidate.key.take(3) == occurrence.key.take(3) if occurrence.kind == :parameter

  candidate.kind != :parameter
end

#definition(path, position) ⇒ Array[Hash[String, untyped]]

RBS:

  • (String path, Hash[String, untyped] position) -> Array[Hash[String, untyped]]

Parameters:

  • path (String)
  • position (Hash[String, untyped])

Returns:

  • (Array[Hash[String, untyped]])


27
28
29
30
31
32
33
# File 'lib/ibex/lsp/symbol_index.rb', line 27

def definition(path, position)
  occurrence = occurrence_at(path, position)
  return [] unless occurrence
  return [include_target_location(occurrence)] if occurrence.kind == :include

  matching(occurrence.key, role: :definition).map { |entry| location(entry) }
end

#ensure_fresh!void

This method returns an undefined value.

RBS:

  • () -> void



185
186
187
188
189
190
191
192
# File 'lib/ibex/lsp/symbol_index.rb', line 185

def ensure_fresh!
  stale = @snapshot_state.any? do |path, expected|
    snapshot = @store.snapshot_for(path)
    actual = snapshot && [snapshot.fetch(:version), snapshot.fetch(:source)]
    actual != expected
  end
  raise ProtocolError.new("symbol snapshot is stale; retry the request", code: -32_602) if stale
end

#hover(path, position) ⇒ Hash[String, untyped]?

RBS:

  • (String path, Hash[String, untyped] position) -> Hash[String, untyped]?

Parameters:

  • path (String)
  • position (Hash[String, untyped])

Returns:

  • (Hash[String, untyped], nil)


82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/ibex/lsp/symbol_index.rb', line 82

def hover(path, position)
  occurrence = occurrence_at(path, position)
  return unless occurrence

  value = hover_value(occurrence)
  return unless value

  {
    "contents" => { "kind" => "markdown", "value" => value },
    "range" => range(occurrence)
  }
end

#hover_value(occurrence) ⇒ String?

RBS:

  • (SymbolOccurrence occurrence) -> String?

Parameters:

Returns:

  • (String, nil)


209
210
211
212
213
214
215
216
# File 'lib/ibex/lsp/symbol_index.rb', line 209

def hover_value(occurrence)
  case occurrence.kind
  when :rule then rule_hover(occurrence)
  when :terminal then terminal_hover(occurrence)
  when :parameter then "`#{occurrence.name}` — formal parameter of `#{occurrence.data[:rule]}`"
  when :include then "Includes `#{occurrence.data[:target]}`"
  end
end

#include_target_location(occurrence) ⇒ Hash[String, untyped]

RBS:

  • (SymbolOccurrence occurrence) -> Hash[String, untyped]

Parameters:

Returns:

  • (Hash[String, untyped])


120
121
122
123
124
# File 'lib/ibex/lsp/symbol_index.rb', line 120

def include_target_location(occurrence)
  target = occurrence.data.fetch(:target)
  point = { "line" => 0, "character" => 0 }
  { "uri" => @store.uri_for(target), "range" => { "start" => point, "end" => point.dup } }
end

#location(occurrence) ⇒ Hash[String, untyped]

RBS:

  • (SymbolOccurrence occurrence) -> Hash[String, untyped]

Parameters:

Returns:

  • (Hash[String, untyped])


115
116
117
# File 'lib/ibex/lsp/symbol_index.rb', line 115

def location(occurrence)
  { "uri" => @store.uri_for(occurrence.path), "range" => range(occurrence) }
end

#matching(key, role: nil) ⇒ Array[SymbolOccurrence]

RBS:

  • (Array[untyped] key, ?role: Symbol?) -> Array[SymbolOccurrence]

Parameters:

  • key (Array[untyped])
  • role: (Symbol, nil) (defaults to: nil)

Returns:



110
111
112
# File 'lib/ibex/lsp/symbol_index.rb', line 110

def matching(key, role: nil)
  @occurrences.select { |entry| entry.key == key && (!role || entry.role == role) }
end

#occurrence_at(path, position) ⇒ SymbolOccurrence?

RBS:

  • (String path, Hash[String, untyped] position) -> SymbolOccurrence?

Parameters:

  • path (String)
  • position (Hash[String, untyped])

Returns:



98
99
100
101
102
103
104
105
106
107
# File 'lib/ibex/lsp/symbol_index.rb', line 98

def occurrence_at(path, position)
  document = @documents[path]
  return unless document

  offset = PositionCodec.new(document.source).byte_offset(position)
  candidates = @occurrences.select do |entry|
    entry.path == path && entry.span.start_byte <= offset && offset < entry.span.end_byte
  end
  candidates.min_by { |entry| entry.span.length }
end

#prepare_rename(path, position) ⇒ Hash[String, untyped]?

RBS:

  • (String path, Hash[String, untyped] position) -> Hash[String, untyped]?

Parameters:

  • path (String)
  • position (Hash[String, untyped])

Returns:

  • (Hash[String, untyped], nil)


47
48
49
50
# File 'lib/ibex/lsp/symbol_index.rb', line 47

def prepare_rename(path, position)
  occurrence = renameable_occurrence(path, position)
  { "range" => range(occurrence), "placeholder" => occurrence.name }
end

#range(occurrence) ⇒ Hash[String, Hash[String, Integer]]

RBS:

  • (SymbolOccurrence occurrence) -> Hash[String, Hash[String, Integer]]

Parameters:

Returns:

  • (Hash[String, Hash[String, Integer]])


127
128
129
130
# File 'lib/ibex/lsp/symbol_index.rb', line 127

def range(occurrence)
  document = @documents.fetch(occurrence.path)
  PositionCodec.new(document.source).range(occurrence.span)
end

#references(path, position, include_declaration: false) ⇒ Array[Hash[String, untyped]]

RBS:

  • (String path, Hash[String, untyped] position, ?include_declaration: bool) -> Array[Hash[String, untyped]]

Parameters:

  • path (String)
  • position (Hash[String, untyped])
  • include_declaration: (Boolean) (defaults to: false)

Returns:

  • (Array[Hash[String, untyped]])


37
38
39
40
41
42
43
44
# File 'lib/ibex/lsp/symbol_index.rb', line 37

def references(path, position, include_declaration: false)
  occurrence = occurrence_at(path, position)
  return [] unless occurrence

  entries = matching(occurrence.key)
  entries = entries.reject { |entry| entry.role == :definition } unless include_declaration
  entries.map { |entry| location(entry) }
end

#rename(path, position, new_name) ⇒ Hash[String, untyped]

RBS:

  • (String path, Hash[String, untyped] position, String new_name) -> Hash[String, untyped]

Parameters:

  • path (String)
  • position (Hash[String, untyped])
  • new_name (String)

Returns:

  • (Hash[String, untyped])


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
# File 'lib/ibex/lsp/symbol_index.rb', line 53

def rename(path, position, new_name)
  ensure_fresh!
  occurrence = renameable_occurrence(path, position)
  validate_new_name(occurrence, new_name)
  entries = matching(occurrence.key)
  validate_unshared(entries)
  validate_nonoverlapping(entries)
  changes = entries.group_by(&:path).transform_values do |path_entries|
    path_entries.sort_by { |entry| entry.span.start_byte }.map do |entry|
      { "range" => range(entry), "newText" => new_name }
    end
  end
  replacements = changed_sources(entries, new_name)
  unless @store.valid_replacements?(replacements)
    raise ProtocolError.new("rename would make the affected grammar closure invalid", code: -32_602)
  end

  document_changes = changes.sort.map do |changed_path, edits|
    snapshot = @store.snapshot_for(changed_path)
    version = snapshot&.fetch(:open) ? snapshot.fetch(:version) : nil
    {
      "textDocument" => { "uri" => @store.uri_for(changed_path), "version" => version },
      "edits" => edits
    }
  end
  { "documentChanges" => document_changes }
end

#renameable_occurrence(path, position) ⇒ SymbolOccurrence

RBS:

  • (String path, Hash[String, untyped] position) -> SymbolOccurrence

Parameters:

  • path (String)
  • position (Hash[String, untyped])

Returns:



133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/ibex/lsp/symbol_index.rb', line 133

def renameable_occurrence(path, position)
  occurrence = occurrence_at(path, position)
  unless occurrence && %i[rule terminal parameter].include?(occurrence.kind) &&
         occurrence.name.match?(IDENTIFIER) && !RESERVED.include?(occurrence.name)
    raise ProtocolError.new("symbol at position cannot be renamed", code: -32_602)
  end
  unless matching(occurrence.key, role: :definition).any?
    raise ProtocolError.new("unresolved symbol cannot be renamed", code: -32_602)
  end

  occurrence
end

#rule_hover(occurrence) ⇒ String

RBS:

  • (SymbolOccurrence occurrence) -> String

Parameters:

Returns:

  • (String)


219
220
221
222
223
224
225
# File 'lib/ibex/lsp/symbol_index.rb', line 219

def rule_hover(occurrence)
  definition = matching(occurrence.key, role: :definition).first
  data = definition&.data || occurrence.data
  lines = ["```ibex", data[:signature] || occurrence.name, "```"]
  lines << data[:documentation] if data[:documentation]
  lines.join("\n")
end

#terminal_hover(occurrence) ⇒ String

RBS:

  • (SymbolOccurrence occurrence) -> String

Parameters:

Returns:

  • (String)


228
229
230
231
232
233
234
235
236
237
238
# File 'lib/ibex/lsp/symbol_index.rb', line 228

def terminal_hover(occurrence)
  definition = matching(occurrence.key, role: :definition).first
  data = definition&.data || occurrence.data
  lines = ["`#{occurrence.name}` — terminal"]
  lines << "Display: #{data[:display]}" if data[:display]
  lines << "Type: `#{data[:type]}`" if data[:type]
  if (precedence = data[:precedence])
    lines << "Precedence: #{precedence[:associativity]} level #{precedence[:level]}"
  end
  lines.join("\n\n")
end

#validate_new_name(occurrence, new_name) ⇒ void

This method returns an undefined value.

RBS:

  • (SymbolOccurrence occurrence, String new_name) -> void

Parameters:



147
148
149
150
151
152
153
154
155
156
157
# File 'lib/ibex/lsp/symbol_index.rb', line 147

def validate_new_name(occurrence, new_name)
  unless new_name.is_a?(String) && new_name.match?(IDENTIFIER) && !RESERVED.include?(new_name)
    raise ProtocolError.new("new name must be a non-reserved grammar identifier", code: -32_602)
  end

  collision = @occurrences.any? do |entry|
    entry.role == :definition && entry.name == new_name && entry.key != occurrence.key &&
      collision_scope?(entry, occurrence)
  end
  raise ProtocolError.new("rename collides with existing symbol #{new_name}", code: -32_602) if collision
end

#validate_nonoverlapping(entries) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[SymbolOccurrence] entries) -> void

Parameters:



167
168
169
170
171
172
173
174
175
# File 'lib/ibex/lsp/symbol_index.rb', line 167

def validate_nonoverlapping(entries)
  entries.group_by(&:path).each_value do |path_entries|
    ordered = path_entries.sort_by { |entry| [entry.span.start_byte, entry.span.end_byte] }
    overlap = (1...ordered.length).any? do |index|
      ordered.fetch(index - 1).span.end_byte > ordered.fetch(index).span.start_byte
    end
    raise ProtocolError.new("rename contains overlapping source edits", code: -32_603) if overlap
  end
end

#validate_unshared(entries) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[SymbolOccurrence] entries) -> void

Parameters:



178
179
180
181
182
# File 'lib/ibex/lsp/symbol_index.rb', line 178

def validate_unshared(entries)
  return unless entries.any? { |entry| @store.roots_for(entry.path).length > 1 }

  raise ProtocolError.new("rename affecting a fragment shared by multiple roots is ambiguous", code: -32_602)
end