mt-lang 0.3.38 → 0.3.39

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.
@@ -37,11 +37,15 @@ module MilkTea
37
37
  result.empty? ? nil : result
38
38
  end
39
39
 
40
+ TRIGGER_KIND_INCOMPLETE = 2
41
+ MAX_COMPLETION_SESSION_ENTRIES = 64
42
+
40
43
  def handle_completion(params)
41
44
  stages = new_perf_stages
42
45
  total_start = stages ? monotonic_time : nil
43
46
  uri = params['textDocument']['uri']
44
47
  @current_completion_uri = uri
48
+ @current_completion_trigger_kind = params.dig('context', 'triggerKind') || 1
45
49
  lsp_line = params['position']['line']
46
50
  lsp_char = params['position']['character']
47
51
  branch = 'none'
@@ -49,50 +53,61 @@ module MilkTea
49
53
 
50
54
  prefix = measure_perf_stage(stages, 'prefix') { current_word_prefix(uri, lsp_line, lsp_char) }
51
55
 
56
+ # Completion session re-filtering: when the editor keeps requesting
57
+ # while the prefix grows (triggerFromIncompleteCompletions), re-filter
58
+ # the previously computed candidate pool instead of rebuilding it.
59
+ response = measure_perf_stage(stages, 'session') { completion_session_response(uri, lsp_line, lsp_char, prefix) }
60
+ if response
61
+ branch = 'session'
62
+ item_count = response[:items].length
63
+ return response
64
+ end
65
+
66
+ branch, item_count, response = measure_perf_stage(stages, 'compute') { compute_completion_items(uri, lsp_line, lsp_char, prefix, stages) }
67
+ store_completion_session(uri, lsp_line, lsp_char, prefix, response)
68
+ response
69
+ rescue StandardError => e
70
+ branch = 'error'
71
+ warn "Error in completion handler: #{e.message}"
72
+ { isIncomplete: false, items: [] }
73
+ ensure
74
+ log_request_stage_breakdown('textDocument/completion', total_start, uri: uri, stages: stages, summary: "branch=#{branch} items=#{item_count}")
75
+ end
76
+
77
+ def compute_completion_items(uri, lsp_line, lsp_char, prefix, stages)
52
78
  import_items = measure_perf_stage(stages, 'import_context') { import_completions(uri, lsp_line, lsp_char) }
53
79
  if import_items
54
- branch = 'import'
55
- item_count = import_items.length
56
- return { isIncomplete: false, items: import_items }
80
+ return ['import', import_items.length, { isIncomplete: false, items: import_items }]
57
81
  end
58
82
 
59
83
  facts = measure_perf_stage(stages, 'facts') { @workspace.get_facts(uri) }
60
84
 
61
85
  unless facts
62
- branch = 'no-facts'
63
- return { isIncomplete: false, items: [] }
86
+ return ['no-facts', 0, { isIncomplete: false, items: [] }]
64
87
  end
65
88
 
66
89
  # Attribute context: complete inside @[...]
67
90
  attr_items = attribute_completions(facts, uri, lsp_line, lsp_char)
68
91
  if attr_items
69
- branch = 'attribute'
70
- item_count = attr_items.length
71
- return { isIncomplete: false, items: attr_items }
92
+ return ['attribute', attr_items.length, { isIncomplete: false, items: attr_items }]
72
93
  end
73
94
 
74
95
  # Format string interpolation: complete inside f"... #{ }
75
96
  fmt_items = format_string_completions(facts, uri, lsp_line, lsp_char)
76
97
  if fmt_items
77
- branch = 'format-string'
78
- item_count = fmt_items.length
79
- return { isIncomplete: false, items: fmt_items }
98
+ return ['format-string', fmt_items.length, { isIncomplete: false, items: fmt_items }]
80
99
  end
81
100
 
82
101
  # Named argument completions: inside function/struct call e.g. Point(x: 1, |)
83
102
  named_items = named_argument_completions(facts, uri, lsp_line, lsp_char)
