Module: Canopus::Workspace::LanguageAware

Defined in:
lib/canopus/workspace/language_aware.rb

Constant Summary collapse

DIAGNOSTIC_SEVERITIES =
{"error" => 1, "warning" => 2, "information" => 3, "hint" => 4}.freeze
DIAGNOSTIC_COLORS =
{1 => :"diagnostic.error", 2 => :"diagnostic.warning",
3 => :"diagnostic.information", 4 => :"diagnostic.hint"}.freeze
CODE_LENS_REQUEST_LIMIT =
64
DOCUMENT_HIGHLIGHT_LIMIT =
10_000
DOCUMENT_HIGHLIGHT_REQUEST_LIMIT =
64
10_000
64
32
16_384
4_096
FOLDING_RANGE_LIMIT =
10_000
FOLDING_RANGE_REQUEST_LIMIT =
64
SELECTION_RANGE_LIMIT =
10_000
SELECTION_RANGE_POSITION_LIMIT =
256
SELECTION_RANGE_DEPTH_LIMIT =
256
SELECTION_RANGE_REQUEST_LIMIT =
64
PREPARE_RENAME_REQUEST_LIMIT =
1
RENAME_VALUE_LIMIT =
4096
LINKED_EDITING_RANGE_LIMIT =
256
LINKED_EDITING_REQUEST_LIMIT =
1
LINKED_EDITING_PATTERN_LIMIT =
4_096
LINKED_EDITING_MARKUP_BYTE_LIMIT =
1 << 20
LINKED_EDITING_MARKUP_TAG_LIMIT =
10_000
LINKED_EDITING_MARKUP_DEPTH_LIMIT =
256
LINKED_EDITING_MARKUP_NAME_LIMIT =
1_024
LINKED_EDITING_HTML_VOID_TAGS =
%w[area base br col embed hr img input link meta param source track wbr].freeze
DOCUMENT_HIGHLIGHT_STYLES =
{
  1 => {color: :selection}.freeze,
  2 => {color: :accent, underline: true, thickness: 2}.freeze,
  3 => {color: :muted, underline: true}.freeze
}.freeze
{color: :accent, underline: true}.freeze
CODE_LENS_RESOLVE_LIMIT =
32
INDENT_GUIDE_LIMIT =
4096
STICKY_SYMBOL_LIMIT =
10_000
STICKY_DEPTH_LIMIT =
64
STICKY_REQUEST_LIMIT =
16
WORKSPACE_SYMBOL_LIMIT =
10_000
WORKSPACE_SYMBOL_QUERY_LIMIT =
256
WORKSPACE_SYMBOL_TIMEOUT =
10
WORKSPACE_SYMBOL_LABEL_BYTES =
512
WORKSPACE_SYMBOL_PREVIEW_BYTES =
512
WORKSPACE_SYMBOL_UTF16_CHUNK_BYTES =
64 * 1024
[2, 3, 4, 5, 10, 11, 23].freeze
[6, 9, 12].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#hover_card ⇒ Object (readonly)

Returns the value of attribute hover_card.



54
55
56
# File 'lib/canopus/workspace/language_aware.rb', line 54

def hover_card
  @hover_card
end

#hover_markup ⇒ Object (readonly)

Returns the value of attribute hover_markup.



54
55
56
# File 'lib/canopus/workspace/language_aware.rb', line 54

def hover_markup
  @hover_markup
end

#semantic_styles ⇒ Object (readonly)

Returns the value of attribute semantic_styles.



54
55
56
# File 'lib/canopus/workspace/language_aware.rb', line 54

def semantic_styles
  @semantic_styles
end

Instance Method Details

#accept_language_result(palette, index) ⇒ Object

Raises:



1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
# File 'lib/canopus/workspace/language_aware.rb', line 1214

def accept_language_result(palette, index)
  item = palette[:items][index]
  return unless item
  current = palette[:editor]
  raise Error, "Document was closed; request again" if current && !@panes.any? { |pane| pane.editors.include?(current) }
  if current && palette[:version] && current.buffer.version != palette[:version]
    raise Error, "Document changed; request #{palette[:kind]} again"
  end
  completion = item if item.is_a?(Provider::Completion)
   = palette[:completion_metadata]&.[](completion)
  item = [:item] if 
  client = &.fetch(:client) || palette[:item_clients]&.[](index) || palette[:client]
  raise Error, "Language server settings changed; request again" if client && @retired_language_clients&.[](client)
  provider, resolver = case palette[:kind]
  when :completion then ["completionProvider", :resolve_completion]
  when :code_actions then palette[:lens] ? ["codeLensProvider", :resolve_code_lens] : ["codeActionProvider", :resolve_code_action]
  end
  capability = client&.capabilities&.fetch(provider, nil) if provider && client.respond_to?(:capabilities)
  if !palette[:resolved] && capability.is_a?(Hash) && capability["resolveProvider"]
    (@language_jobs ||= []) << Thread.new do
      resolved = client.public_send(resolver, item).await(timeout: 10)
      post do
        next if palette[:completion_generation] && palette[:completion_generation] != @completion_generation
        items = palette[:items].dup
        items[index] = item.merge(resolved || {})
        accept_language_result(palette.merge(items: items, resolved: true, client: client), index)
      end
    rescue StandardError => error
      post { @message = error.message }
    end
    return
  end
  case palette[:kind]
  when :completion
    editor = palette[:editor]
    return accept_provider_completion(completion, editor) if completion && !
    text_edit = item["textEdit"]
    range = if text_edit
      range = text_edit["range"] || text_edit["replace"] || text_edit.fetch("insert")
      Sadr::Protocol.offset(editor.buffer.rope, range.fetch("start"))...Sadr::Protocol.offset(editor.buffer.rope, range.fetch("end"))
    else
      editor.primary.range
    end
    value = text_edit ? text_edit.fetch("newText") : item["insertText"] || item.fetch("label")
    snippet = item["insertTextFormat"] == 2
    variables = editor.snippet_variables(workspace_root: @root, clipboard: @window.respond_to?(:clipboard) ? @window.clipboard : nil) if snippet
    expanded = snippet ? Snippet.new(value, variables: variables).text : value
    additional = item.fetch("additionalTextEdits", []).map do |entry|
      [Sadr::Protocol.offset(editor.buffer.rope, entry.fetch("range").fetch("start"))...Sadr::Protocol.offset(editor.buffer.rope, entry.fetch("range").fetch("end")), entry.fetch("newText")]
    end.sort_by { |entry| entry.first.begin }
    editor.buffer.rope.apply_edits((additional + [[range, expanded]]).sort_by { |entry| entry.first.begin })
    editor.buffer.begin_undo_group
    begin
      editor.select(range.begin, range.end)
      editor.buffer.edit(additional, kind: :completion) unless additional.empty?
      snippet ? editor.insert_snippet(value, variables: variables) : editor.insert_text(value, auto_indent: false, pair: false)
    ensure
      editor.buffer.end_undo_group
    end
    show_snippet_choices(editor) if snippet
    command = item["command"]
    client.execute_command(command.fetch("command"), arguments: command.fetch("arguments", [])) if command && client
  when :locations, :symbols, :workspace_symbol_results
    location = item["location"] || item
    jump_to_language_location(location)
  when :code_actions
    raise Error, item["disabled"]["reason"].to_s if item["disabled"]
    run_command = lambda do
      command = item["command"]
      if command
        command = item if command.is_a?(String)
        client.execute_command(command.fetch("command"), arguments: command.fetch("arguments", []))
      end
    end
    return confirm_workspace_edit(item["edit"], label: item.fetch("title", "Apply code action?"), on_applied: run_command) if resource_workspace_edit?(item["edit"])
    apply_workspace_edit(item["edit"]) if item["edit"]
    run_command.call
  end
end

#apply_workspace_edit(edit, confirmed: false) ⇒ Object



1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
# File 'lib/canopus/workspace/language_aware.rb', line 1111

def apply_workspace_edit(edit, confirmed: false)
  raise Error, "invalid workspace edit" unless edit.is_a?(Hash)
  if resource_workspace_edit?(edit)
    raise Error, "Resource changes require a file-operation confirmation" unless confirmed
    require_relative "edit/plan"
    return Workspace::Edit::Plan.new(self, edit).apply
  end
  # LSP prefers documentChanges when both representations are supplied.
  documents = if edit.key?("documentChanges")
    changes = edit["documentChanges"]
    raise Error, "invalid documentChanges" unless changes.is_a?(Array)
    changes.map do |change|
      raise Error, "invalid document change" unless change.is_a?(Hash)
      raise Error, "Resource changes require a file-operation confirmation" if change["kind"]
      [change.fetch("textDocument"), change.fetch("edits")]
    end
  else
    changes = edit.fetch("changes", {})
    raise Error, "invalid workspace changes" unless changes.is_a?(Hash)
    changes.map { |uri, edits| [{"uri" => uri}, edits] }
  end
  pending, snapshots, originals = {}, {}, {}
  plans = documents.map do |document, edits|
    raise Error, "invalid text document edit" unless document.is_a?(Hash) && edits.is_a?(Array)
    path = canonical_path(Sadr::Protocol.path(document.fetch("uri")))
    raise Error, "language server edit is outside project" unless path.start_with?(@root + File::SEPARATOR)
    actual = File.exist?(path) ? File.realpath(path) : path
    raise Error, "language server edit follows a link outside project" unless actual.start_with?(@root + File::SEPARATOR)
    buffer = @buffers[path] || (pending[path] ||= Buffer.open(path))
    raise Error, "cannot edit a read-only document" if buffer.read_only
    originals[buffer] ||= [buffer.rope, buffer.version]
    rope, version = snapshots.fetch(buffer) { originals.fetch(buffer) }
    requested_version = document["version"]
    unless requested_version.nil? || (requested_version.is_a?(Integer) && requested_version == version)
      raise Error, "language server edit has stale document version"
    end
    changes = edits.map do |entry|
      unless entry.is_a?(Hash) && entry["range"].is_a?(Hash) && entry["newText"].is_a?(String) && entry["newText"].valid_encoding?
        raise Error, "invalid LSP text edit"
      end
      range = entry.fetch("range")
      [Sadr::Protocol.offset(rope, range.fetch("start"))...Sadr::Protocol.offset(rope, range.fetch("end")), entry.fetch("newText")]
    end
    # Repeated TextDocumentEdits address the preceding staged snapshot.
    snapshots[buffer] = [rope.apply_edits(changes), version + (changes.empty? ? 0 : 1)]
    [buffer, changes]
  end
  unless originals.all? { |buffer, (rope, version)| buffer.rope.equal?(rope) && buffer.version == version }
    raise Error, "document changed while preparing language server edits"
  end
  @buffers.merge!(pending)
  grouped = []
  originals.each_key { |buffer| buffer.begin_undo_group; grouped << buffer }
  plans.each { |buffer, changes| buffer.edit(changes, kind: :lsp) }
  {"applied" => true}
