Class: Rigor::LanguageServer::DiagnosticPublisher

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/language_server/diagnostic_publisher.rb

Overview

Converts buffer state into textDocument/publishDiagnostics notifications. Owns the Rigor Analysis::Runner orchestration for the per-buffer single-file scope path that editor mode v1 already supports — every publish_for(uri) call materialises a BufferBinding from the BufferTable entry, runs the Runner, and pushes the resulting LSP Diagnostic[] through the writer.

Debouncing is wired via an optional Debouncer injected at construction (delay defaults to 200ms quiet-time); without a debouncer each call blocks synchronously (primarily for specs). When several buffers' debounce timers elapse around the same moment, the PublishBatcher coalesces them into one #publish_many round dispatched across the fork-based worker pool (issue #142) instead of N independent, GVL-serialized Runner calls.

Constant Summary collapse

SEVERITY_MAP =

Maps Rigor severity symbols to LSP DiagnosticSeverity integers per spec § "Diagnostic":

1 = Error, 2 = Warning, 3 = Information, 4 = Hint.
{
  error: 1,
  warning: 2,
  info: 3,
  hint: 4
}.freeze
PROJECT_ROUND_KEY =

The single Debouncer key every save round shares (#246). Per-URI keys would let two saves start two concurrent rounds; one key means a burst collapses into the last one.

:__rigor_project_round__

Instance Method Summary collapse

Constructor Details

#initialize(writer:, buffer_table:, project_context:, debouncer: nil, debounce_seconds: 0.2) ⇒ DiagnosticPublisher

Returns a new instance of DiagnosticPublisher.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/rigor/language_server/diagnostic_publisher.rb', line 47

def initialize(writer:, buffer_table:, project_context:,
               debouncer: nil, debounce_seconds: 0.2)
  @writer = writer
  @buffer_table = buffer_table
  @project_context = project_context
  @debouncer = debouncer
  @debounce_seconds = debounce_seconds
  @round_lock = Mutex.new
  @round_running = false
  @round_pending = false
  # Issue #142 — coalesces buffers whose OWN debounce timers elapse close together into one
  # `#publish_many` round instead of N independent GVL-serialized `Runner` calls. Separate from
  # `@round_lock` above, which single-flights the whole-project SAVE round; this single-flights the
  # per-buffer DIDCHANGE batch instead. See `PublishBatcher` for the coalescing mechanics.
  @batcher = PublishBatcher.new(
    on_batch: ->(uris) { publish_many(uris) },
    on_error: ->(e) { warn "DiagnosticPublisher batch round: #{e.class}: #{e.message}" }
  )
end

Instance Method Details

#cancel_pendingObject

Cancels every in-flight debounced task. Called from Server#handle_shutdown so pending publishes don't fire against a closed STDOUT.



128
129
130
# File 'lib/rigor/language_server/diagnostic_publisher.rb', line 128

def cancel_pending
  @debouncer&.cancel_all
end

#publish_empty(uri) ⇒ Object

Publishes an EMPTY diagnostic array for uri. The LSP-spec idiom for "clear inline markers" — called from didClose so clients drop stale highlights when the user closes a buffer.



122
123
124
# File 'lib/rigor/language_server/diagnostic_publisher.rb', line 122

def publish_empty(uri)
  notify(uri, [])
end

#publish_for(uri) ⇒ Object

Run analysis for the buffer at uri (looked up in the BufferTable) and push a textDocument/publishDiagnostics notification. No-op when the URI isn't a file:// form or the buffer isn't currently open. When a Debouncer is wired, the analysis is scheduled async per the configured debounce_seconds and joins the batch coalescing layer (PublishBatcher) once its own quiet-time elapses; otherwise it runs inline (primarily for specs).



72
73
74
75
76
77
78
79
80
81
# File 'lib/rigor/language_server/diagnostic_publisher.rb', line 72

def publish_for(uri)
  path = Uri.to_path(uri)
  return if path.nil?

  if @debouncer
    @debouncer.schedule(uri, delay: @debounce_seconds) { @batcher.enqueue(uri) }
  else
    run_and_notify(uri, path)
  end
end

#publish_many(uris) ⇒ Object

Issue #142 — publishes N dirty buffers' diagnostics through ONE dispatch across the fork-based worker pool (Analysis::Runner::BufferPoolDispatcher) instead of N independent, GVL-serialized Runner calls. Each URI's OWN BufferBinding (its logical path bound to its OWN editor tempfile) travels with it, so a worker analyses that buffer's in-flight bytes — never the file as it sits on disk — even when several buffers are dispatched in the same round.

Degrades to #run_and_notify's existing single-buffer path when only one URI is eligible after filtering (a buffer closed mid-debounce-window is dropped; a desynchronised one publishes empty immediately) — a lone edit takes exactly the path it takes today. The dispatcher itself degrades to sequential in-process execution for any other precondition (see BufferPoolDispatcher#dispatchable?), so a pool that cannot start never fails a publish, only slows it back down to today's wall time.



95
96
97
98
99
100
101
# File 'lib/rigor/language_server/diagnostic_publisher.rb', line 95

def publish_many(uris)
  eligible = uris.uniq.filter_map { |uri| eligible_job(uri) }
  return if eligible.empty?
  return run_and_notify(eligible.first.fetch(:uri), eligible.first.fetch(:path)) if eligible.size == 1

  publish_batch(eligible)
end

#publish_project(saved_uri) ⇒ Object

Runs one whole-project save round and publishes to the publish set (#246). Called from didSave.

Analysis scope is the whole project; the PUBLISH SET is the open buffers that are not dirty, plus the buffer that was just saved. A dirty buffer is excluded because only its own didChange analysis has seen its bytes — publishing this round's on-disk answer for it would replace correct markers with markers for a file the user has already changed.

Scheduled through the same Debouncer the per-buffer path uses, under one project-wide key, so the dispatcher never blocks on it.



112
113
114
115
116
117
118
# File 'lib/rigor/language_server/diagnostic_publisher.rb', line 112

def publish_project(saved_uri)
  if @debouncer
    @debouncer.schedule(PROJECT_ROUND_KEY, delay: 0) { run_project_round(saved_uri) }
  else
    run_project_round(saved_uri)
  end
end