84
103
  if named_items
85
- branch = 'named-arg'
86
- item_count = named_items.length
87
- return { isIncomplete: false, items: named_items }
104
+ return ['named-arg', named_items.length, { isIncomplete: false, items: named_items }]
88
105
  end
89
106
 
90
107
  # Specialization context: inside name[...]
91
108
  spec_items = specialization_completions(facts, uri, lsp_line, lsp_char)
92
109
  if spec_items
93
- branch = 'specialization'
94
- item_count = spec_items.length
95
- return { isIncomplete: false, items: spec_items }
110
+ return ['specialization', spec_items.length, { isIncomplete: false, items: spec_items }]
96
111
  end
97
112
 
98
113
  # When user is typing after '.', return module members or method completions.
@@ -105,7 +120,6 @@ module MilkTea
105
120
  if dot_recv
106
121
  # Module member access: rl.init_window, rl.RAYWHITE, etc.
107
122
  if (module_binding = facts.imports[dot_recv])
108
- branch = 'module'
109
123
  items = measure_perf_stage(stages, 'build') do
110
124
  result = []
111
125
  module_binding.functions.each do |fname, binding|
@@ -152,8 +166,7 @@ module MilkTea
152
166
  end
153
167
  result
154
168
  end
155
- item_count = items.length
156
- return { isIncomplete: false, items: items }
169
+ return ['module', items.length, { isIncomplete: false, items: items }]
157
170
  end
158
171
  if (type_receiver = measure_perf_stage(stages, 'type_receiver') { resolve_type_receiver_info(facts, dot_recv, dot_recv_path) })
159
172
  receiver_label = type_receiver[:label]
@@ -162,7 +175,6 @@ module MilkTea
162
175
 
163
176
  # Enum/Flags member access: Color.RED, KeyboardKey.A, etc.
164
177
  if type.is_a?(Types::EnumBase)
165
- branch = 'enum-members'
166
178
  items = measure_perf_stage(stages, 'build') do
167
179
  type.members.filter_map do |mname|
168
180
  next if !prefix.empty? && !mname.start_with?(prefix)
@@ -176,13 +188,11 @@ module MilkTea
176
188
  }
177
189
  end
178
190
  end
179
- item_count = items.length
180
- return { isIncomplete: false, items: items }
191
+ return ['enum-members', items.length, { isIncomplete: false, items: items }]
181
192
  end
182
193
 
183
194
  # Variant arm access: Option.none, Result.success, etc.
184
195
  if type.is_a?(Types::Variant)
185
- branch = 'variant-arms'
186
196
  items = measure_perf_stage(stages, 'build') do
187
197
  type.arm_names.filter_map do |aname|
188
198
  next if !prefix.empty? && !aname.start_with?(prefix)
@@ -196,13 +206,11 @@ module MilkTea
196
206
  }
197
207
  end
198
208
  end
199
- item_count = items.length
200
- return { isIncomplete: false, items: items }
209
+ return ['variant-arms', items.length, { isIncomplete: false, items: items }]
201
210
  end
202
211
 
203
212
  # Nested struct type members: ShapeGroup.CircleData, etc.
204
213
  if type.is_a?(Types::Struct) && type.respond_to?(:nested_types) && type.nested_types.any?
205
- branch = 'nested-types'
206
214
  items = measure_perf_stage(stages, 'build') do
207
215
  type.nested_types.filter_map do |nt_name, _nt_type|
208
216
  next if !prefix.empty? && !nt_name.start_with?(prefix)
@@ -216,15 +224,12 @@ module MilkTea
216
224
  }
217
225
  end
218
226
  end
219
- item_count = items.length
220
- return { isIncomplete: false, items: items } unless items.empty?
227
+ return ['nested-types', items.length, { isIncomplete: false, items: items }] unless items.empty?
221
228
  end
222
229
 
223
230
  items = measure_perf_stage(stages, 'build') { completion_items_for_type_receiver(facts, type, prefix) }
224
231
  unless items.empty?