ensure
  grouped&.reverse_each(&:end_undo_group)
  pending&.each { |path, buffer| buffer.close unless @buffers[path].equal?(buffer) }
end

#bracket_decorations(buffer, rows, current) ⇒ Object



2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
# File 'lib/canopus/workspace/language_aware.rb', line 2560

def bracket_decorations(buffer, rows, current)
  return [] unless current.is_a?(Editor) && current.buffer.equal?(buffer)
  return [] if buffer.rope.respond_to?(:lazy?) && buffer.rope.lazy?

  values = settings_for_editor(current)
  colorize = values["bracket_colorization"]
  guides = values["indent_guides"]
  return [] unless colorize || guides["enabled"]

  items = colorize ? current.language_document.brackets(rows).flat_map do |pair|
    style = {color: :"bracket.#{pair.depth % 6 + 1}", foreground: true}.freeze
    [pair.open_row, pair.close_row].zip([pair.open_range, pair.close_range]).filter_map do |row, range|
      Decoration::Item.new(:highlight, range, row, nil, style, 15 + pair.depth, :bracket, nil) if rows.cover?(row)
    end
  end : []
  return items.freeze unless guides["enabled"]

  active = active_structure_guide(current, guides["active"])
  first = rows.begin.clamp(0, buffer.line_count)
  last = (rows.exclude_end? ? rows.end : rows.end + 1).clamp(first, buffer.line_count)
  guide_count = 0
  (first...last).each do |row|
    start = buffer.rope.line_start(row)
    indentation_offsets(buffer.line(row), current.tab_size, limit: INDENT_GUIDE_LIMIT - guide_count).each do |column, offset|
      highlighted = active && row > active[:start_line] && row <= active[:end_line] && column == active[:column]
      style = {guide: true, active: !!highlighted,
        color: highlighted ? :"indent.guide.active" : :"indent.guide"}.freeze
      items << Decoration::Item.new(:highlight, (start + offset...start + offset), row, nil,
        style, highlighted ? 13 : 2, :bracket, nil)
      guide_count += 1
    end
    # ponytail: one viewport never needs more than this; paginate if deeply indented generated files become common.
    break if guide_count >= INDENT_GUIDE_LIMIT
  end
  items.freeze
end


2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
# File 'lib/canopus/workspace/language_aware.rb', line 2659

def breadcrumb_context(current)
  return unless breadcrumbs_enabled?(current)
  path = current.buffer.path
  return unless path

  entry = document_symbol_entry(current)
  chain = entry ? document_symbol_chain(current, current.primary.head) : []
  symbols = if chain.any? { |symbol| symbol.kind == :structure }
    chain.last(2)
  else
    container = chain.reverse.find { |symbol| BREADCRUMB_CONTAINER_KINDS.include?(symbol.kind) }
    callable = chain.reverse.find { |symbol| BREADCRUMB_CALLABLE_KINDS.include?(symbol.kind) }
    [container, callable].compact.uniq.sort_by(&:depth)
  end
  prefix = @root + File::SEPARATOR
  relative = path.start_with?(prefix) ? path.delete_prefix(prefix) : File.basename(path)
  project_path = relative if path.start_with?(prefix)
  items = [{kind: :path, label: relative.freeze, value: project_path&.freeze}.freeze]
  items.concat(symbols.map { |symbol| {kind: :symbol, label: symbol.name, value: symbol}.freeze })
  {buffer: current.buffer, document: current.language_document, path: path, version: current.buffer.version,
   client: active_language_client(current.language_document.definition.name, "documentSymbol"),
   generation: entry&.dig(:generation), items: items.freeze}.freeze
end

Returns:

  • (Boolean)


2607
2608
2609
# File 'lib/canopus/workspace/language_aware.rb', line 2607

def breadcrumbs_enabled?(current)
  language_setting(current, "breadcrumbs", "enabled")
end

#cancel_workspace_symbol_search ⇒ Object



344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/canopus/workspace/language_aware.rb', line 344

def cancel_workspace_symbol_search
  request = futures = nil
  (@workspace_symbol_lock ||= Mutex.new).synchronize do
    request = @workspace_symbol_request
    if request
      @workspace_symbol_request = nil
      @workspace_symbol_generation = @workspace_symbol_generation.to_i + 1
      futures = request[:futures].dup
      request[:futures].clear
    end
  end
  cancel_language_requests(futures) if request
  nil
end

#close_language_documents(buffer = nil) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/canopus/workspace/language_aware.rb', line 56

def close_language_documents(buffer = nil)
  invalidate_hierarchy(buffer)
  @opened_lsp_documents&.keys&.each do |key|
    client, document = key
    next if buffer && !document.equal?(buffer)
    begin
      close_language_document(client, document) if document.path
    rescue StandardError => error
      forget_language_document(client, document)
      self.message = "Language server close failed: #{error.message}"
    end
  end
end

#code_lens_decorations(buffer, rows) ⇒ Object



2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
# File 'lib/canopus/workspace/language_aware.rb', line 2411

def code_lens_decorations(buffer, rows)
  active = (@language_clients || {}).values.flatten
  active = @clients.values if active.empty?
  caches = (@code_lens_cache || {}).select do |key, _cache|
    key[1].equal?(buffer) && key[2] == buffer.version && active.include?(key[0])
  end
  caches.flat_map do |key, cache|
    cache[:entries].filter_map do |entry|
      next unless rows.cover?(entry[:row])
      command = entry[:command]
      next unless command

      click = unless entry[:disabled]
        ->(_editor, _offset) { execute_code_lens(key, cache[:generation], command) }
      end
      Decoration::Item.new(:block, nil, entry[:row], command.fetch("title"),
        {position: :above, color: entry[:disabled] ? :muted : :accent},
        30 + entry[:index], :code_lens, click)
    end
  end.sort_by { |item| [item.row, item.priority] }.freeze
end

#confirm_workspace_edit(edit, label: "Apply language server changes?", response: nil, on_applied: nil) ⇒ Object

Raises:



1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
# File 'lib/canopus/workspace/language_aware.rb', line 1095

def confirm_workspace_edit(edit, label: "Apply language server changes?", response: nil, on_applied: nil)
  raise Error, "invalid workspace edit" unless edit.is_a?(Hash)
  changes = edit.fetch("documentChanges", [])
  raise Error, "invalid documentChanges" unless changes.is_a?(Array) && changes.length <= 10_000
  details = Array(edit["documentChanges"]).filter_map do |change|
    next unless change.is_a?(Hash) && change["kind"]
    paths = change["kind"] == "rename" ? change.values_at("oldUri", "newUri") : [change["uri"]]
    raise Error, "resource URI is too long" unless paths.all? { |uri| uri.is_a?(String) && uri.bytesize <= 16_384 }
    suffix = change.dig("options", "overwrite") ? " (overwrite; backup retained)" : ""
    "#{change['kind']}: #{paths.map { |uri| Sadr::Protocol.path(uri) }.join(' → ')}#{suffix}"
  end
  self.palette = {kind: :workspace_edit, query: label.to_s.slice(0, 1_024), index: details.empty? ? 0 : 1, matches: ["Apply changes", "Cancel"],
    edit: edit, response: response, on_applied: on_applied, details: details}
  @window&.request_frame
  @palette
end

#diagnostic_decorations(buffer, rows) ⇒ Object



1077
1078
1079
1080
1081
1082
1083
1084
1085
# File 'lib/canopus/workspace/language_aware.rb', line 1077

def diagnostic_decorations(buffer, rows)
  state = [buffer.version, @diagnostic_generation || 0, @settings["diagnostics"]]
  cached = @diagnostic_decoration_cache&.[](buffer)
  unless cached && cached.first == state
    cached = [state, build_diagnostic_decorations(buffer).freeze]
    (@diagnostic_decoration_cache ||= {})[buffer] = cached
  end
  cached.last.select { |item| diagnostic_item_visible?(buffer, item, rows) }
end

#diagnostics_for(buffer) ⇒ Object



801
802
803
804
805
806
807
808
809
810
811
812
# File 'lib/canopus/workspace/language_aware.rb', line 801

def diagnostics_for(buffer)
  return [] unless buffer.path
  uri = Sadr::Protocol.uri(buffer.path)
  local = @diagnostics.for_uri(uri).reject { |entry| entry.source == :lsp }.map(&:diagnostic)
  clients = [*(@language_clients || {}).values.flatten, *(@lsp_diagnostics || {}).keys].uniq
  lsp = clients.flat_map do |client|
    next [] unless @opened_lsp_documents&.key?([client, buffer]) &&
      @diagnostic_versions&.dig(client, uri) == buffer.version
    @lsp_diagnostics.fetch(client).fetch(uri, [])
  end.uniq
  local + lsp
