mt-lang 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/AUTHORS +3 -0
  4. data/Gemfile +19 -0
  5. data/Gemfile.lock +82 -0
  6. data/LICENSE +21 -0
  7. data/README.md +1208 -0
  8. data/Rakefile +332 -0
  9. data/bin/mtc +8 -0
  10. data/bin/profile-mtc-checks +133 -0
  11. data/bin/tracy-profiler +0 -0
  12. data/docs/build-guide.md +536 -0
  13. data/docs/index.html +2778 -0
  14. data/docs/language-design.md +1519 -0
  15. data/docs/language-manual.md +1534 -0
  16. data/lib/milk_tea/base.rb +49 -0
  17. data/lib/milk_tea/bindings/bindgen/ast_parser.rb +398 -0
  18. data/lib/milk_tea/bindings/bindgen/declaration.rb +319 -0
  19. data/lib/milk_tea/bindings/bindgen/emitter.rb +387 -0
  20. data/lib/milk_tea/bindings/bindgen/overrides.rb +134 -0
  21. data/lib/milk_tea/bindings/bindgen/type_mapper.rb +622 -0
  22. data/lib/milk_tea/bindings/bindgen.rb +207 -0
  23. data/lib/milk_tea/bindings/cli.rb +121 -0
  24. data/lib/milk_tea/bindings/imported_bindings/defaults.rb +210 -0
  25. data/lib/milk_tea/bindings/imported_bindings/generator.rb +1114 -0
  26. data/lib/milk_tea/bindings/imported_bindings/method_source.rb +190 -0
  27. data/lib/milk_tea/bindings/imported_bindings/naming.rb +286 -0
  28. data/lib/milk_tea/bindings/imported_bindings.rb +215 -0
  29. data/lib/milk_tea/bindings/opengl_registry.rb +547 -0
  30. data/lib/milk_tea/bindings/raw_bindings/defaults.rb +1338 -0
  31. data/lib/milk_tea/bindings/raw_bindings.rb +269 -0
  32. data/lib/milk_tea/bindings/steamworks.rb +629 -0
  33. data/lib/milk_tea/bindings/upstream_sources.rb +352 -0
  34. data/lib/milk_tea/bindings/vendored_box2d.rb +79 -0
  35. data/lib/milk_tea/bindings/vendored_c_library.rb +322 -0
  36. data/lib/milk_tea/bindings/vendored_cjson.rb +52 -0
  37. data/lib/milk_tea/bindings/vendored_flecs.rb +78 -0
  38. data/lib/milk_tea/bindings/vendored_glfw.rb +77 -0
  39. data/lib/milk_tea/bindings/vendored_libuv.rb +75 -0
  40. data/lib/milk_tea/bindings/vendored_pcre2.rb +84 -0
  41. data/lib/milk_tea/bindings/vendored_raylib.rb +131 -0
  42. data/lib/milk_tea/bindings/vendored_sdl3.rb +78 -0
  43. data/lib/milk_tea/bindings/vendored_steamworks.rb +71 -0
  44. data/lib/milk_tea/bindings/vendored_tool.rb +71 -0
  45. data/lib/milk_tea/bindings/vendored_tools.rb +23 -0
  46. data/lib/milk_tea/bindings/vendored_tracy.rb +37 -0
  47. data/lib/milk_tea/bindings.rb +23 -0
  48. data/lib/milk_tea/core/ast.rb +311 -0
  49. data/lib/milk_tea/core/async_runtime_installer.rb +28 -0
  50. data/lib/milk_tea/core/binding_types.rb +18 -0
  51. data/lib/milk_tea/core/bindings/attribute_binding.rb +58 -0
  52. data/lib/milk_tea/core/bindings/function_binding.rb +5 -0
  53. data/lib/milk_tea/core/bindings/module_binding.rb +58 -0
  54. data/lib/milk_tea/core/bindings/value_binding.rb +21 -0
  55. data/lib/milk_tea/core/c_backend/aggregate_utils.rb +103 -0
  56. data/lib/milk_tea/core/c_backend/control_flow_emission.rb +412 -0
  57. data/lib/milk_tea/core/c_backend/expressions.rb +468 -0
  58. data/lib/milk_tea/core/c_backend/feature_detection.rb +483 -0
  59. data/lib/milk_tea/core/c_backend/reachability.rb +398 -0
  60. data/lib/milk_tea/core/c_backend/reinterpret.rb +226 -0
  61. data/lib/milk_tea/core/c_backend/runtime_helpers.rb +1080 -0
  62. data/lib/milk_tea/core/c_backend/statements.rb +563 -0
  63. data/lib/milk_tea/core/c_backend/type_collectors.rb +1545 -0
  64. data/lib/milk_tea/core/c_backend/type_declaration.rb +223 -0
  65. data/lib/milk_tea/core/c_backend/type_system.rb +345 -0
  66. data/lib/milk_tea/core/c_backend.rb +287 -0
  67. data/lib/milk_tea/core/compatibility_helpers.rb +79 -0
  68. data/lib/milk_tea/core/compile_time/const_eval.rb +187 -0
  69. data/lib/milk_tea/core/compile_time.rb +329 -0
  70. data/lib/milk_tea/core/control_flow/builder.rb +582 -0
  71. data/lib/milk_tea/core/control_flow/constant_propagation.rb +143 -0
  72. data/lib/milk_tea/core/control_flow/dataflow.rb +74 -0
  73. data/lib/milk_tea/core/control_flow/definite_assignment.rb +79 -0
  74. data/lib/milk_tea/core/control_flow/graph.rb +90 -0
  75. data/lib/milk_tea/core/control_flow/liveness.rb +24 -0
  76. data/lib/milk_tea/core/control_flow/nullability_flow.rb +43 -0
  77. data/lib/milk_tea/core/control_flow/reachability.rb +22 -0
  78. data/lib/milk_tea/core/control_flow/termination.rb +31 -0
  79. data/lib/milk_tea/core/control_flow.rb +34 -0
  80. data/lib/milk_tea/core/cst.rb +48 -0
  81. data/lib/milk_tea/core/cst_builder.rb +19 -0
  82. data/lib/milk_tea/core/ir.rb +85 -0
  83. data/lib/milk_tea/core/keywords.rb +97 -0
  84. data/lib/milk_tea/core/lexer/character_classes.rb +60 -0
  85. data/lib/milk_tea/core/lexer/format_strings.rb +225 -0
  86. data/lib/milk_tea/core/lexer/heredocs.rb +199 -0
  87. data/lib/milk_tea/core/lexer/indentation.rb +62 -0
  88. data/lib/milk_tea/core/lexer/numbers.rb +96 -0
  89. data/lib/milk_tea/core/lexer/recovery.rb +32 -0
  90. data/lib/milk_tea/core/lexer/strings.rb +167 -0
  91. data/lib/milk_tea/core/lexer/symbols.rb +71 -0
  92. data/lib/milk_tea/core/lexer/trivia.rb +53 -0
  93. data/lib/milk_tea/core/lexer.rb +430 -0
  94. data/lib/milk_tea/core/lowering/artifacts.rb +26 -0
  95. data/lib/milk_tea/core/lowering/async/analysis.rb +245 -0
  96. data/lib/milk_tea/core/lowering/async/lowering.rb +1399 -0
  97. data/lib/milk_tea/core/lowering/async/normalization.rb +459 -0
  98. data/lib/milk_tea/core/lowering/async.rb +714 -0
  99. data/lib/milk_tea/core/lowering/block.rb +1052 -0
  100. data/lib/milk_tea/core/lowering/calls.rb +1565 -0
  101. data/lib/milk_tea/core/lowering/declarations.rb +214 -0
  102. data/lib/milk_tea/core/lowering/dyn.rb +206 -0
  103. data/lib/milk_tea/core/lowering/events.rb +1054 -0
  104. data/lib/milk_tea/core/lowering/expressions.rb +1645 -0
  105. data/lib/milk_tea/core/lowering/foreign_cstr.rb +206 -0
  106. data/lib/milk_tea/core/lowering/functions.rb +242 -0
  107. data/lib/milk_tea/core/lowering/loops.rb +1087 -0
  108. data/lib/milk_tea/core/lowering/lowering_context.rb +80 -0
  109. data/lib/milk_tea/core/lowering/proc.rb +419 -0
  110. data/lib/milk_tea/core/lowering/resolve.rb +2516 -0
  111. data/lib/milk_tea/core/lowering/scans.rb +220 -0
  112. data/lib/milk_tea/core/lowering/str_buffer.rb +125 -0
  113. data/lib/milk_tea/core/lowering/utils.rb +1453 -0
  114. data/lib/milk_tea/core/lowering.rb +378 -0
  115. data/lib/milk_tea/core/module_binder.rb +181 -0
  116. data/lib/milk_tea/core/module_loader/errors.rb +21 -0
  117. data/lib/milk_tea/core/module_loader.rb +479 -0
  118. data/lib/milk_tea/core/module_path_resolver.rb +153 -0
  119. data/lib/milk_tea/core/module_roots.rb +88 -0
  120. data/lib/milk_tea/core/parser/attributes.rb +71 -0
  121. data/lib/milk_tea/core/parser/blocks.rb +119 -0
  122. data/lib/milk_tea/core/parser/declarations.rb +749 -0
  123. data/lib/milk_tea/core/parser/expressions.rb +624 -0
  124. data/lib/milk_tea/core/parser/recovery.rb +131 -0
  125. data/lib/milk_tea/core/parser/statements.rb +756 -0
  126. data/lib/milk_tea/core/parser/types.rb +271 -0
  127. data/lib/milk_tea/core/parser.rb +400 -0
  128. data/lib/milk_tea/core/prelude_installer.rb +29 -0
  129. data/lib/milk_tea/core/pretty_printer/ast_formatter.rb +917 -0
  130. data/lib/milk_tea/core/pretty_printer/base_formatter.rb +87 -0
  131. data/lib/milk_tea/core/pretty_printer/ir_formatter.rb +300 -0
  132. data/lib/milk_tea/core/pretty_printer.rb +17 -0
  133. data/lib/milk_tea/core/semantic_analyzer/analysis_context.rb +314 -0
  134. data/lib/milk_tea/core/semantic_analyzer/attributes.rb +184 -0
  135. data/lib/milk_tea/core/semantic_analyzer/calls.rb +992 -0
  136. data/lib/milk_tea/core/semantic_analyzer/expressions.rb +1735 -0
  137. data/lib/milk_tea/core/semantic_analyzer/flow_refinement.rb +355 -0
  138. data/lib/milk_tea/core/semantic_analyzer/foreign_functions.rb +155 -0
  139. data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +354 -0
  140. data/lib/milk_tea/core/semantic_analyzer/generics.rb +383 -0
  141. data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +78 -0
  142. data/lib/milk_tea/core/semantic_analyzer/module_context.rb +35 -0
  143. data/lib/milk_tea/core/semantic_analyzer/name_resolution.rb +1438 -0
  144. data/lib/milk_tea/core/semantic_analyzer/nullability.rb +421 -0
  145. data/lib/milk_tea/core/semantic_analyzer/statements.rb +1308 -0
  146. data/lib/milk_tea/core/semantic_analyzer/top_level.rb +588 -0
  147. data/lib/milk_tea/core/semantic_analyzer/type_compatibility.rb +307 -0
  148. data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +851 -0
  149. data/lib/milk_tea/core/semantic_analyzer.rb +327 -0
  150. data/lib/milk_tea/core/token.rb +26 -0
  151. data/lib/milk_tea/core/token_stream.rb +30 -0
  152. data/lib/milk_tea/core/types/layout.rb +243 -0
  153. data/lib/milk_tea/core/types/predicates.rb +609 -0
  154. data/lib/milk_tea/core/types/registry.rb +83 -0
  155. data/lib/milk_tea/core/types/types.rb +1696 -0
  156. data/lib/milk_tea/core/types/visitor.rb +442 -0
  157. data/lib/milk_tea/core.rb +25 -0
  158. data/lib/milk_tea/dap/backends/lldb_dap.rb +158 -0
  159. data/lib/milk_tea/dap/protocol.rb +58 -0
  160. data/lib/milk_tea/dap/server/breakpoints.rb +66 -0
  161. data/lib/milk_tea/dap/server/debug_map.rb +291 -0
  162. data/lib/milk_tea/dap/server/handlers.rb +374 -0
  163. data/lib/milk_tea/dap/server/launch.rb +261 -0
  164. data/lib/milk_tea/dap/server/lldb_backend.rb +380 -0
  165. data/lib/milk_tea/dap/server/pause_diagnostics.rb +109 -0
  166. data/lib/milk_tea/dap/server/utilities.rb +160 -0
  167. data/lib/milk_tea/dap/server/wire.rb +55 -0
  168. data/lib/milk_tea/dap/server.rb +131 -0
  169. data/lib/milk_tea/dap/session.rb +152 -0
  170. data/lib/milk_tea/dap.rb +6 -0
  171. data/lib/milk_tea/lsp/dependency_resolution.rb +52 -0
  172. data/lib/milk_tea/lsp/diagnostics.rb +611 -0
  173. data/lib/milk_tea/lsp/protocol.rb +104 -0
  174. data/lib/milk_tea/lsp/server/call_hierarchy.rb +274 -0
  175. data/lib/milk_tea/lsp/server/code_actions.rb +446 -0
  176. data/lib/milk_tea/lsp/server/code_lens.rb +97 -0
  177. data/lib/milk_tea/lsp/server/completion.rb +1099 -0
  178. data/lib/milk_tea/lsp/server/configuration.rb +167 -0
  179. data/lib/milk_tea/lsp/server/debug_info.rb +45 -0
  180. data/lib/milk_tea/lsp/server/definition.rb +779 -0
  181. data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +239 -0
  182. data/lib/milk_tea/lsp/server/execute_command.rb +25 -0
  183. data/lib/milk_tea/lsp/server/folding_range.rb +153 -0
  184. data/lib/milk_tea/lsp/server/formatting.rb +575 -0
  185. data/lib/milk_tea/lsp/server/hover.rb +1465 -0
  186. data/lib/milk_tea/lsp/server/inlay_hints.rb +204 -0
  187. data/lib/milk_tea/lsp/server/lifecycle.rb +234 -0
  188. data/lib/milk_tea/lsp/server/linked_editing_range.rb +43 -0
  189. data/lib/milk_tea/lsp/server/on_type_formatting.rb +73 -0
  190. data/lib/milk_tea/lsp/server/progress.rb +47 -0
  191. data/lib/milk_tea/lsp/server/references.rb +433 -0
  192. data/lib/milk_tea/lsp/server/rename.rb +598 -0
  193. data/lib/milk_tea/lsp/server/selection_range.rb +130 -0
  194. data/lib/milk_tea/lsp/server/semantic_tokens.rb +1745 -0
  195. data/lib/milk_tea/lsp/server/signature_help.rb +200 -0
  196. data/lib/milk_tea/lsp/server/text_documents.rb +125 -0
  197. data/lib/milk_tea/lsp/server/type_hierarchy.rb +167 -0
  198. data/lib/milk_tea/lsp/server/utilities.rb +520 -0
  199. data/lib/milk_tea/lsp/server.rb +415 -0
  200. data/lib/milk_tea/lsp/workspace/analysis.rb +209 -0
  201. data/lib/milk_tea/lsp/workspace/caches.rb +260 -0
  202. data/lib/milk_tea/lsp/workspace/collection.rb +98 -0
  203. data/lib/milk_tea/lsp/workspace/definition_index.rb +184 -0
  204. data/lib/milk_tea/lsp/workspace/dependency_graph.rb +282 -0
  205. data/lib/milk_tea/lsp/workspace/store.rb +154 -0
  206. data/lib/milk_tea/lsp/workspace/utilities.rb +490 -0
  207. data/lib/milk_tea/lsp/workspace.rb +127 -0
  208. data/lib/milk_tea/lsp.rb +13 -0
  209. data/lib/milk_tea/packages/atomic_write.rb +63 -0
  210. data/lib/milk_tea/packages/dependency_solver.rb +194 -0
  211. data/lib/milk_tea/packages/graph.rb +139 -0
  212. data/lib/milk_tea/packages/lock.rb +421 -0
  213. data/lib/milk_tea/packages/manager_cli.rb +485 -0
  214. data/lib/milk_tea/packages/manifest.rb +356 -0
  215. data/lib/milk_tea/packages/manifest_editor.rb +134 -0
  216. data/lib/milk_tea/packages/registry_metadata_provider.rb +34 -0
  217. data/lib/milk_tea/packages/registry_store.rb +420 -0
  218. data/lib/milk_tea/packages/services.rb +41 -0
  219. data/lib/milk_tea/packages/source_cache.rb +71 -0
  220. data/lib/milk_tea/packages/source_fetcher.rb +124 -0
  221. data/lib/milk_tea/packages/source_resolver.rb +389 -0
  222. data/lib/milk_tea/packages/version.rb +164 -0
  223. data/lib/milk_tea/packages.rb +16 -0
  224. data/lib/milk_tea/tooling/asset_pack.rb +141 -0
  225. data/lib/milk_tea/tooling/build.rb +1124 -0
  226. data/lib/milk_tea/tooling/build_cache.rb +240 -0
  227. data/lib/milk_tea/tooling/cli.rb +2688 -0
  228. data/lib/milk_tea/tooling/cst_formatter.rb +13 -0
  229. data/lib/milk_tea/tooling/debug_info_formatter.rb +456 -0
  230. data/lib/milk_tea/tooling/debug_map.rb +248 -0
  231. data/lib/milk_tea/tooling/docs_app.rb +369 -0
  232. data/lib/milk_tea/tooling/error_formatter.rb +86 -0
  233. data/lib/milk_tea/tooling/formatter.rb +787 -0
  234. data/lib/milk_tea/tooling/linter/doc_tags.rb +212 -0
  235. data/lib/milk_tea/tooling/linter/fix_engine.rb +237 -0
  236. data/lib/milk_tea/tooling/linter/flow_rules.rb +380 -0
  237. data/lib/milk_tea/tooling/linter/imports_platform.rb +841 -0
  238. data/lib/milk_tea/tooling/linter/release_rules.rb +744 -0
  239. data/lib/milk_tea/tooling/linter/reserved_names.rb +199 -0
  240. data/lib/milk_tea/tooling/linter/rules.rb +593 -0
  241. data/lib/milk_tea/tooling/linter/source_helpers.rb +407 -0
  242. data/lib/milk_tea/tooling/linter/trailing_comma.rb +139 -0
  243. data/lib/milk_tea/tooling/linter/visitors.rb +1286 -0
  244. data/lib/milk_tea/tooling/linter.rb +798 -0
  245. data/lib/milk_tea/tooling/project_scaffold.rb +82 -0
  246. data/lib/milk_tea/tooling/public/css/docs.css +166 -0
  247. data/lib/milk_tea/tooling/public/js/docs.js +94 -0
  248. data/lib/milk_tea/tooling/run.rb +408 -0
  249. data/lib/milk_tea/tooling/templates/wasm_shell.html +48 -0
  250. data/lib/milk_tea/tooling/toolchain_cli.rb +157 -0
  251. data/lib/milk_tea/tooling/views/404.erb +7 -0
  252. data/lib/milk_tea/tooling/views/index.erb +87 -0
  253. data/lib/milk_tea/tooling/views/layout.erb +69 -0
  254. data/lib/milk_tea/tooling/views/module.erb +97 -0
  255. data/lib/milk_tea/tooling/views/stdlib.erb +21 -0
  256. data/lib/milk_tea/tooling.rb +20 -0
  257. data/lib/milk_tea.rb +8 -0
  258. metadata +426 -0