225
- branch = 'type-receiver'
226
- item_count = items.length
227
- return { isIncomplete: false, items: items }
232
+ return ['type-receiver', items.length, { isIncomplete: false, items: items }]
228
233
  end
229
234
  end
230
235
 
@@ -237,17 +242,13 @@ module MilkTea
237
242
  receiver_type = measure_perf_stage(stages, 'imported_value_receiver') { val_binding.type }
238
243
  items = measure_perf_stage(stages, 'build') { completion_items_for_value_receiver(facts, receiver_type, prefix) }
239
244
  unless items.empty?
240
- branch = 'imported-value-receiver'
241
- item_count = items.length
242
- return { isIncomplete: false, items: items }
245
+ return ['imported-value-receiver', items.length, { isIncomplete: false, items: items }]
243
246
  end
244
247
  end
245
248
  else
246
249
  chain_items = measure_perf_stage(stages, 'value_chain') { value_chain_completions(facts, dot_recv_path, lsp_line, lsp_char, prefix) }
247
250
  if chain_items
248
- branch = 'value-chain'
249
- item_count = chain_items.length
250
- return { isIncomplete: false, items: chain_items }
251
+ return ['value-chain', chain_items.length, { isIncomplete: false, items: chain_items }]
251
252
  end
252
253
  end
253
254
  end
@@ -255,14 +256,11 @@ module MilkTea
255
256
  if (receiver_type = measure_perf_stage(stages, 'value_receiver') { resolve_dot_receiver_value_type(facts, dot_recv, lsp_line + 1, lsp_char + 1) })
256
257
  items = measure_perf_stage(stages, 'build') { completion_items_for_value_receiver(facts, receiver_type, prefix) }
257
258
  unless items.empty?
258
- branch = 'value-receiver'
259
- item_count = items.length
260
- return { isIncomplete: false, items: items }
259
+ return ['value-receiver', items.length, { isIncomplete: false, items: items }]
261
260
  end
262
261
  end
263
262
 
264
263
  # Method completions on a non-module receiver.
265
- branch = 'method-fallback'
266
264
  method_items = measure_perf_stage(stages, 'build') do
267
265
  result = []
268
266
  facts.methods.each do |_recv_type, methods|
@@ -296,10 +294,9 @@ module MilkTea
296
294
  method_items = method_items.first(MAX_COMPLETION_ITEMS)
297
295
  item_count = MAX_COMPLETION_ITEMS
298
296
  end
299
- return { isIncomplete: truncated, items: method_items }
297
+ return ['method-fallback', item_count, { isIncomplete: truncated, items: method_items }]
300
298
  end
301
299
 
302
- branch = 'global'
303
300
  items = measure_perf_stage(stages, 'build') do
304
301
  result = []
305
302
  function_docs_cache = {}
@@ -450,13 +447,51 @@ module MilkTea
450
447
  items = items.first(MAX_COMPLETION_ITEMS)
451
448
  item_count = MAX_COMPLETION_ITEMS
452
449
  end