end

#dismiss_hover ⇒ Object



55
# File 'lib/canopus/workspace/language_aware.rb', line 55

def dismiss_hover = @hover_card = nil

#document_highlight_decorations(buffer, rows, current) ⇒ Object



814
815
816
817
818
819
820
821
# File 'lib/canopus/workspace/language_aware.rb', line 814

def document_highlight_decorations(buffer, rows, current)
  return [] unless current.is_a?(Editor) && current.buffer.equal?(buffer)

  client = active_language_client(current.language_document.definition.name, "documentHighlight")
  key = document_highlight_key(client, current, buffer)
  value = (@document_highlight_cache || {})[key]
  value.is_a?(Array) ? value.select { |item| diagnostic_item_visible?(buffer, item, rows) } : []
end


911
912
913
914
915
916
917
918
919
920
921
922
923
# File 'lib/canopus/workspace/language_aware.rb', line 911

def document_link_decorations(buffer, rows, current)
  return [] unless current.is_a?(Editor) && current.buffer.equal?(buffer)

  client = active_language_client(current.language_document.definition.name, "documentLink")
  cache = (@document_link_cache || {})[document_link_key(client, current, buffer)]
  return [] unless cache.is_a?(Hash)

  cache[:entries].filter_map do |entry|
    next unless diagnostic_item_visible?(buffer, entry[:decoration], rows)

    entry[:decoration]
  end
end

#document_symbol_chain(current, offset) ⇒ Object

Returns cached symbols containing the byte offset, outermost first. This never asks a language server or parser to do work and is safe during paint.



2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
# File 'lib/canopus/workspace/language_aware.rb', line 2633

def document_symbol_chain(current, offset)
  entry = document_symbol_entry(current)
  return [] unless entry && offset.is_a?(Integer) && offset.between?(0, current.buffer.rope.bytesize)

  result = []
  parent_id = nil
  while (symbol = containing_document_symbol(entry, parent_id, offset))
    result << symbol
    parent_id = symbol.id
  end
  result.freeze
end

#document_symbol_generation(current) ⇒ Object



2655
2656
2657
# File 'lib/canopus/workspace/language_aware.rb', line 2655

def document_symbol_generation(current)
  document_symbol_entry(current)&.dig(:generation)
end

#document_symbol_siblings(current, symbol, generation:) ⇒ Object

Breadcrumb navigation consumes the already-indexed sibling list. Generation invalidates navigation as soon as its cache is replaced.



2648
2649
2650
2651
2652
2653
# File 'lib/canopus/workspace/language_aware.rb', line 2648

def document_symbol_siblings(current, symbol, generation:)
  entry = document_symbol_entry(current)
  return [] unless entry && entry[:generation] == generation && entry[:by_id][symbol.id].equal?(symbol)

  entry[:children][symbol.parent_id] || []
end

#expand_selection(current = editor) ⇒ Object



1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
# File 'lib/canopus/workspace/language_aware.rb', line 1314

def expand_selection(current = editor)
  state = @selection_range_states&.[](current)
  if state && selection_range_state_valid?(current, state)
    return apply_selection_expansion(current, state)
  end
  invalidate_selection_ranges(editor: current) if state || @pending_selection_ranges&.key?(current)

  selections = current.selections.dup.freeze
  unless selections.length.between?(1, SELECTION_RANGE_POSITION_LIMIT)
    @message = "Too many selections to expand"
    return false
  end
  document, buffer, version = current.language_document, current.buffer, current.buffer.version
  heads = selections.map(&:head).freeze
  language = document.definition.name
  client = active_language_client(language, "selectionRange")
  pending = {document: document, buffer: buffer, version: version, selections: selections,
    heads: heads, source: :lsp, client: client, supported: selection_range_supported?(client)}
  unless buffer.path && !buffer.read_only && (client || language_server_options(language))
    return selection_with_antares(current, pending)
  end
  unless selection_range_supported?(client) || !client
    return selection_with_antares(current, pending)
  end

  (@pending_selection_ranges ||= {})[current] = pending
  unless request_selection_ranges(buffer, version, client, heads)
    @pending_selection_ranges.delete(current)
    return selection_with_antares(current, pending)
  end
  @message = "Loading selection ranges…"
  nil
end

#fold_current ⇒ Object



1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
# File 'lib/canopus/workspace/language_aware.rb', line 1366

def fold_current
  current, document = editor, editor.language_document
  buffer, version, cursor = current.buffer, current.buffer.version, current.primary.head
  language = document.definition.name
  client = active_language_client(language, "foldingRange")
  key = folding_range_key(client, buffer, version)
  cache = @folding_range_cache ||= {}
  return cache[key] == false ? fold_with_antares(current, document, version, cursor) :
    apply_fold(current, cache[key], cursor) if cache.key?(key)

  unless buffer.path && !buffer.read_only && (client || language_server_options(language))
    return fold_with_antares(current, document, version, cursor)
  end
  unless folding_range_supported?(client) || !client
    cache_folding_ranges(key, false)
    return fold_with_antares(current, document, version, cursor)
  end

  (@pending_folds ||= {})[current] = {document: document, buffer: buffer, version: version,
    cursor: cursor, source: :lsp, client: client}
  unless request_folding_ranges(buffer, version, client)
    @pending_folds.delete(current)
    return fold_with_antares(current, document, version, cursor)
  end
  @message = "Loading fold ranges…"
end

#inlay_hint_decorations(buffer, rows) ⇒ Object



2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
# File 'lib/canopus/workspace/language_aware.rb', line 2304

def inlay_hint_decorations(buffer, rows)
  active = (@language_clients || {}).values.flatten
  active = @clients.values if active.empty?
  cache = (@inlay_hint_cache || {}).select do |key, _entry|
    key[1].equal?(buffer) && key[2] == buffer.version && active.include?(key[0])
  end
  covered = []
  cache.sort_by { |_key, entry| -entry.first }.flat_map do |key, entry|
    items = entry.last.select { |item| rows.cover?(item.row) && covered.none? { |range| range.cover?(item.row) } }
    covered << (key[3]...key[4])
    items
  end.sort_by { |item| [item.range.begin, item.priority] }.freeze
end

#invalidate_brackets(buffer = nil) ⇒ Object



2597
2598
2599
2600
2601
# File 'lib/canopus/workspace/language_aware.rb', line 2597

def invalidate_brackets(buffer = nil)
  @decorations.invalidate(:bracket, buffer: buffer)
  @window&.request_frame unless @closed
  nil
end

#invalidate_code_lenses(buffer = nil, client: nil) ⇒ Object



2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
# File 'lib/canopus/workspace/language_aware.rb', line 2547

def invalidate_code_lenses(buffer = nil, client: nil)
  @code_lens_generation = @code_lens_generation.to_i + 1
  @code_lens_cache&.delete_if do |key, _|
    (!buffer || key[1].equal?(buffer)) && (!client || key[0].equal?(client))
  end
  @code_lens_requests&.delete_if do |_id, entry|
    (!buffer || entry[:buffer].equal?(buffer)) && (!client || entry[:client]&.equal?(client))
  end
  @decorations.invalidate(:code_lens, buffer: buffer)
  @window&.request_frame unless @closed
  nil
end

#invalidate_diagnostics(buffer = nil) ⇒ Object



1087
1088
1089
1090
1091
# File 'lib/canopus/workspace/language_aware.rb', line 1087

def invalidate_diagnostics(buffer = nil)
  @diagnostic_generation = (@diagnostic_generation || 0) + 1
  buffer ? @diagnostic_decoration_cache&.delete(buffer) : @diagnostic_decoration_cache&.clear
  @decorations.invalidate(:diagnostics, buffer: buffer)
end

#invalidate_document_highlights(buffer = nil, client: nil, editor: nil) ⇒ Object



895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
# File 'lib/canopus/workspace/language_aware.rb', line 895

def invalidate_document_highlights(buffer = nil, client: nil, editor: nil)
  buffer ||= editor&.buffer
  @document_highlight_cache&.delete_if do |key, _value|
    (!buffer || key[2].equal?(buffer)) && (!client || key[0]&.equal?(client)) && (!editor || key[1].equal?(editor))
  end
  @document_highlight_requests&.delete_if do |current, entry|
    matches = (!buffer || entry[:buffer].equal?(buffer)) && (!client || entry[:client]&.equal?(client)) &&
      (!editor || current.equal?(editor))
    entry[:future]&.cancel if matches
    matches
  end
  @decorations.invalidate(:document_highlight, buffer: buffer)
  @window&.request_frame unless @closed
  nil
end


1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
# File 'lib/canopus/workspace/language_aware.rb', line 1005

def invalidate_document_links(buffer = nil, client: nil, editor: nil)
  buffer ||= editor&.buffer
  @document_link_cache&.delete_if do |key, cache|
    matches = (!buffer || key[2].equal?(buffer)) && (!client || key[0]&.equal?(client)) &&
      (!editor || key[1].equal?(editor))
    cache[:entries].each { |entry| entry[:future]&.cancel } if matches && cache.is_a?(Hash)
    matches
  end
  @document_link_requests&.delete_if do |current, request|
    matches = (!buffer || request[:buffer].equal?(buffer)) && (!client || request[:client]&.equal?(client)) &&
      (!editor || current.equal?(editor))
    request[:future]&.cancel if matches
    matches
  end
  @decorations.invalidate(:document_link, buffer: buffer)
  @window&.request_frame unless @closed
  nil
end

#invalidate_folding_ranges(buffer = nil, client: nil, editor: nil) ⇒ Object



1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
# File 'lib/canopus/workspace/language_aware.rb', line 1448