@@ -0,0 +1,446 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ module LSP
5
+ class Server
6
+ FakeWarning = Struct.new(:line, :column, :length, :symbol_name, keyword_init: true)
7
+
8
+ module ServerCodeActions
9
+ private
10
+
11
+ def handle_code_action(params)
12
+ uri = params.dig('textDocument', 'uri')
13
+ return [] unless uri
14
+
15
+ total_start = monotonic_time
16
+ content = @workspace.get_content(uri)
17
+ return [] unless content
18
+
19
+ only_kinds = params.dig('context', 'only')
20
+ want_quickfix = only_kinds.nil? || only_kinds.any? { |k| k == 'quickFix' || k.start_with?('quickFix.') }
21
+ want_fixall = !only_kinds.nil? && only_kinds.any? { |k| k == 'source.fixAll' || k == 'source' || k.start_with?('source.') }
22
+
23
+ actions = []
24
+ lines = content.lines
25
+ requested_diagnostics = params.dig('context', 'diagnostics') || []
26
+ reserved_primitive_name_fixes = nil
27
+
28
+ quickfix_start = monotonic_time
29
+
30
+ # ── Per-diagnostic quickfix actions ──────────────────────────────────
31
+ if want_quickfix
32
+ requested_diagnostics.each do |diag|
33
+ code = diag['code']
34
+ message = diag['message'].to_s
35
+ diag_line = diag.dig('range', 'start', 'line').to_i + 1 # 1-based
36
+ diag_start_char = diag.dig('range', 'start', 'character').to_i
37
+ diag_end_char = diag.dig('range', 'end', 'character').to_i
38
+ source_line = lines[diag_line - 1].to_s
39
+
40
+ if message.start_with?('cannot assign ') && !source_line.empty?
41
+ expected_type = message[/\bexpected\s+(.+)\z/, 1]&.strip
42
+ equal_index = source_line.index('=')
43
+ if expected_type && !expected_type.empty? && equal_index
44
+ rhs = source_line[(equal_index + 1)..]&.strip
45
+ if rhs && !rhs.empty? && !rhs.start_with?("#{expected_type}<-")
46
+ indent = source_line[/\A\s*/] || ''
47
+ lhs = source_line[0..equal_index].rstrip
48
+ simple_value = rhs.match?(/\A(?:[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*|[0-9][0-9_]*(?:\.[0-9][0-9_]*)?|0x[0-9A-Fa-f_]+|0b[01_]+|0o[0-7_]+)\z/)
49
+ casted_rhs = simple_value ? "#{expected_type}<-#{rhs}" : "#{expected_type}<-(#{rhs})"
50
+ new_line = "#{indent}#{lhs} #{casted_rhs}\n"
51
+ actions << {
52
+ title: "Cast expression to #{expected_type}",
53
+ kind: 'quickFix',
54
+ diagnostics: [diag],
55
+ edit: {
56
+ changes: {
57
+ uri => [{
58
+ range: {
59
+ start: { line: diag_line - 1, character: 0 },
60
+ end: { line: diag_line, character: 0 }
61
+ },
62
+ newText: new_line
63
+ }]
64
+ }
65
+ }
66
+ }
67
+ end
68
+ end
69
+ end
70
+
71
+ case code
72
+ when 'reserved-primitive-name'
73
+ reserved_primitive_name_fixes ||= Linter.collect_reserved_primitive_name_fixes(content, path: uri)
74
+ fix = reserved_primitive_name_fixes.find do |candidate|
75
+ declaration_site = candidate.sites.first
76
+ declaration_site.line == diag_line && declaration_site.column == (diag_start_char + 1)
77
+ end
78
+ next unless fix
79
+
80
+ edits = fix.sites.uniq { |site| [site.line, site.column] }.sort_by { |site| [site.line, site.column] }.map do |site|
81
+ {
82
+ range: {
83
+ start: { line: site.line - 1, character: site.column - 1 },
84
+ end: { line: site.line - 1, character: site.column - 1 + site.length }
85
+ },
86
+ newText: fix.replacement_name
87
+ }
88
+ end
89
+
90
+ actions << {
91
+ title: "Rename '#{fix.original_name}' to '#{fix.replacement_name}'",
92
+ kind: 'quickFix',
93
+ diagnostics: [diag],
94
+ edit: {
95
+ changes: {
96
+ uri => edits
97
+ }
98
+ }
99
+ }
100
+
101
+ when 'prefer-let', 'redundant-ignored-match-binding', 'prefer-let-else',
102
+ 'prefer-var-else', 'redundant-bool-compare', 'redundant-else',
103
+ 'redundant-return', 'unused-import', 'trailing-list-comma'
104
+ fake_warning = FakeWarning.new(
105
+ line: diag_line,
106
+ column: diag_start_char + 1,
107
+ length: (diag_end_char - diag_start_char).positive? ? diag_end_char - diag_start_char : nil,
108
+ symbol_name: message[/'([^']+)'/, 1],
109
+ )
110
+ edits = Linter::FixEngine.edits_for_rule(code, lines, fake_warning)
111
+ next if edits.empty?
112
+
113
+ actions << {
114
+ title: Linter.quick_fix_title(code),
115
+ kind: 'quickFix',
116
+ diagnostics: [diag],
117
+ edit: { changes: { uri => Linter::FixEngine.edits_to_lsp_text_edits(edits, uri) } }
118
+ }
119
+
120
+ when 'line-too-long'
121
+ fix = Formatter.build_long_line_wrap_fix(
122
+ content,
123
+ diag_line - 1,
124
+ max_line_length: Formatter.resolve_max_line_length(uri),
125
+ path: uri,
126
+ )
127
+ next unless fix
128
+
129
+ actions << {
130
+ title: Linter.quick_fix_title('line-too-long'),
131
+ kind: 'quickFix',
132
+ diagnostics: [diag],
133
+ edit: {
134
+ changes: {
135
+ uri => [{
136
+ range: {
137
+ start: { line: fix[:start_line_idx], character: 0 },
138
+ end: { line: fix[:end_line_idx] + 1, character: 0 }
139
+ },
140
+ newText: fix[:new_text]
141
+ }]
142
+ }
143
+ }
144
+ }
145
+
146
+ when 'shadow'
147
+ # Offer to rename to _ prefix; editor will invoke textDocument/rename.
148
+ # This action just annotates — the actual rename is a client-side refactor.
149
+ actions << {
150
+ title: "Add '_' prefix to suppress shadow warning",
151
+ kind: 'quickFix',
152
+ diagnostics: [diag]
153
+ }
154
+
155
+ when 'unused-param'
156
+ next if source_line.empty?
157
+
158
+ token = source_line[diag_start_char...diag_end_char].to_s
159
+ token = token.strip
160
+ next if token.empty?
161
+ next if token.start_with?('_')
162
+
163
+ replacement = "_#{token.gsub(/\A_+/, '')}"
164
+ actions << {
165
+ title: "Rename parameter '#{token}' to '#{replacement}'",
166
+ kind: 'quickFix',
167
+ diagnostics: [diag],
168
+ edit: {
169
+ changes: {
170
+ uri => [{
171
+ range: {
172
+ start: { line: diag_line - 1, character: diag_start_char },
173
+ end: { line: diag_line - 1, character: diag_end_char }
174
+ },
175
+ newText: replacement
176
+ }]
177
+ }
178
+ }
179
+ }
180
+
181
+ when 'dead-assignment'
182
+ next if source_line.empty?
183
+
184
+ # Linter guarantees this write is overwritten before any read,
185
+ # so dropping the statement is semantics-preserving.
186
+ actions << {
187
+ title: 'Remove dead assignment',
188
+ kind: 'quickFix',
189
+ diagnostics: [diag],
190
+ edit: {
191
+ changes: {
192
+ uri => [{
193
+ range: {
194
+ start: { line: diag_line - 1, character: 0 },
195
+ end: { line: diag_line, character: 0 }
196
+ },
197
+ newText: ''
198
+ }]
199
+ }
200
+ }
201
+ }
202
+
203
+ else
204
+ next if source_line.empty?
205
+
206
+ # Wrap unsafe-required pointer casts into a local unsafe block.
207
+ if message == 'pointer cast requires unsafe' || message == 'ref to pointer cast requires unsafe'
208
+ next if source_line.match?(/\A\s*unsafe:\s*\z/)
209
+
210
+ indent = source_line[/\A\s*/] || ''
211
+ body = source_line.sub(/\A\s*/, '').rstrip
212
+ next if body.empty?
213
+
214
+ wrapped = "#{indent}unsafe:\n#{indent} #{body}\n"
215
+ actions << {
216
+ title: 'Wrap statement in unsafe block',
217
+ kind: 'quickFix',
218
+ diagnostics: [diag],
219
+ edit: {
220
+ changes: {
221
+ uri => [{
222
+ range: {
223
+ start: { line: diag_line - 1, character: 0 },
224
+ end: { line: diag_line, character: 0 }
225
+ },
226
+ newText: wrapped
227
+ }]
228
+ }
229
+ }
230
+ }
231
+ end
232
+
233
+ # "match on Foo is missing cases: A, B, C"
234
+ if message =~ /\Amatch on (\S+) is missing cases: (.+)\z/
235
+ _scrutinee_type = $1
236
+ missing = $2.split(', ').map(&:strip).reject(&:empty?)
237
+ next if missing.empty?
238
+
239
+ indent = source_line[/\A\s*/] || ''
240
+ arm_indent = indent + ' '
241
+ body_indent = arm_indent + ' '
242
+ new_arms = missing.map do |arm_name|
243
+ "#{arm_indent}#{arm_name}:\n#{body_indent}return\n"
244
+ end.join
245
+
246
+ match_end_line = find_match_end_line(lines, diag_line - 1)
247
+ insert_line = match_end_line || diag_line - 1
248
+
249
+ actions << {
250
+ title: "Add missing match #{missing.length == 1 ? 'arm' : 'arms'}: #{missing.join(', ')}",
251
+ kind: 'quickFix',
252
+ diagnostics: [diag],
253
+ edit: {
254
+ changes: {
255
+ uri => [{
256
+ range: {
257
+ start: { line: insert_line, character: 0 },
258
+ end: { line: insert_line, character: 0 }
259
+ },
260
+ newText: new_arms
261
+ }]
262
+ }
263
+ }
264
+ }
265
+ end
266
+
267
+ # "unknown type X" or "unknown name Y" with import suggestion
268
+ if message =~ /\Aunknown (?:type|callable) (\S+)\z/
269
+ _type_name = $1
270
+ suggestion = diag.dig('data', 'suggestion')
271
+ next unless suggestion.is_a?(String)
272
+
273
+ if suggestion =~ /did you mean ['"]([^'"]+)['"]/
274
+ full_path = $1
275
+ parts = full_path.split('.')
276
+ next unless parts.length >= 2
277
+
278
+ import_module = parts[0..-2].join('.')
279
+ _import_type = parts.last
280
+
281
+ actions << {
282
+ title: "Import #{full_path}",
283
+ kind: 'quickFix',
284
+ diagnostics: [diag],
285
+ edit: {
286
+ changes: {
287
+ uri => [{
288
+ range: {
289
+ start: { line: 0, character: 0 },
290
+ end: { line: 0, character: 0 }
291
+ },
292
+ newText: "import #{import_module}\n"
293
+ }]
294
+ }
295
+ }
296
+ }
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end # want_quickfix
302
+ quickfix_ms = elapsed_ms(quickfix_start)
303
+
304
+ # ── source.fixAll: apply all lint auto-fixes at once ───────────────
305
+ # Skip for files outside the workspace root (library files) and very
306
+ # large files to keep codeAction latency bounded.
307
+ fixall_ms = 0.0
308
+ fixall_skipped_reason = want_fixall ? skip_expensive_work_reason(uri, content) : 'not-requested'
309
+ unless fixall_skipped_reason
310
+ fixall_start = monotonic_time
311
+ begin
312
+ content_hash = content.hash
313
+ cached_fixall = @fixall_cache[uri]
314
+ if cached_fixall &&
315
+ cached_fixall[:content_hash] == content_hash
316
+ fixed = cached_fixall[:fixed]
317
+ else
318
+ fixed = begin
319
+ Linter.fix_source(content, path: uri)
320
+ rescue StandardError
321
+ content
322
+ end
323
+ @fixall_cache[uri] = {
324
+ content_hash: content_hash,
325
+ fixed: fixed,
326
+ }
327
+ end
328
+ if fixed != content
329
+ line_count = content.count("\n")
330
+ actions << {
331
+ title: Linter::FIX_ALL_TITLE,
332
+ kind: 'source.fixAll',
333
+ edit: {
334
+ changes: {
335
+ uri => [{
336
+ range: {
337
+ start: { line: 0, character: 0 },
338
+ end: { line: line_count + 1, character: 0 }
339
+ },
340
+ newText: fixed
341
+ }]
342
+ }
343
+ }
344
+ }
345
+ end
346
+ rescue StandardError => e
347
+ warn "Error building source.fixAll action: #{e.message}"
348
+ end
349
+ fixall_ms = elapsed_ms(fixall_start)
350
+ end
351
+
352
+ elapsed = elapsed_ms(total_start)
353
+ short_uri = shorten_uri(uri) || uri
354
+ fixall_detail = fixall_skipped_reason ? "skipped(#{fixall_skipped_reason})" : "generated(ms=#{fixall_ms})"
355
+ log_perf_breakdown('textDocument/codeAction', elapsed,
356
+ "uri=#{short_uri} bytes=#{content.bytesize} lines=#{content.count("\n") + 1} diagnostics=#{requested_diagnostics.length} actions=#{actions.length} fixAll=#{fixall_detail} stages_ms=quickfix:#{quickfix_ms},fixAll:#{fixall_ms}")
357
+
358
+ actions
359
+ rescue StandardError => e
360
+ warn "Error in codeAction handler: #{e.message}"
361
+ []
362
+ end
363
+
364
+ def handle_document_diagnostic(params)
365
+ uri = params.dig('textDocument', 'uri')
366
+ return { kind: 'full', items: [] } unless uri
367
+
368
+ content = @workspace.get_content(uri)
369
+ diagnostics = @workspace.collect_diagnostics(uri)
370
+ fingerprint = diagnostics_fingerprint(content, diagnostics)
371
+ previous_result_id = params['previousResultId']
372
+ cached = @diagnostic_report_cache[uri]
373
+
374
+ if cached && cached[:result_id] == previous_result_id && cached[:fingerprint] == fingerprint
375
+ return {
376
+ kind: 'unchanged',
377
+ resultId: cached[:result_id]
378
+ }
379
+ end
380
+
381
+ result_id = next_diagnostic_result_id(uri, fingerprint)
382
+ @diagnostic_report_cache[uri] = {
383
+ result_id: result_id,
384
+ fingerprint: fingerprint
385
+ }
386
+
387
+ {
388
+ kind: 'full',
389
+ resultId: result_id,
390
+ items: diagnostics
391
+ }
392
+ rescue StandardError => e
393
+ warn "Error in documentDiagnostic handler: #{e.message}"
394
+ { kind: 'full', items: [] }
395
+ end
396
+
397
+ def handle_workspace_diagnostic(params)
398
+ previous_ids = params['previousResultIds'] || []
399
+ prev_map = previous_ids.each_with_object({}) do |entry, h|
400
+ h[entry['uri']] = entry['value'] if entry.is_a?(Hash) && entry['uri']
401
+ end
402
+
403
+ all_uris = @workspace.open_document_uris
404
+ items = all_uris.filter_map do |uri|
405
+ content = @workspace.get_content(uri)
406
+ next if content.empty?
407
+
408
+ diagnostics = @workspace.collect_diagnostics(uri)
409
+ fingerprint = diagnostics_fingerprint(content, diagnostics)
410
+ result_id = next_diagnostic_result_id(uri, fingerprint)
411
+
412
+ cached = @workspace_diagnostic_cache[uri]
413
+ if cached && cached[:result_id] == prev_map[uri] && cached[:fingerprint] == fingerprint
414
+ { uri: uri, kind: 'unchanged', resultId: result_id, items: [] }
415
+ else
416
+ @workspace_diagnostic_cache[uri] = { result_id: result_id, fingerprint: fingerprint }
417
+ { uri: uri, kind: 'full', resultId: result_id, items: diagnostics }
418
+ end
419
+ end
420
+
421
+ { items: items }
422
+ rescue StandardError => e
423
+ warn "Error in workspace/diagnostic handler: #{e.message}"
424
+ { items: [] }
425
+ end
426
+
427
+ def refresh_workspace_diagnostics
428
+ @protocol.write_notification('workspace/diagnostic/refresh', nil)
429
+ end
430
+
431
+ def find_match_end_line(lines, match_start_idx)
432
+ return nil if match_start_idx >= lines.length
433
+
434
+ match_indent = lines[match_start_idx][/\A\s*/].length
435
+ (match_start_idx + 1...lines.length).each do |i|
436
+ line = lines[i]
437
+ next if line.strip.empty?
438
+ line_indent = line[/\A\s*/].length
439
+ return i if line_indent <= match_indent
440
+ end
441
+ nil
442
+ end
443
+ end
444
+ end
445
+ end
446
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MilkTea
4
+ module LSP
5
+ class Server
6
+ module ServerCodeLens
7
+ private
8
+
9
+ def handle_code_lens(params)
10
+ uri = params.dig("textDocument", "uri")
11
+ return [] unless uri
12
+
13
+ symbols = @workspace.get_symbols(uri)
14
+ return [] if symbols.empty?
15
+
16
+ function_symbols = symbols.select { |s| s[:kind].to_s == "function" && s[:line] }
17
+ function_symbols.map do |sym|
18
+ line = sym[:line] - 1
19
+ {
20
+ range: {
21
+ start: { line: line, character: 0 },
22
+ end: { line: line, character: 0 },
23
+ },
24
+ data: {
25
+ uri: uri,
26
+ name: sym[:name],
27
+ line: sym[:line],
28
+ column: sym[:column],
29
+ },
30
+ }
31
+ end
32
+ rescue StandardError => e
33
+ warn "Error in codeLens handler: #{e.message}"
34
+ []
35
+ end
36
+
37
+ def handle_code_lens_resolve(params)
38
+ data = params["data"] || params[:data]
39
+ return params unless data.is_a?(Hash) || data.respond_to?(:[])
40
+
41
+ name = data["name"] || data[:name]
42
+ return params if name.to_s.empty?
43
+
44
+ uri = data["uri"] || data[:uri]
45
+ count = if uri
46
+ facts = @workspace.get_facts(uri)
47
+ if facts && module_level_name?(facts, name)
48
+ level_kind = module_level_name?(facts, name)
49
+ if level_kind == :method
50
+ refs = static_method_code_lens_count(uri, name, data, facts)
51
+ refs || module_level_reference_locations(uri, name, facts, include_declaration: true).length
52
+ else
53
+ module_level_reference_locations(uri, name, facts, include_declaration: true).length
54
+ end
55
+ else
56
+ ast = @workspace.get_ast(uri)
57
+ if ast && module_level_ast_name?(ast, name)
58
+ refs = module_level_reference_locations(uri, name, nil, include_declaration: true)
59
+ refs.length
60
+ else
61
+ refs = @workspace.find_all_references(name)
62
+ refs ? refs.length : 0
63
+ end
64
+ end
65
+ else
66
+ refs = @workspace.find_all_references(name)
67
+ refs ? refs.length : 0
68
+ end
69
+
70
+ params["command"] = {
71
+ "title" => "#{count} reference#{count == 1 ? "" : "s"}",
72
+ "command" => "",
73
+ }
74
+
75
+ params
76
+ rescue StandardError => e
77
+ warn "Error in codeLens/resolve handler: #{e.message}"
78
+ params
79
+ end
80
+
81
+ def static_method_code_lens_count(uri, name, data, facts)
82
+ line = data["line"] || data[:line]
83
+ column = data["column"] || data[:column]
84
+ return nil unless line && column
85
+
86
+ token = @workspace.find_token_at(uri, line - 1, column - 1)
87
+ return nil unless token&.type == :identifier
88
+
89
+ target = resolve_static_type_reference_target(uri, token, facts)
90
+ return nil unless target
91
+
92
+ static_type_method_references(target, include_declaration: true).length
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end