Class: Ibex::LSP::SymbolIndex
- Inherits:
-
Object
- Object
- Ibex::LSP::SymbolIndex
- 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 =
/\A[A-Za-z_][A-Za-z0-9_]*\z/- RESERVED =
%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
- #changed_sources(entries, new_name) ⇒ Hash[String, String]
- #collision_scope?(candidate, occurrence) ⇒ Boolean
- #definition(path, position) ⇒ Array[Hash[String, untyped]]
- #ensure_fresh! ⇒ void
- #hover(path, position) ⇒ Hash[String, untyped]?
- #hover_value(occurrence) ⇒ String?
- #include_target_location(occurrence) ⇒ Hash[String, untyped]
-
#initialize(store, path) ⇒ SymbolIndex
constructor
A new instance of SymbolIndex.
- #location(occurrence) ⇒ Hash[String, untyped]
- #matching(key, role: nil) ⇒ Array[SymbolOccurrence]
- #occurrence_at(path, position) ⇒ SymbolOccurrence?
- #prepare_rename(path, position) ⇒ Hash[String, untyped]?
- #range(occurrence) ⇒ Hash[String, Hash[String, Integer]]
- #references(path, position, include_declaration: false) ⇒ Array[Hash[String, untyped]]
- #rename(path, position, new_name) ⇒ Hash[String, untyped]
- #renameable_occurrence(path, position) ⇒ SymbolOccurrence
- #rule_hover(occurrence) ⇒ String
- #terminal_hover(occurrence) ⇒ String
- #validate_new_name(occurrence, new_name) ⇒ void
- #validate_nonoverlapping(entries) ⇒ void
- #validate_unshared(entries) ⇒ void
Constructor Details
#initialize(store, path) ⇒ SymbolIndex
Returns a new instance of SymbolIndex.
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]
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
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]]
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.
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]?
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?
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]
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]
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]
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?
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]?
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]]
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]]
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]
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
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
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
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.
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.
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.
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 |