453
- { isIncomplete: truncated, items: items }
454
- rescue StandardError => e
455
- branch = 'error'
456
- warn "Error in completion handler: #{e.message}"
457
- { isIncomplete: false, items: [] }
458
- ensure
459
- log_request_stage_breakdown('textDocument/completion', total_start, uri: uri, stages: stages, summary: "branch=#{branch} items=#{item_count}")
450
+ ['global', item_count, { isIncomplete: truncated, items: items }]
451
+ end
452
+
453
+ # Serve a previously computed candidate pool when the editor is
454
+ # re-triggering with a longer prefix (triggerFromIncompleteCompletions)
455
+ # on the same line. Safe only when the text before the cursor is a strict
456
+ # extension of the cached context and the word prefix has grown; re-filter
457
+ # the (already prefix-filtered) pool with the longer prefix.
458
+ def completion_session_response(uri, lsp_line, lsp_char, prefix)
459
+ return nil if prefix.empty?
460
+ return nil unless @current_completion_trigger_kind == TRIGGER_KIND_INCOMPLETE
461
+
462
+ content = @workspace.get_content(uri)
463
+ line_prefix = (content.split("\n", -1)[lsp_line] || '')[0...lsp_char]
464
+ cached = @completion_session_cache[[uri, lsp_line]]
465
+ return nil unless cached
466
+ return nil unless line_prefix.start_with?(cached[:line_prefix])
467
+ return nil unless prefix.start_with?(cached[:prefix])
468
+
469
+ items = cached[:items].select { |item| item[:label].to_s.start_with?(prefix) }
470
+ { isIncomplete: cached[:isIncomplete], items: items }
471
+ end
472
+
473
+ def store_completion_session(uri, lsp_line, lsp_char, prefix, response)
474
+ return if prefix.empty?
475
+
476
+ content = @workspace.get_content(uri)
477
+ line_prefix = (content.split("\n", -1)[lsp_line] || '')[0...lsp_char]
478
+ key = [uri, lsp_line]
479
+ @completion_session_cache[key] = {
480
+ line_prefix: line_prefix,
481
+ prefix: prefix,
482
+ isIncomplete: response[:isIncomplete],
483
+ items: response[:items],
484
+ }
485
+ if @completion_session_order_set.add?(key)
486
+ @completion_session_order << key
487
+ evict_completion_sessions while @completion_session_order.length > MAX_COMPLETION_SESSION_ENTRIES
488
+ end
489
+ end
490
+
491
+ def evict_completion_sessions
492
+ key = @completion_session_order.shift
493
+ @completion_session_cache.delete(key)
494
+ @completion_session_order_set.delete(key)
460
495
  end
461
496
 
462
497
  def value_chain_completions(facts, dot_recv_path, lsp_line, lsp_char, prefix)
@@ -821,6 +856,11 @@ module MilkTea
821
856
  return params if name.to_s.empty?
822
857
 
823
858
  uri = data['uri'] || ''
859
+ key = [uri, name]
860
+ if @completion_resolve_cache.key?(key)
861
+ return @completion_resolve_cache[key]
862
+ end
863
+
824
864
  definition_entry = @workspace.find_definition_token_global(name, preferred_uri: uri)
825
865
  return params unless definition_entry
826
866
 
@@ -828,7 +868,7 @@ module MilkTea
828
868
  docs = signature_help_markdown_for_doc_comment(doc_comment)
829
869
  return params if docs.empty?
830
870
 
831
- params.merge('documentation' => { 'kind' => 'markdown', 'value' => docs })
871
+ @completion_resolve_cache[key] = params.merge('documentation' => { 'kind' => 'markdown', 'value' => docs })
832
872
  rescue StandardError => e
833
873
  warn "Error in completion resolve handler: #{e.message}"
834
874
  params
@@ -862,6 +902,7 @@ module MilkTea
862
902
 
863
903
  current_path = uri_to_path(uri)
864
904
  return nil unless current_path
905
+ current_path = File.expand_path(current_path)
865
906
 
866
907
  roots = MilkTea::ModuleRoots.roots_for_path(current_path)
867
908
  fs_dir = dir_segments.join(File::SEPARATOR)
@@ -870,23 +911,13 @@ module MilkTea
870
911
  filter_lower = filter.downcase
871
912
 
872
913
  roots.each do |root|
873
- search_dir = fs_dir.empty? ? root : File.join(root, fs_dir)
874
- next unless File.directory?(search_dir)
875
-
876
- Dir.children(search_dir).sort.each do |name|
877
- next if name.start_with?('.')
878
- full_path = File.join(search_dir, name)
879
-
880
- if name.end_with?('.mt')
881
- mod_name = name.delete_suffix('.mt')
882
- next if mod_name.start_with?('.')
883
- next if full_path == current_path
884
- next unless filter.empty? || mod_name.downcase.start_with?(filter_lower)
885
- modules[mod_name] = mod_name
886
- elsif File.directory?(full_path) && module_dir_contains_mt?(full_path)
887
- next unless filter.empty? || name.downcase.start_with?(filter_lower)
888
- modules[name] = name
889
- end
914
+ names = @workspace.module_importable_names(root, fs_dir, current_path: current_path)
915
+ next unless names
916
+
917
+ names.each_key do |mod_name|
918
+ next unless filter.empty? || mod_name.downcase.start_with?(filter_lower)
919
+
920
+ modules[mod_name] = mod_name
890
921
  end