def invalidate_folding_ranges(buffer = nil, client: nil, editor: nil)
  affected = []
  unless editor && !buffer && !client
    @folding_range_cache&.delete_if do |key, _value|
      matches = (!buffer || key[1].equal?(buffer)) && (!client || key[0]&.equal?(client))
      affected << key[1] if matches
      matches
    end
    @folding_range_requests&.delete_if do |_id, request|
      matches = (!buffer || request[:buffer].equal?(buffer)) && (!client || request[:client]&.equal?(client))
      if matches
        affected << request[:buffer]
        request[:future]&.cancel
      end
      matches
    end
  end
  @pending_folds&.delete_if do |current, pending|
    (!buffer || pending[:buffer].equal?(buffer)) && (!editor || current.equal?(editor)) &&
      (!client || pending[:client]&.equal?(client) || affected.any? { |item| item.equal?(pending[:buffer]) })
  end
  nil
end

#invalidate_inlay_hints(buffer = nil, client: nil) ⇒ Object



2401
2402
2403
2404
2405
2406
2407
2408
2409
# File 'lib/canopus/workspace/language_aware.rb', line 2401

def invalidate_inlay_hints(buffer = nil, client: nil)
  @inlay_hint_generation = @inlay_hint_generation.to_i + 1
  @inlay_hint_cache&.delete_if do |key, _|
    (!buffer || key[1].equal?(buffer)) && (!client || key[0].equal?(client))
  end
  @decorations.invalidate(:inlay_hint, buffer: buffer)
  @window&.request_frame unless @closed
  nil
end

#invalidate_linked_editing_ranges(buffer = nil, client: nil, editor: nil) ⇒ Object



1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
# File 'lib/canopus/workspace/language_aware.rb', line 1064

def invalidate_linked_editing_ranges(buffer = nil, client: nil, editor: nil)
  @linked_editing_requests&.delete_if do |_id, snapshot|
    matches = (!buffer || snapshot[:buffer].equal?(buffer)) && (!client || snapshot[:client]&.equal?(client)) &&
      (!editor || snapshot[:editor].equal?(editor))
    if matches
      snapshot[:future]&.cancel
      release_linked_editing_snapshot(snapshot)
    end
    matches
  end
  nil
end

#invalidate_prepare_rename(buffer = nil, client: nil, editor: nil) ⇒ Object



682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
# File 'lib/canopus/workspace/language_aware.rb', line 682

def invalidate_prepare_rename(buffer = nil, client: nil, editor: nil)
  @prepare_rename_requests&.delete_if do |_id, snapshot|
    matches = (!buffer || snapshot[:buffer].equal?(buffer)) && (!editor || snapshot[:editor].equal?(editor)) &&
      (!client || snapshot[:client]&.equal?(client))
    if matches
      snapshot[:future]&.cancel
      release_rename_snapshot(snapshot)
    end
    matches
  end
  snapshot = @palette&.dig(:kind) == :rename && @palette[:rename]
  if snapshot && (!buffer || snapshot[:buffer].equal?(buffer)) && (!editor || snapshot[:editor].equal?(editor)) &&
      (!client || snapshot[:client]&.equal?(client))
    self.palette = nil
  end
  nil
end

#invalidate_selection_ranges(buffer = nil, client: nil, editor: nil) ⇒ Object



1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
# File 'lib/canopus/workspace/language_aware.rb', line 1472

def invalidate_selection_ranges(buffer = nil, client: nil, editor: nil)
  affected = []
  unless editor && !buffer && !client
    @selection_range_requests&.delete_if do |_id, request|
      matches = (!buffer || request[:buffer].equal?(buffer)) && (!client || request[:client]&.equal?(client))
      if matches
        affected << request[:buffer]
        request[:future]&.cancel
      end
      matches
    end
  end
  @pending_selection_ranges&.delete_if do |current, pending|
    (!buffer || pending[:buffer].equal?(buffer)) && (!editor || current.equal?(editor)) &&
      (!client || pending[:client]&.equal?(client) || affected.any? { |item| item.equal?(pending[:buffer]) })
  end
  @selection_range_states&.delete_if do |current, state|
    (!buffer || state[:buffer].equal?(buffer)) && (!editor || current.equal?(editor)) &&
      (!client || state[:client]&.equal?(client) || affected.any? { |item| item.equal?(state[:buffer]) })
  end
  cancel_unused_selection_range_requests
  nil
end

#invalidate_sticky_symbols(buffer = nil, client: nil) ⇒ Object



2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
# File 'lib/canopus/workspace/language_aware.rb', line 2820

def invalidate_sticky_symbols(buffer = nil, client: nil)
  @sticky_symbol_cache&.delete_if do |key, _|
    (!buffer || key[1].equal?(buffer)) && (!client || key[0].equal?(client))
  end
  @sticky_symbol_requests&.delete_if do |_id, entry|
    matches = (!buffer || entry[:buffer].equal?(buffer)) && (!client || entry[:client]&.equal?(client))
    entry[:future]&.cancel if matches
    matches
  end
  @sticky_fallback_cache&.delete_if { |editor, _| editor.buffer.equal?(buffer) } if buffer
  @sticky_fallback_cache&.clear unless buffer || client
  @sticky_context_cache&.clear
  @sticky_symbol_start_attempts&.clear unless buffer || client
  @window&.request_frame unless @closed
  nil
end

#language_client(buffer = editor.buffer, feature: nil, timeout: nil) ⇒ Object

Raises:



106
107
108
109
110
# File 'lib/canopus/workspace/language_aware.rb', line 106

def language_client(buffer = editor.buffer, feature: nil, timeout: nil)
  client = language_clients(buffer, feature: feature, timeout: timeout).first
  raise Error, "No language server supports #{feature}" if !client && feature
  client || raise(Error, "No language server configured for #{definition_for(buffer.path).name}")
end

#language_clients(buffer = editor.buffer, feature: nil, timeout: nil) ⇒ Object

Raises:



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/canopus/workspace/language_aware.rb', line 87

def language_clients(buffer = editor.buffer, feature: nil, timeout: nil)
  raise Error, "language servers are disabled for large read-only documents" if buffer.read_only
  language = definition_for(buffer.path)
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
  (@client_lock ||= Mutex.new).synchronize do
    options = language_server_options(language.name)
    raise Error, "No language server configured for #{language.name}" unless options
    remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
    raise Sadr::Timeout, "language server startup timed out" if remaining && remaining <= 0
    ensure_language_server(language.name, options, timeout: remaining)
    clients = language_client_list(language.name)
    @opened_lsp_documents ||= {}
    clients.each do |client|
      open_language_document(client, buffer, language.name) unless @opened_lsp_documents[[client, buffer]]
    end
    route_language_clients(language.name, clients, feature)
  end
end

#language_ready(current, document) ⇒ Object



1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
# File 'lib/canopus/workspace/language_aware.rb', line 1392

def language_ready(current, document)
  if @palette&.dig(:kind) == :outline && @palette[:editor].equal?(current) && @palette[:document].equal?(document)
    if @palette[:version] != current.buffer.version || (!@palette[:loading] && !document.syntax_ready?)
      @palette[:version], @palette[:loading] = current.buffer.version, true
      @palette[:items], @palette[:matches] = [], []
      @palette.delete(:all_matches)
      @palette.delete(:search)
      @palette.delete(:indices)
      document.request(syntax: true)
    elsif document.syntax_ready? && (@palette[:loading] || !@palette[:items].equal?(document.outline))
      symbols = document.outline
      @palette[:items] = symbols
      @palette[:matches] = symbols.map { |symbol| "#{'  ' * symbol.depth}#{symbol.name}" }
      @palette.delete(:all_matches)
      @palette.delete(:search)
      @palette[:loading], @palette[:partial] = false, !document.syntax_complete?
      update_palette
      @message = @palette[:partial] ? "Outline (visible region only)" : "Outline"
      @window&.request_frame
    elsif @palette[:loading] && !document.pending?
      @palette[:loading] = false
      @message = "Outline analysis unavailable"
    end
  end
  pending = @pending_folds&.[](current)
  if pending && pending[:source] == :antares
    unless pending_fold_valid?(current, pending, document)
      @pending_folds.delete(current)
    else
      return if !document.syntax_ready? && document.pending?
      @pending_folds.delete(current)
      if document.syntax_ready?
        apply_fold(current, document.fold_ranges, pending[:cursor])
      else
        @message = "Fold analysis unavailable"
      end
    end
  end

  pending = @pending_selection_ranges&.[](current)
  return unless pending && pending[:source] == :antares
  unless pending_selection_range_valid?(current, pending, document)
    @pending_selection_ranges.delete(current)
    return
  end
  chains = document.selection_ranges(pending[:heads])
  return if !chains && document.pending?
  @pending_selection_ranges.delete(current)
  if chains
    apply_selection_ranges(current, pending,
      normalize_antares_selection_ranges(pending[:buffer].rope, pending[:heads], chains))
  else
    @message = "Selection analysis unavailable"
  end
end

#language_request(kind, **options) ⇒ Object



111
112
113
114
115
116
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/canopus/workspace/language_aware.rb', line 111

