Module: MilkTea::LSP::Server::ServerDiagnosticsScheduling
- Included in:
- MilkTea::LSP::Server
- Defined in:
- lib/milk_tea/lsp/server/diagnostics_scheduling.rb
Constant Summary collapse
- SURFACE_DECL_LINE =
Declaration-prefix regex. Unlike the old form, it also covers the
const functioncompound, async/foreign/external/editable/static modifiers, andattributedeclarations.static_assertis not matched:staticrequires whitespace before the keyword, and a bare module-level statement cannot be a declaration. %r{\A(?:(?:public|foreign|external|async|editable|static|const)\s+)*(?:function|struct|union|enum|flags|variant|interface|event|type|const|var|extending|opaque|attribute)\b}- SURFACE_FIELD_LINE =
A bare
name: Typeline. Only struct/union fields (and continuation parameter lines) take this shape; local declarations uselet/var, named arguments use=, and match-arm labels start with a keyword or pattern. Requires a type-like token after the colon so_:arm labels are not treated as surface. /\A[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Za-z_\[\]]/
Instance Method Summary collapse
- #cancel_diagnostics(uri) ⇒ Object
- #collect_diagnostics_for_content(uri, _content, lint_tier: :full) ⇒ Object
-
#dependency_export_surface_fingerprint(content) ⇒ Object
Over-approximation of the module's externally-observable surface.
- #dependency_import_fingerprint(content) ⇒ Object
- #dependency_refresh_required_for_edit?(changed_uri, previous_content, current_content) ⇒ Boolean
- #drain_diagnostics_queue ⇒ Object
- #lint_tier_rank(lint_tier) ⇒ Object
- #more_strict_lint_tier(a, b) ⇒ Object
- #notify_diagnostic_errors(uri, diagnostics) ⇒ Object
- #process_diagnostics_for_uri(uri) ⇒ Object
- #schedule_diagnostics(uri, force: false, lint_tier: :full) ⇒ Object
- #start_diagnostics_workers ⇒ Object
- #stop_diagnostics_workers ⇒ Object
Instance Method Details
#cancel_diagnostics(uri) ⇒ Object
55 56 57 58 59 60 61 62 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 55 def cancel_diagnostics(uri) @diagnostics_mutex.synchronize do @diagnostics_generation[uri] += 1 @diagnostics_pending.delete(uri) @diagnostics_last_scheduled_hash.delete(uri) @diagnostics_perf[:cancelled] += 1 if perf_logging? end end |
#collect_diagnostics_for_content(uri, _content, lint_tier: :full) ⇒ Object
172 173 174 175 176 177 178 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 172 def collect_diagnostics_for_content(uri, _content, lint_tier: :full) @workspace.collect_diagnostics(uri, lint_tier: lint_tier) rescue StandardError => e warn "LSP diagnostics error #{uri}: #{e.}" warn " #{e.backtrace.first(6).join("\n ")}" if e.backtrace [] end |
#dependency_export_surface_fingerprint(content) ⇒ Object
Over-approximation of the module's externally-observable surface. It must never MISS a surface change (a miss leaves shared-cache analyses of dependents stale); false positives only trigger a harmless re-analysis. In addition to declaration lines it captures:
- `@[...]` attributes (packed/align/deprecated change layout/docs)
- multi-line declaration headers, so parameter/signature edits on
continuation lines are detected
- struct/union field lines (`name: Type`)
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 227 def dependency_export_surface_fingerprint(content) lines = content.to_s.lines.map(&:strip) surface = [] i = 0 while i < lines.length line = lines[i] if line.empty? || line.start_with?('#') i += 1 next end if line.start_with?('@[') || line.match?(SURFACE_DECL_LINE) || line.match?(SURFACE_FIELD_LINE) surface << line # Multi-line header: absorb continuation lines until the # terminating ':' so edits inside the header are surfaced too. unless line.end_with?(':') i += 1 while i < lines.length cont = lines[i] break if cont.empty? surface << cont i += 1 break if cont.end_with?(':') end next end end i += 1 end surface.join("\n") end |
#dependency_import_fingerprint(content) ⇒ Object
195 196 197 198 199 200 201 202 203 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 195 def dependency_import_fingerprint(content) content.to_s.each_line.filter_map do |line| stripped = line.strip next if stripped.empty? || stripped.start_with?('#') next unless stripped.start_with?('import ') stripped end.join("\n") end |
#dependency_refresh_required_for_edit?(changed_uri, previous_content, current_content) ⇒ Boolean
259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 259 def dependency_refresh_required_for_edit?(changed_uri, previous_content, current_content) return false if previous_content == current_content return true if dependency_import_fingerprint(previous_content) != dependency_import_fingerprint(current_content) # Keep the open-document dependency index fresh (related_open_document_uris # updates it as a side effect) so dependent tracking stays accurate. @workspace.(changed_uri) # A surface edit can invalidate shared-cache analyses of NON-open # dependents (their cached entries are recomputed against this module # on their next pull), so clearing must not be gated on there being # open dependents to refresh. dependency_export_surface_fingerprint(previous_content) != dependency_export_surface_fingerprint(current_content) end |
#drain_diagnostics_queue ⇒ Object
103 104 105 106 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 103 def drain_diagnostics_queue @diagnostics_queue.clear nil end |
#lint_tier_rank(lint_tier) ⇒ Object
180 181 182 183 184 185 186 187 188 189 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 180 def lint_tier_rank(lint_tier) case Linter.normalize_lint_tier(lint_tier) when :full 2 when :fast 1 else 0 end end |
#more_strict_lint_tier(a, b) ⇒ Object
191 192 193 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 191 def more_strict_lint_tier(a, b) lint_tier_rank(a) >= lint_tier_rank(b) ? Linter.normalize_lint_tier(a) : Linter.normalize_lint_tier(b) end |
#notify_diagnostic_errors(uri, diagnostics) ⇒ Object
274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 274 def notify_diagnostic_errors(uri, diagnostics) errors = diagnostics.select { |d| d.is_a?(Hash) && (d["severity"] || d[:severity]) == 1 } return if errors.empty? @notified_error_uris ||= Set.new return if @notified_error_uris.include?(uri) @notified_error_uris.add(uri) path = uri_to_path(uri) || uri short_path = path.split("/").last(1).join (:warning, "#{short_path}: #{errors.length} error#{errors.length == 1 ? '' : 's'}") end |
#process_diagnostics_for_uri(uri) ⇒ Object
108 109 110 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 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 108 def process_diagnostics_for_uri(uri) loop do snapshot = nil @diagnostics_mutex.synchronize do snapshot = @diagnostics_pending.delete(uri) end break unless snapshot @diagnostics_perf[:dequeued] += 1 if perf_logging? diagnostics = collect_diagnostics_for_content(uri, snapshot[:content], lint_tier: snapshot[:lint_tier]) publish = false @diagnostics_mutex.synchronize do publish = snapshot[:generation] == @diagnostics_generation[uri] end if publish if defined?(@pull_diagnostics_active) && @pull_diagnostics_active @diagnostics_perf[:collected_for_pull] += 1 if perf_logging? else @diagnostics_perf[:published] += 1 if perf_logging? @protocol.write_notification('textDocument/publishDiagnostics', { uri: uri, diagnostics: diagnostics }) notify_diagnostic_errors(uri, diagnostics) cross = @workspace.cross_file_diagnostics if cross&.any? cross.each do |target_uri, items| @protocol.write_notification('textDocument/publishDiagnostics', { uri: target_uri, diagnostics: items }) end end end # Fresh facts just landed for this document; let the editor # re-fetch semantic tokens so analyzed highlighting replaces the # lexical fallback that was served while facts were unavailable. refresh_client_semantic_tokens elsif perf_logging? @diagnostics_perf[:dropped_stale] += 1 end end ensure requeue = false @diagnostics_mutex.synchronize do @diagnostics_enqueued.delete(uri) if @diagnostics_pending.key?(uri) @diagnostics_enqueued << uri requeue = true end end if requeue @diagnostics_perf[:requeued] += 1 if perf_logging? @diagnostics_queue << uri if perf_logging? @diagnostics_perf[:queue_peak] = [@diagnostics_perf[:queue_peak], @diagnostics_queue.length].max end end end |
#schedule_diagnostics(uri, force: false, lint_tier: :full) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 7 def schedule_diagnostics(uri, force: false, lint_tier: :full) content = @workspace.get_content(uri) content_digest = Digest::SHA256.hexdigest(content) normalized_lint_tier = Linter.normalize_lint_tier(lint_tier) enqueue = false @diagnostics_mutex.synchronize do previous = @diagnostics_last_scheduled_hash[uri] if !force && previous && previous[:digest] == content_digest && lint_tier_rank(previous[:lint_tier]) >= lint_tier_rank(normalized_lint_tier) @diagnostics_perf[:skipped_unchanged] += 1 if perf_logging? return end @diagnostics_generation[uri] += 1 @diagnostics_last_scheduled_hash[uri] = { digest: content_digest, lint_tier: normalized_lint_tier, } @diagnostics_perf[:scheduled] += 1 if perf_logging? pending = @diagnostics_pending[uri] pending_lint_tier = pending ? pending[:lint_tier] : normalized_lint_tier merged_lint_tier = if pending && pending[:content] == content more_strict_lint_tier(pending_lint_tier, normalized_lint_tier) else normalized_lint_tier end @diagnostics_pending[uri] = { generation: @diagnostics_generation[uri], content: content, lint_tier: merged_lint_tier, } unless @diagnostics_enqueued.include?(uri) @diagnostics_enqueued << uri enqueue = true end end if enqueue @diagnostics_queue << uri if perf_logging? @diagnostics_perf[:queue_peak] = [@diagnostics_perf[:queue_peak], @diagnostics_queue.length].max end end end |
#start_diagnostics_workers ⇒ Object
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 64 def start_diagnostics_workers return if @diagnostics_workers.any?(&:alive?) DIAGNOSTICS_WORKER_COUNT.times do |index| @diagnostics_workers << Thread.new do if Thread.current.respond_to?(:name=) Thread.current.name = "mt-lsp-diagnostics-#{index + 1}" end loop do uri = @diagnostics_queue.pop break if uri == :__stop__ process_diagnostics_for_uri(uri) end rescue StandardError => e warn "LSP diagnostics worker error: #{e.}" warn " #{e.backtrace.first(8).join("\n ")}" if e.backtrace end end end |
#stop_diagnostics_workers ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/milk_tea/lsp/server/diagnostics_scheduling.rb', line 86 def stop_diagnostics_workers workers = @diagnostics_workers return if workers.empty? workers.length.times { @diagnostics_queue << :__stop__ } workers.each do |worker| worker.join(1.0) next unless worker.alive? worker.kill worker.join end @diagnostics_workers = [] rescue StandardError => e warn "LSP diagnostics worker shutdown error: #{e.}" end |