891
922
  end
892
923
 
@@ -904,14 +935,6 @@ module MilkTea
904
935
  end
905
936
  end
906
937
 
907
- def module_dir_contains_mt?(dir)
908
- Dir.children(dir).any? do |name|
909
- next false if name.start_with?('.')
910
- full = File.join(dir, name)
911
- name.end_with?('.mt') || (File.directory?(full) && module_dir_contains_mt?(full))
912
- end
913
- end
914
-
915
938
  def attribute_completions(facts, uri, line, char)
916
939
  content = @workspace.get_content(uri)
917
940
  return nil unless content
@@ -142,6 +142,10 @@ module MilkTea
142
142
  end
143
143
  end
144
144
  end
145
+ # Fresh facts just landed for this document; let the editor
146
+ # re-fetch semantic tokens so analyzed highlighting replaces the
147
+ # lexical fallback that was served while facts were unavailable.
148
+ refresh_client_semantic_tokens
145
149
  elsif perf_logging?
146
150
  @diagnostics_perf[:dropped_stale] += 1
147
151
  end
@@ -223,12 +227,6 @@ module MilkTea
223
227
  dependency_export_surface_fingerprint(previous_content) != dependency_export_surface_fingerprint(current_content)
224
228
  end
225
229
 
226
- def semantic_tokens_allow_last_good_fallback?(uri)
227
- @diagnostics_mutex.synchronize do
228
- @diagnostics_pending.key?(uri) || @diagnostics_enqueued.include?(uri)
229
- end
230
- end
231
-
232
230
  def notify_diagnostic_errors(uri, diagnostics)
233
231
  errors = diagnostics.select { |d| d.is_a?(Hash) && (d["severity"] || d[:severity]) == 1 }
234
232
  return if errors.empty?
@@ -10,19 +10,29 @@ module MilkTea
10
10
  stages = new_perf_stages
11
11
  total_start = stages ? monotonic_time : nil
12
12
  uri = params['textDocument']['uri']
13
+
14
+ content_hash = @workspace.get_content(uri).hash
15
+ facts = @workspace.get_facts(uri)
16
+ facts_id = facts&.object_id
17
+ cached = @document_symbol_cache[uri]
18
+ if cached && cached[:content_hash] == content_hash && cached[:facts_id] == facts_id
19
+ return cached[:result]
20
+ end
21
+
13
22
  symbols = measure_perf_stage(stages, 'symbols') { @workspace.get_symbols(uri) }
14
23
  result = measure_perf_stage(stages, 'format') { symbols.map { |sym| format_document_symbol(sym) } }
15
24
 
16
- # Enrich with hierarchical children from AST
17
- ast = @workspace.get_ast(uri)
25
+ # Enrich with hierarchical children from AST. Use the facts' own AST so
26
+ # binding_resolution node object_ids line up with the outline locals.
27
+ ast = facts&.ast || @workspace.get_ast(uri)
18
28
  if ast && result
19
- facts = @workspace.get_facts(uri)
20
29
  enrich_with_children(result, ast, facts)
21
30
  end
22
31
 
23
32
  module_name = resolve_outline_module_name(uri)
24
33
  result = wrap_in_module_hierarchy(result, module_name, uri) if module_name && result&.any?
25
34
 
35
+ @document_symbol_cache[uri] = { content_hash: content_hash, facts_id: facts_id, result: result }
26
36
  result
27
37
  rescue StandardError => e
28
38
  warn "Error in documentSymbol handler: #{e.message}"