def language_request(kind, **options)
  return request_workspace_symbols(options.fetch(:query, "")) if kind == :workspace_symbols

  current, buffer, offset = editor, editor.buffer, editor.primary.head
  if kind == :inlayHint
    requested = request_visible_inlay_hints(current)
    @message = "#{kind}…" if requested
    return requested
  end
  return request_completions(current, buffer, offset) if kind == :completion

  version = buffer.version
  (@language_jobs ||= []) << Thread.new do
    begin
      feature = language_request_feature(kind)
      language_name = definition_for(buffer.path).name
      aggregate = %i[codeAction diagnostic].include?(kind)
      deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 10 if aggregate
      clients = language_clients(buffer, feature: feature,
        timeout: deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC))
      uri = Sadr::Protocol.uri(buffer.path)
      position = Sadr::Protocol.position(buffer.rope, offset)
      raise Error, "No language server supports #{feature}" if clients.empty?
      if kind == :codeAction
        pending = clients.map do |owner|
          [owner, owner.code_action(uri, Sadr::Protocol.range(buffer.rope, current.primary.range),
            {diagnostics: diagnostics_for(buffer)}), nil]
        rescue StandardError => error
          [owner, nil, error]
        end
        pairs, failures, successes = [], [], 0
        pending.each do |owner, future, failure|
          if failure
            failures << failure
            next
          end
          begin
            result = language_request_await(future, deadline)
            successes += 1
            pairs.concat(Array(result).map { |item| [item, owner] })
          rescue StandardError => error
            failures << error
          end
        end
        raise failures.first if successes.zero? && !failures.empty?
        pairs = deduplicate_client_items(pairs)
        client, result, item_clients = clients.first, pairs.map(&:first), pairs.map(&:last)
      elsif kind == :diagnostic
        pending = clients.map do |owner|
          [owner, owner.diagnostic(uri), nil]
        rescue StandardError => error
          [owner, nil, error]
        end
        failures, successes = [], 0
        results = pending.filter_map do |owner, future, failure|
          if failure
            failures << failure
            next
          end
          begin
            value = language_request_await(future, deadline)
            successes += 1
            [owner, value]
          rescue StandardError => error
            failures << error
            nil
          end
        end
        raise failures.first if successes.zero? && !failures.empty?
        client, result = clients.first, results
      else
        client = clients.first
        result = case kind
        when :hover, :definition, :typeDefinition, :implementation, :signatureHelp
          method = {typeDefinition: :type_definition, signatureHelp: :signature_help}.fetch(kind, kind)
          client.public_send(method, uri, position).await(timeout: 10)
        when :references then client.references(uri, position, include_declaration: true).await(timeout: 10)
        when :rename then client.rename(uri, position, options.fetch(:name)).await(timeout: 10)
        when :formatting then client.formatting(uri, {tabSize: current.tab_size, insertSpaces: !current.use_tabs}).await(timeout: 10)
        when :documentSymbol then client.document_symbol(uri).await(timeout: 10)
        when :codeLens then client.code_lens(uri).await(timeout: 10)
        when :semantic_tokens then client.semantic_tokens(uri, version: buffer.version)
        when :workspace_symbols then client.workspace_symbols(options.fetch(:query, "")).await(timeout: 10)
        else raise Error, "unknown language request #{kind}"
        end
      end
      post do
        routed = route_language_clients(language_name, language_client_list(language_name), feature)
        if %i[codeAction diagnostic].include?(kind)
          next unless clients == routed
        else
          next unless routed.first.equal?(client)
        end
        next unless @panes.any? { |pane| pane.editors.include?(current) }
        if buffer.version == version
          if kind == :diagnostic
            result.each { |owner, value| display_language_result(kind, value, owner, current) }
          else
            display_language_result(kind, result, client, current, item_clients: item_clients)
          end
        else
          @message = "Document changed; request #{kind} again"
        end
      end
    rescue StandardError => error
      post { @message = error.message unless client && @retired_language_clients&.[](client) }
    ensure
      cancel_language_requests(pending)
    end
  end
  @language_jobs.reject! { |thread| !thread.alive? }
  @message = "#{kind}…"
end

#linked_editing_range(current = editor) ⇒ Object



1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
# File 'lib/canopus/workspace/language_aware.rb', line 1024

def linked_editing_range(current = editor)
  invalidate_linked_editing_ranges
  buffer = current.buffer
  unless buffer.path && !buffer.read_only
    @message = "Linked editing is not available here"
    return false
  end

  snapshot = linked_editing_snapshot(current)
  language, client = snapshot.values_at(:language, :client)
  fallback = linked_editing_markup?(snapshot)
  unless client || language_server_options(language) || fallback
    release_linked_editing_snapshot(snapshot)
    @message = "Linked editing is not available here"
    return false
  end
  if client && !snapshot[:supported] && !fallback
    release_linked_editing_snapshot(snapshot)
    @message = "Language server does not support linked editing"
    return false
  end
  requests = @linked_editing_requests ||= {}
  if requests.length >= LINKED_EDITING_REQUEST_LIMIT
    release_linked_editing_snapshot(snapshot)
    @message = "Too many linked editing requests"
    return false
  end

  id = @linked_editing_request_id = @linked_editing_request_id.to_i + 1
  requests[id] = snapshot
  start_linked_editing_request(id, snapshot)
  @message = "Loading linked ranges…"
  nil
rescue StandardError => error
  @linked_editing_requests&.delete_if { |_id, request| request.equal?(snapshot) }
  release_linked_editing_snapshot(snapshot) if snapshot
  @message = error.message
  false
end

#minimap_settings(current) ⇒ Object



2683
2684
2685
2686
2687
# File 'lib/canopus/workspace/language_aware.rb', line 2683

def minimap_settings(current)
  @settings["minimap"].merge(
    @settings["languages"].fetch(current.language_document.definition.name, {}).fetch("minimap", {})
  )
end

#post(&block) ⇒ Object



83
84
85
86
# File 'lib/canopus/workspace/language_aware.rb', line 83

def post(&block)
  @main_queue << block
  @window&.request_frame
end

#prepare_rename(current = editor) ⇒ Object



619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
# File 'lib/canopus/workspace/language_aware.rb', line 619

def prepare_rename(current = editor)
  self.palette = nil
  invalidate_prepare_rename
  unless current && current.buffer.path
    self.palette = {kind: :rename, query: +"", index: 0, matches: []}
    return true
  end

  snapshot = rename_snapshot(current)
  language, client = snapshot.values_at(:language, :client)
  unless client || language_server_options(language)
    return open_rename_palette(snapshot, "")
  end
  return open_rename_palette(snapshot, "") if client && !snapshot[:prepare_supported]

  requests = @prepare_rename_requests ||= {}
  if requests.length >= PREPARE_RENAME_REQUEST_LIMIT
    release_rename_snapshot(snapshot)
    @message = "Too many rename requests"
    return false
  end
  id = @prepare_rename_request_id = @prepare_rename_request_id.to_i + 1
  requests[id] = snapshot
  start_prepare_rename(id, snapshot)
  @message = "Preparing rename…"
  nil
rescue StandardError => error
  @prepare_rename_requests&.delete_if { |_id, request| request.equal?(snapshot) }
  release_rename_snapshot(snapshot) if snapshot
  @message = error.message
  false
end

#refresh_sticky_fallback(current, document = current.language_document) ⇒ Object



2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
# File 'lib/canopus/workspace/language_aware.rb', line 2714

def refresh_sticky_fallback(current, document = current.language_document)
  return [] unless (sticky_scroll_enabled?(current) || breadcrumbs_enabled?(current)) &&
    document.equal?(current.language_document) && document.syntax_ready?

  buffer = current.buffer
  regions = document.structure_regions
  cached = @sticky_fallback_cache&.[](current)
  return cached.last[:symbols] if cached && cached[0] == buffer.version && cached[1].equal?(document) && cached[2].equal?(regions)

  rope = buffer.rope
  drafts = regions.first(STICKY_SYMBOL_LIMIT).filter_map do |region|
    first, last, kind = region.values_at(:start_line, :end_line, :kind)
    next unless %i[block region].include?(kind) && first.is_a?(Integer) && last.is_a?(Integer) &&
      first >= 0 && last > first && last < buffer.line_count

    start = rope.line_start(first)
    finish = last + 1 < buffer.line_count ? rope.line_start(last + 1) : rope.bytesize
    label = sticky_source_label(rope, first)
    label = "Line #{first + 1}" if label.empty?
    [truncate_diagnostic_message(label, 256).freeze, (start...finish).freeze, (start...start).freeze]
  end.uniq { |_name, range, _selection| [range.begin, range.end] }
    .sort_by { |_name, range, _selection| [range.begin, -range.end] }
  parents = []
  values = drafts.each_with_index.map do |(name, range, selection), id|
    parents.pop until parents.empty? || strictly_contains?(parents.last.range, range)
    parent = parents.last
    symbol = Language::DocumentSymbol.new(id, name, :structure, range, selection,
      parent ? parent.depth + 1 : 0, parent&.id)
    parents << symbol
    symbol
  end.freeze
  entry = sticky_cache_entry(buffer, buffer.version, nil, values)
  @sticky_fallback_cache ||= {}
  @sticky_fallback_cache[current] = [buffer.version, document, regions, entry]
  @sticky_context_cache&.clear
  values
end

#rename_prepared(snapshot, name) ⇒ Object



652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
# File 'lib/canopus/workspace/language_aware.rb', line 652

def rename_prepared(snapshot, name)
  unless valid_rename_value?(name) && name.bytesize <= RENAME_VALUE_LIMIT
    release_rename_snapshot(snapshot)
    @message = "Invalid rename value"
    return false
  end
  unless rename_snapshot_valid?(snapshot)
    release_rename_snapshot(snapshot)
    @message = "Rename cancelled because the document changed"
    return false
  end

  requests = @prepare_rename_requests ||= {}
  if requests.length >= PREPARE_RENAME_REQUEST_LIMIT
    release_rename_snapshot(snapshot)
    @message = "Too many rename requests"
    return false
  end
  id = @prepare_rename_request_id = @prepare_rename_request_id.to_i + 1
  requests[id] = snapshot
  start_prepared_rename(id, snapshot, name.dup.freeze)
  @message = "rename…"
  nil
rescue StandardError => error
  @prepare_rename_requests&.delete_if { |_id, request| request.equal?(snapshot) }
  release_rename_snapshot(snapshot)
  @message = error.message
  false
end

#request_code_lenses(current, visible_rows, start: false) ⇒ Object



2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
# File 'lib/canopus/workspace/language_aware.rb', line 2449

def request_code_lenses(current, visible_rows, start: false)
  @language_jobs&.reject! { |thread| !thread.alive? }
  buffer = current.buffer
  return false unless buffer.path && !buffer.read_only
  unless visible_rows.is_a?(Range) && visible_rows.begin.is_a?(Integer) && visible_rows.end.is_a?(Integer)
    raise ArgumentError, "code lens rows must be an integer range"
  end
  first = visible_rows.begin.clamp(0, buffer.line_count)
  last = (visible_rows.exclude_end? ? visible_rows.end : visible_rows.end + 1).clamp(first, buffer.line_count)
  return false if first == last

  language = current.language_document.definition.name
  client = active_language_client(language, "codeLens")
  return false unless client || start
  provider = client&.capabilities&.[]("codeLensProvider")
  return false if client && provider != true && !provider.is_a?(Hash)
  return false unless settings_for_path(buffer.path, language: language)["code_lens"]["enabled"]
  cached = (@code_lens_cache || {}).find do |key, _cache|
    client && key[0].equal?(client) && key[1].equal?(buffer) && key[2] == buffer.version
  end
  generation = @code_lens_generation.to_i
  pending = (@code_lens_requests || {}).values.any? do |entry|
    entry[:buffer].equal?(buffer) && entry[:version] == buffer.version &&
      (!client || !entry[:client] || entry[:client].equal?(client))
  end
  if cached
    resolve_visible_code_lenses(*cached, first...last)
    return false
  end
  return false if pending || (@code_lens_requests || {}).length >= CODE_LENS_REQUEST_LIMIT
  unless client
    options = language_server_options(language)
    attempts = @code_lens_start_attempts ||= {}
    return false if attempts.key?(language) && attempts[language] == options
    attempts[language] = options
    return false unless options
  end

  @code_lens_request_id = @code_lens_request_id.to_i + 1
  id, version = @code_lens_request_id, buffer.version
  request = {client: client, buffer: buffer, version: version, generation: generation}
  (@code_lens_requests ||= {})[id] = request
  job = Thread.new do
    begin
      owner = language_client(buffer, feature: "codeLens")
      request[:client] = owner
      capability = owner.capabilities["codeLensProvider"]
      supported = capability == true || capability.is_a?(Hash)
      valid = buffer.version == version && language_client_active?(owner) && @code_lens_requests&.[](id).equal?(request)
      result = owner.code_lens(Sadr::Protocol.uri(buffer.path)).await(timeout: 10) if supported && valid
      post do
        pending = @code_lens_requests&.delete(id)
        @language_jobs&.reject! { |thread| !thread.alive? }
        next unless pending.equal?(request) && supported && valid && language_client_active?(owner)
        next unless buffer.version == version && @panes.any? { |pane| pane.editors.any? { |editor| editor.buffer.equal?(buffer) } }

        begin
          cache_code_lenses(owner, buffer, version, result, generation)
        rescue StandardError => error
          cache_code_lenses(owner, buffer, version, [], generation)
          @message = error.message
        end
      end
    rescue StandardError => error
      post do
        pending = @code_lens_requests&.delete(id)
        @language_jobs&.reject! { |thread| !thread.alive? }
        @code_lens_start_attempts&.delete(language) unless client
        if pending.equal?(request) && owner && language_client_active?(owner) && buffer.version == version &&
            @panes.any? { |pane| pane.editors.any? { |editor| editor.buffer.equal?(buffer) } }
          cache_code_lenses(owner, buffer, version, [], generation)
        end
        @message = error.message if pending.equal?(request) && !(owner && @retired_language_clients&.[](owner))
      end
    ensure
      worker = Thread.current
      post { @language_jobs&.delete(worker) }
    end
  end
  (@language_jobs ||= []) << job
  @language_jobs.reject! { |thread| !thread.alive? }
  true
end

#request_document_highlights(current) ⇒ Object



823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
# File 'lib/canopus/workspace/language_aware.rb', line 823

def request_document_highlights(current)
  requests = @document_highlight_requests ||= {}
  requests.delete_if do |editor, entry|
    hidden = @panes.none? { |pane| pane.active.equal?(editor) }
    entry[:future]&.cancel if hidden
    hidden
  end
  buffer = current.buffer
  return false unless buffer.path && !buffer.read_only && @panes.any? { |pane| pane.active.equal?(current) }

  language = current.language_document.definition.name
  client = active_language_client(language, "documentHighlight")
  key = document_highlight_key(client, current, buffer)
  cache = @document_highlight_cache ||= {}
  return false if cache.key?(key)

  pending = requests[current]
  if pending && pending[:buffer].equal?(buffer) && pending[:version] == buffer.version &&
      pending[:head] == current.primary.head && (!client || !pending[:client] || pending[:client].equal?(client))
    return false
  end
  invalidate_document_highlights(editor: current) if pending || cache.keys.any? { |entry| entry[1].equal?(current) }
  unless client || language_server_options(language)
    cache_document_highlights(key, false)
    return false
  end
  return false if requests.length >= DOCUMENT_HIGHLIGHT_REQUEST_LIMIT

  request = {client: client, editor: current, buffer: buffer, version: buffer.version,
    head: current.primary.head, rope: buffer.rope, uri: Sadr::Protocol.uri(buffer.path)}
  requests[current] = request
  @decorations.invalidate(:document_highlight, buffer: buffer)
  @window&.request_frame
  job = Thread.new do
    begin
      owner = language_client(buffer, feature: "documentHighlight")
      request[:client] = owner
      supported = document_highlight_supported?(owner)
      valid = document_highlight_request_valid?(request, owner)
      future = owner.document_highlight(request[:uri], Sadr::Protocol.position(request[:rope], request[:head])) if supported && valid
      request[:future] = future
      result = future.await(timeout: 10) if future && document_highlight_request_valid?(request, owner)
      future&.cancel unless document_highlight_request_valid?(request, owner)
      highlights = supported && valid ? normalize_document_highlights(request[:rope], result) : false
      post do
        next unless @document_highlight_requests&.[](current).equal?(request)
        @document_highlight_requests.delete(current)
        next unless document_highlight_result_valid?(request, owner)

        cache_document_highlights(document_highlight_key(owner, current, buffer,
          request[:version], request[:head], supported), highlights)
      end
    rescue StandardError => error
      post do
        next unless @document_highlight_requests&.[](current).equal?(request)
        @document_highlight_requests.delete(current)
        next unless owner ? document_highlight_result_valid?(request, owner) : document_highlight_editor_valid?(request)

        cache_document_highlights(document_highlight_key(owner, current, buffer,
          request[:version], request[:head]), false)
        @message = error.message unless owner && @retired_language_clients&.[](owner)
      end
    ensure
      worker = Thread.current
      post { @language_jobs&.delete(worker) }
    end
  end
  (@language_jobs ||= []) << job
  @language_jobs.reject! { |thread| !thread.alive? }
  true
end


925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
# File 'lib/canopus/workspace/language_aware.rb', line 925

def request_document_links(current)
  requests = @document_link_requests ||= {}
  requests.delete_if do |editor, request|
    hidden = @panes.none? { |pane| pane.active.equal?(editor) }
    request[:future]&.cancel if hidden
    hidden
  end
  buffer = current.buffer
  return false unless buffer.path && !buffer.read_only && @panes.any? { |pane| pane.active.equal?(current) }

  language = current.language_document.definition.name
  client = active_language_client(language, "documentLink")
  key = document_link_key(client, current, buffer)
  return false if (@document_link_cache ||= {}).key?(key)

  pending = requests[current]
  if pending && pending[:buffer].equal?(buffer) && pending[:version] == buffer.version &&
      pending[:document].equal?(current.language_document) &&
      (!client || !pending[:client] || pending[:client].equal?(client)) &&
      (!pending[:client] || pending[:supported] == document_link_supported?(client) &&
        pending[:resolve] == document_link_resolve_supported?(client))
    return false
  end
  invalidate_document_links(editor: current) if pending || @document_link_cache.keys.any? { |entry| entry[1].equal?(current) }
  unless client || language_server_options(language)
    cache_document_links(key, false)
    return false
  end
  return false if requests.length >= DOCUMENT_LINK_REQUEST_LIMIT

  request = {client: client, editor: current, buffer: buffer, version: buffer.version,
    rope: buffer.rope, uri: Sadr::Protocol.uri(buffer.path), language: language,
    document: current.language_document,
    supported: client && document_link_supported?(client),
    resolve: client && document_link_resolve_supported?(client)}
  requests[current] = request
  @decorations.invalidate(:document_link, buffer: buffer)
  job = Thread.new do
    begin
      owner = language_client(buffer, feature: "documentLink")
      if request[:client]
        next unless request[:client].equal?(owner)
      else
        request[:client] = owner
        request[:supported] = document_link_supported?(owner)
        request[:resolve] = document_link_resolve_supported?(owner)
      end
      supported, resolve = request.values_at(:supported, :resolve)
      valid = document_link_request_valid?(request, owner)
      future = owner.document_link(request[:uri]) if supported && valid
      request[:future] = future
      result = future.await(timeout: 10) if future && document_link_request_valid?(request, owner)
      future&.cancel unless document_link_request_valid?(request, owner)
      links = supported && valid ? normalize_document_links(request[:rope], result, resolve: resolve) : false
      post do
        next unless @document_link_requests&.[](current).equal?(request)
        @document_link_requests.delete(current)
        next unless document_link_result_valid?(request, owner)

        cache_document_links(document_link_key(owner, current, buffer, request[:version], supported, resolve), links)
      end
    rescue StandardError => error
      post do
        next unless @document_link_requests&.[](current).equal?(request)
        @document_link_requests.delete(current)
        next unless owner ? document_link_result_valid?(request, owner) : document_link_editor_valid?(request)

        cache_document_links(document_link_key(owner, current, buffer, request[:version]), false)
        @message = error.message unless owner && @retired_language_clients&.[](owner)
      end
    ensure
      worker = Thread.current
      post { @language_jobs&.delete(worker) }
    end
  end
  (@language_jobs ||= []) << job
  @language_jobs.reject! { |thread| !thread.alive? }
  true
end

#request_inlay_hints(current, visible_rows, start: false) ⇒ Object



2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
# File 'lib/canopus/workspace/language_aware.rb', line 2318

def request_inlay_hints(current, visible_rows, start: false)
  @language_jobs&.reject! { |thread| !thread.alive? }
  buffer = current.buffer
  return false unless buffer.path && !buffer.read_only
  unless visible_rows.is_a?(Range) && visible_rows.begin.is_a?(Integer) && visible_rows.end.is_a?(Integer)
    raise ArgumentError, "inlay hint rows must be an integer range"
  end
  first = visible_rows.begin.clamp(0, buffer.line_count)
  last = (visible_rows.exclude_end? ? visible_rows.end : visible_rows.end + 1).clamp(first, buffer.line_count)
  return false if first == last

  language = current.language_document.definition.name
  client = active_language_client(language, "inlayHint")
  return false unless client || start
  return false if client && !client.capabilities["inlayHintProvider"]
  settings = settings_for_path(buffer.path, language: language)["inlay_hints"]
  return false unless settings["enabled"]
  unless client
    options = language_server_options(language)
    attempts = @inlay_hint_start_attempts ||= {}
    return false if attempts.key?(language) && attempts[language] == options
    attempts[language] = options
    return false unless options
  end

  requested = [first - 50, 0].max...[last + 50, buffer.line_count].min
  cached = (@inlay_hint_cache || {}).keys.any? do |key|
    (!client || key[0].equal?(client)) && key[1].equal?(buffer) && key[2] == buffer.version &&
      key[3] <= first && key[4] >= last
  end
  pending = (@inlay_hint_requests || {}).values.any? do |entry|
    entry[:buffer].equal?(buffer) && entry[:version] == buffer.version && entry[:generation] == @inlay_hint_generation.to_i &&
      (!client || !entry[:client] || entry[:client].equal?(client)) && entry[:range].begin <= first && entry[:range].end >= last
  end
  return false if cached || pending

  @inlay_hint_request_id = @inlay_hint_request_id.to_i + 1
  id, version, generation = @inlay_hint_request_id, buffer.version, @inlay_hint_generation.to_i
  rope = buffer.rope
  byte_range = rope.line_start(requested.begin)...(requested.end == buffer.line_count ? rope.bytesize : rope.line_start(requested.end))
  protocol_range = Sadr::Protocol.range(rope, byte_range)
  (@inlay_hint_requests ||= {})[id] = {client: client, buffer: buffer, version: version, generation: generation, range: requested}
  job = Thread.new do
    begin
      owner = language_client(buffer, feature: "inlayHint")
      supported = owner.capabilities["inlayHintProvider"]
      valid = buffer.version == version && @inlay_hint_generation.to_i == generation && language_client_active?(owner)
      result = owner.inlay_hint(Sadr::Protocol.uri(buffer.path), protocol_range).await(timeout: 10) if supported && valid
      post do
        @inlay_hint_requests&.delete(id)
        @language_jobs&.reject! { |thread| !thread.alive? }
        next unless supported && valid && @inlay_hint_generation.to_i == generation && language_client_active?(owner)
        next unless buffer.version == version && @panes.any? { |pane| pane.editors.any? { |editor| editor.buffer.equal?(buffer) } }

        cache_inlay_hints(owner, buffer, version, requested, result, id, settings)
      end
    rescue StandardError => error
      post do
        @inlay_hint_requests&.delete(id)
        @language_jobs&.reject! { |thread| !thread.alive? }
        @message = error.message unless owner && @retired_language_clients&.[](owner)
      end
    end
  end
  (@language_jobs ||= []) << job
  @language_jobs.reject! { |thread| !thread.alive? }
  true
end

#request_sticky_symbols(current) ⇒ Object



2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
# File 'lib/canopus/workspace/language_aware.rb', line 2752

def request_sticky_symbols(current)
  return false unless sticky_scroll_enabled?(current) || breadcrumbs_enabled?(current)

  buffer = current.buffer
  return false unless buffer.path && !buffer.read_only
  language = current.language_document.definition.name
  client = active_language_client(language, "documentSymbol")
  cache = @sticky_symbol_cache || {}
  return false if client && cache.key?([client, buffer, buffer.version])
  return false if client && !client.capabilities["documentSymbolProvider"]

  requests = @sticky_symbol_requests ||= {}
  return false if requests.values.any? { |entry| entry[:buffer].equal?(buffer) && entry[:version] == buffer.version }
  return false if requests.length >= STICKY_REQUEST_LIMIT
  unless client
    options = language_server_options(language)
    attempts = @sticky_symbol_start_attempts ||= {}
    return false if attempts.key?(language) && attempts[language] == options
    attempts[language] = options
    return false unless options
  end

  id = @sticky_symbol_request_id = @sticky_symbol_request_id.to_i + 1
  request = {client: client, buffer: buffer, version: buffer.version,
    rope: buffer.rope, uri: Sadr::Protocol.uri(buffer.path)}
  requests[id] = request
  job = Thread.new do
    begin
      owner = language_client(buffer, feature: "documentSymbol")
      request[:client] = owner
      supported = !!owner.capabilities["documentSymbolProvider"]
      active = @sticky_symbol_requests&.[](id).equal?(request) && buffer.version == request[:version] && language_client_active?(owner)
      future = owner.document_symbol(request[:uri]) if supported && active
      request[:future] = future
      if future && @sticky_symbol_requests&.[](id).equal?(request)
        symbols = normalize_document_symbols(request[:rope], future.await(timeout: 10), request[:uri])
      else
        future&.cancel
        symbols = false
      end
      post do
        pending = @sticky_symbol_requests&.delete(id)
        next unless pending.equal?(request) && buffer.version == request[:version] && language_client_active?(owner) &&
          buffer.path && Sadr::Protocol.uri(buffer.path) == request[:uri] &&
          @opened_lsp_documents&.key?([owner, buffer]) &&
          @panes.any? { |pane| pane.editors.any? { |editor| editor.buffer.equal?(buffer) } }

        cache_sticky_symbols(owner, buffer, request[:version], symbols)
      end
    rescue StandardError => error
      post do
        pending = @sticky_symbol_requests&.delete(id)
        if pending.equal?(request) && owner && buffer.version == request[:version] && language_client_active?(owner) &&
            buffer.path && Sadr::Protocol.uri(buffer.path) == request[:uri] && @opened_lsp_documents&.key?([owner, buffer])
          cache_sticky_symbols(owner, buffer, request[:version], false)
          @message = error.message unless @retired_language_clients&.[](owner)
        end
      end
    ensure
      worker = Thread.current
      post { @language_jobs&.delete(worker) }
    end
  end
  (@language_jobs ||= []) << job
  @language_jobs.reject! { |thread| !thread.alive? }
  true
end

#request_visible_code_lenses(current = editor) ⇒ Object



2533
2534
2535
2536
2537
2538
2539
2540
2541
# File 'lib/canopus/workspace/language_aware.rb', line 2533

def request_visible_code_lenses(current = editor)
  @code_lens_start_attempts&.delete(current.language_document.definition.name)
  map = current.display_map
  first = current.scroll_y.floor.clamp(0, map.row_count - 1)
  last = [first + current.viewport_rows, map.row_count - 1].min
  visible_code_lens_ranges(current, first...(last + 1)).map do |rows|
    request_code_lenses(current, rows, start: true)
  end.any?
end

#request_visible_inlay_hints(current = editor) ⇒ Object



2387
2388
2389
2390
2391
2392
2393
2394
2395
# File 'lib/canopus/workspace/language_aware.rb', line 2387

def request_visible_inlay_hints(current = editor)
  @inlay_hint_start_attempts&.delete(current.language_document.definition.name)
  map = current.display_map
  first = current.scroll_y.floor.clamp(0, map.row_count - 1)
  last = [first + current.viewport_rows, map.row_count - 1].min
  visible_inlay_hint_ranges(current, first...(last + 1)).map do |rows|
    request_inlay_hints(current, rows, start: true)
  end.any?
end

#request_workspace_symbols(query) ⇒ Object

Raises:



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
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
333
334
335
336
337
338
339
340
341
342
# File 'lib/canopus/workspace/language_aware.rb', line 266

def request_workspace_symbols(query)
  valid = query.is_a?(String) && query.valid_encoding? && query.bytesize.between?(1, WORKSPACE_SYMBOL_QUERY_LIMIT) &&
    !query.match?(/[\0\r\n]/) && !query.strip.empty?
  raise Error, "workspace symbol query must be 1 to #{WORKSPACE_SYMBOL_QUERY_LIMIT} bytes" unless valid

  cancel_workspace_symbol_search
  generation = @workspace_symbol_generation = @workspace_symbol_generation.to_i + 1
  query = query.dup.freeze
  request = {generation: generation, futures: [], clients: []}
  (@workspace_symbol_lock ||= Mutex.new).synchronize { @workspace_symbol_request = request }
  empty = [].freeze
  self.palette = {kind: :workspace_symbol_results, query: +"", index: 0, matches: empty, items: empty,
    all_matches: empty, indices: empty, workspace_symbol_groups: {}.freeze,
    workspace_symbol_index: Spica::Index.new(empty, tie_break: :index), workspace_symbol_generation: generation}
  job = Thread.new do
    deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + WORKSPACE_SYMBOL_TIMEOUT
    clients = workspace_symbol_clients
    (@workspace_symbol_lock ||= Mutex.new).synchronize do
      request[:clients] = clients if @workspace_symbol_request.equal?(request)
    end
    pending = clients.filter_map do |client|
      next unless workspace_symbol_request_current?(request)
      entry = begin
        [client, client.workspace_symbols(query), nil]
      rescue StandardError => error
        [client, nil, error]
      end
      tracked = (@workspace_symbol_lock ||= Mutex.new).synchronize do
        if @workspace_symbol_request.equal?(request)
          request[:futures] << entry
          true
        end
      end
      cancel_language_requests([entry]) unless tracked
      entry if tracked
    end
    symbols, seen, successes = [], {}, 0
    pending.each do |client, future, failure|
      next if failure || !workspace_symbol_request_current?(request)
      begin
        response = language_request_await(future, deadline)
        next if client.respond_to?(:running?) && !client.running?
        values = normalize_workspace_symbols(response)
        successes += 1
        values.each do |symbol|
          break if symbols.length >= WORKSPACE_SYMBOL_LIMIT
          next if seen.key?(symbol)
          seen[symbol] = true
          symbols << symbol
        end
      rescue StandardError
        nil
      end
    end
    if successes.zero? && workspace_symbol_request_current?(request)
      fallback_deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + WORKSPACE_SYMBOL_TIMEOUT
      symbols = workspace_symbol_fallback(query, generation, fallback_deadline)
    end
    next unless workspace_symbol_request_current?(request)
    symbols = deduplicate_workspace_symbols(symbols).first(WORKSPACE_SYMBOL_LIMIT).freeze
    prepared = prepare_workspace_symbol_palette(symbols, generation)
    post do
      install_workspace_symbol_palette(request, prepared)
    end
  rescue StandardError => error
    post { @message = error.message if workspace_symbol_request_current?(request) }
  ensure
    cancel_language_requests(pending)
    (@workspace_symbol_lock ||= Mutex.new).synchronize do
      request[:futures].clear if @workspace_symbol_request.equal?(request)
    end
  end
  (@language_jobs ||= []) << job
  @language_jobs.reject! { |thread| !thread.alive? }
  @message = "Searching workspace symbols…"
  job
end

#resource_workspace_edit?(edit) ⇒ Boolean

Returns:

  • (Boolean)


1092
1093
1094
# File 'lib/canopus/workspace/language_aware.rb', line 1092

def resource_workspace_edit?(edit)
  edit.is_a?(Hash) && edit["documentChanges"].is_a?(Array) && edit["documentChanges"].any? { |change| change.is_a?(Hash) && change["kind"] }
end

#run_save_actions(buffer) ⇒ Object

Raises:

  • (Sadr::Timeout)


482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# File 'lib/canopus/workspace/language_aware.rb', line 482

def run_save_actions(buffer)
  language = definition_for(buffer.path)
  values = settings_for_path(buffer.path, language: language.name)
  kinds = values["code_actions_on_save"]
  return false unless values["format_on_save"] || !kinds.empty?
  return false unless language_server_options(language.name)

  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + values["format_on_save_timeout"] / 1_000.0
  remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
  raise Sadr::Timeout, "save actions timed out" if remaining <= 0
  clients = language_clients(buffer, timeout: remaining)
  raise Sadr::Timeout, "save actions timed out" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
  buffer.begin_undo_group
  ran = false
  begin
    formatter = route_language_clients(language.name, clients, "formatting").find do |client|
      capability = client.capabilities["documentFormattingProvider"]
      capability == true || capability.is_a?(Hash)
    end
    if values["format_on_save"] && formatter
      version = buffer.version
      edits = save_action_await(formatter.formatting(Sadr::Protocol.uri(buffer.path),
        {tabSize: values["tab_size"], insertSpaces: !values["use_tabs"]}), deadline)
      raise Error, "document changed while awaiting save formatting" unless buffer.version == version
      apply_save_format(buffer, version, edits)
      ran = true
    end
    action_clients = route_language_clients(language.name, clients, "codeAction").select do |client|
      provider = client.capabilities["codeActionProvider"]
      provider == true || provider.is_a?(Hash)
    end
    unless action_clients.empty?
      kinds.each { |kind| ran = run_code_action_on_save(action_clients, buffer, kind, deadline) || ran }
    end
    ran
  ensure
    buffer.end_undo_group
    clients.each { |client| @save_action_errors&.delete(client) }
  end
end

#semantic_spans_for(buffer, row) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/canopus/workspace/language_aware.rb', line 69

def semantic_spans_for(buffer, row)
  saved = @semantic_styles && @semantic_styles[buffer]
  return [] unless saved && saved.first == buffer.version
  base = buffer.rope.line_start(row)
  map = {"class" => "Name.Class", "type" => "Name.Class", "function" => "Name.Function", "method" => "Name.Function",
    "keyword" => "Keyword", "comment" => "Comment", "string" => "Literal.String", "number" => "Literal.Number"}
  saved.last.filter_map do |token|
    next unless token[:line] == row
    first = buffer.rope.offset_at_utf16_point(Denebola::Point.new(row, token[:character]))
    last = buffer.rope.offset_at_utf16_point(Denebola::Point.new(row, token[:character] + token[:length]))
    [first - base, last - base, @theme.token_color(map.fetch(token[:name], "Name"))]
  end
end

#show_breadcrumb_menu(pane, current, item, context) ⇒ Object



2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
# File 'lib/canopus/workspace/language_aware.rb', line 2689

def show_breadcrumb_menu(pane, current, item, context)
  return false unless breadcrumb_context_valid?(pane, current, context) && context[:items].any? { |entry| entry.equal?(item) }

  activate_tab(pane, current)
  if item[:kind] == :path
    source = item[:value]
    return false unless source
    directory = File.dirname(source)
    values = files.select { |path| File.dirname(path) == directory }
    labels = values
  else
    source = item[:value]
    values = document_symbol_siblings(current, source, generation: context[:generation])
      .select { |symbol| breadcrumb_symbol_category(symbol) == breadcrumb_symbol_category(source) }
    labels = breadcrumb_symbol_labels(current.buffer, values)
  end
  return false if values.empty?

  self.palette = {kind: :breadcrumbs, query: +"", index: 0, matches: labels, items: values,
    breadcrumb_kind: item[:kind], breadcrumb_source: item, breadcrumb_context: context,
    breadcrumb_editor: current, breadcrumb_pane: pane}
  update_palette
  true
end

#show_diagnostics ⇒ Object



1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
# File 'lib/canopus/workspace/language_aware.rb', line 1293

def show_diagnostics
  entries = @diagnostics.all
  labels = entries.map do |entry|
    row = entry.diagnostic.dig("range", "start", "line")
    "#{File.basename(Sadr::Protocol.path(entry.uri))}:#{row + 1} #{entry.diagnostic['message']}"
  end
  items = entries.map { |entry| {"uri" => entry.uri, "range" => entry.diagnostic.fetch("range")} }
  self.palette = {kind: :locations, query: +"", index: 0, matches: labels, items: items}
  update_palette
end

#show_outline ⇒ Object



1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
# File 'lib/canopus/workspace/language_aware.rb', line 1303

def show_outline
  if editor.language_document.definition.name == "ruby"
    document = editor.language_document
    symbols = document.outline
    self.palette = {kind: :outline, query: +"", index: 0, matches: symbols.map { |symbol| "#{'  ' * symbol.depth}#{symbol.name}" }, items: symbols,
      editor: editor, document: document, version: editor.buffer.version, loading: !document.syntax_ready?, partial: !document.syntax_complete?}
    @message = @palette[:loading] ? "Analyzing outline…" : @palette[:partial] ? "Outline (visible region only)" : "Outline"
  else
    language_request(:documentSymbol)
  end
end

#shrink_selection(current = editor) ⇒ Object



1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
# File 'lib/canopus/workspace/language_aware.rb', line 1348

def shrink_selection(current = editor)
  state = @selection_range_states&.[](current)
  unless state && selection_range_state_valid?(current, state) && !state[:history].empty?
    invalidate_selection_ranges(editor: current)
    @message = "No smaller selection"
    return false
  end

  previous, chains = state[:history].pop
  current.set_selections(previous, merge: false)
  state[:current] = previous
  state[:chains] = chains
  current.reveal_cursor
  @message = "Selection shrunk"
  @window&.request_frame
  true
end

#sticky_context(current, display_row) ⇒ Object



2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
# File 'lib/canopus/workspace/language_aware.rb', line 2611

def sticky_context(current, display_row)
  return [] unless sticky_scroll_enabled?(current)

  map = current.display_map
  first = display_row.clamp(0, map.row_count - 1)
  entry = document_symbol_entry(current)
  return [] unless entry
  top = map.to_buffer(DisplayPoint.new(first, 0))
  maximum = language_setting(current, "sticky_scroll", "max_lines")
  context_key = [entry[:generation], map.tree.object_id, first, top, maximum]
  contexts = @sticky_context_cache ||= {}
  return contexts[context_key] if contexts.key?(context_key)

  result = document_symbol_chain(current, top)
    .select { |symbol| map.to_display(symbol.selection.begin).row < first }
    .last(maximum).freeze
  contexts.shift while contexts.length >= 256
  contexts[context_key] = result
end

#sticky_scroll_enabled?(current) ⇒ Boolean

Returns:

  • (Boolean)


2603
2604
2605
# File 'lib/canopus/workspace/language_aware.rb', line 2603

def sticky_scroll_enabled?(current)
  language_setting(current, "sticky_scroll", "enabled")
end

#visible_code_lens_ranges(current, display_rows) ⇒ Object



2543
2544
2545
# File 'lib/canopus/workspace/language_aware.rb', line 2543

def visible_code_lens_ranges(current, display_rows)
  visible_language_ranges(current, display_rows, gap: 1)
end

#visible_inlay_hint_ranges(current, display_rows) ⇒ Object



2397
2398
2399
# File 'lib/canopus/workspace/language_aware.rb', line 2397

def visible_inlay_hint_ranges(current, display_rows)
  visible_language_ranges(current, display_rows, gap: 101)
end