mt-lang 0.3.37 → 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.
- checksums.yaml +4 -4
- data/docs/lsp-performance.md +347 -0
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/core/c_backend/reinterpret.rb +2 -2
- data/lib/milk_tea/core/c_backend.rb +12 -0
- data/lib/milk_tea/core/compile_time.rb +46 -17
- data/lib/milk_tea/core/lexer.rb +12 -0
- data/lib/milk_tea/core/lowering/resolve.rb +2 -3
- data/lib/milk_tea/core/lowering.rb +12 -0
- data/lib/milk_tea/core/module_loader.rb +59 -37
- data/lib/milk_tea/core/parser.rb +12 -0
- data/lib/milk_tea/core/semantic_analyzer/attributes.rb +2 -1
- data/lib/milk_tea/core/semantic_analyzer/function_binding.rb +8 -20
- data/lib/milk_tea/core/semantic_analyzer/interface_conformance.rb +2 -1
- data/lib/milk_tea/core/semantic_analyzer/statements.rb +10 -10
- data/lib/milk_tea/core/semantic_analyzer/top_level.rb +9 -11
- data/lib/milk_tea/core/semantic_analyzer/type_declaration.rb +14 -5
- data/lib/milk_tea/core/semantic_analyzer.rb +6 -31
- data/lib/milk_tea/lsp/diagnostics.rb +15 -5
- data/lib/milk_tea/lsp/server/code_actions.rb +12 -2
- data/lib/milk_tea/lsp/server/completion.rb +100 -77
- data/lib/milk_tea/lsp/server/diagnostics_scheduling.rb +4 -6
- data/lib/milk_tea/lsp/server/formatting.rb +184 -60
- data/lib/milk_tea/lsp/server/hover.rb +321 -41
- data/lib/milk_tea/lsp/server/inlay_hints.rb +33 -11
- data/lib/milk_tea/lsp/server/lifecycle.rb +12 -0
- data/lib/milk_tea/lsp/server/references.rb +3 -1
- data/lib/milk_tea/lsp/server/selection_range.rb +4 -4
- data/lib/milk_tea/lsp/server/semantic_tokens.rb +63 -20
- data/lib/milk_tea/lsp/server/text_documents.rb +2 -2
- data/lib/milk_tea/lsp/server/type_hierarchy.rb +7 -7
- data/lib/milk_tea/lsp/server/utilities.rb +4 -0
- data/lib/milk_tea/lsp/server.rb +9 -0
- data/lib/milk_tea/lsp/workspace/analysis.rb +14 -3
- data/lib/milk_tea/lsp/workspace/caches.rb +17 -1
- data/lib/milk_tea/lsp/workspace/collection.rb +4 -0
- data/lib/milk_tea/lsp/workspace/module_index.rb +134 -0
- data/lib/milk_tea/lsp/workspace/store.rb +9 -4
- data/lib/milk_tea/lsp/workspace.rb +8 -0
- data/lib/milk_tea/packages/manifest.rb +9 -0
- metadata +5 -3
|
@@ -12,23 +12,21 @@ module MilkTea
|
|
|
12
12
|
|
|
13
13
|
content = @workspace.get_content(uri)
|
|
14
14
|
cache_key = content.hash
|
|
15
|
+
facts = @workspace.peek_facts(uri)
|
|
16
|
+
facts_id = facts&.object_id
|
|
15
17
|
cached = @semantic_tokens_cache[uri]
|
|
16
|
-
if cached && cached[:content_hash] == cache_key
|
|
18
|
+
if cached && cached[:content_hash] == cache_key && cached[:facts_id] == facts_id
|
|
17
19
|
elapsed = elapsed_ms(total_start)
|
|
18
20
|
short_uri = shorten_uri(uri) || uri
|
|
19
21
|
log_perf_breakdown('textDocument/semanticTokens/full', elapsed,
|
|
20
22
|
"uri=#{short_uri} bytes=#{content.bytesize} lines=#{content.count("\n") + 1} cache=hit data_len=#{cached[:data].length}")
|
|
21
|
-
return { data: cached[:data] }
|
|
23
|
+
return { resultId: cached[:result_id], data: cached[:data] }
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
tokens_start = monotonic_time
|
|
25
27
|
tokens = @workspace.get_tokens(uri) || []
|
|
26
28
|
tokens_ms = elapsed_ms(tokens_start)
|
|
27
29
|
|
|
28
|
-
facts_start = monotonic_time
|
|
29
|
-
facts = @workspace.get_facts(uri, allow_last_good_fallback: semantic_tokens_allow_last_good_fallback?(uri))
|
|
30
|
-
facts_ms = elapsed_ms(facts_start)
|
|
31
|
-
|
|
32
30
|
build_start = monotonic_time
|
|
33
31
|
semantic_entries = build_semantic_token_entries(tokens, facts)
|
|
34
32
|
build_ms = elapsed_ms(build_start)
|
|
@@ -37,21 +35,21 @@ module MilkTea
|
|
|
37
35
|
data = encode_semantic_tokens(semantic_entries)
|
|
38
36
|
encode_ms = elapsed_ms(encode_start)
|
|
39
37
|
|
|
40
|
-
@semantic_tokens_cache[uri] = { content_hash: cache_key, data: data }
|
|
41
|
-
|
|
42
38
|
result_id = next_semantic_token_result_id(uri)
|
|
39
|
+
@semantic_tokens_cache[uri] = { content_hash: cache_key, data: data, result_id: result_id, facts_id: facts_id }
|
|
43
40
|
@semantic_tokens_delta_cache[uri] = {
|
|
44
41
|
result_id: result_id,
|
|
45
42
|
content_hash: cache_key,
|
|
43
|
+
facts_id: facts_id,
|
|
46
44
|
entries: semantic_entries,
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
elapsed = elapsed_ms(total_start)
|
|
50
48
|
short_uri = shorten_uri(uri) || uri
|
|
51
49
|
log_perf_breakdown('textDocument/semanticTokens/full', elapsed,
|
|
52
|
-
"uri=#{short_uri} bytes=#{content.bytesize} lines=#{content.count("\n") + 1} cache=miss tokens=#{tokens.length} entries=#{semantic_entries.length} data_len=#{data.length} facts
|
|
50
|
+
"uri=#{short_uri} bytes=#{content.bytesize} lines=#{content.count("\n") + 1} cache=miss tokens=#{tokens.length} entries=#{semantic_entries.length} data_len=#{data.length} facts=#{facts_id ? 'on' : 'off'} stages_ms=tokens:#{tokens_ms},build:#{build_ms},encode:#{encode_ms}")
|
|
53
51
|
|
|
54
|
-
{ data: data }
|
|
52
|
+
{ resultId: result_id, data: data }
|
|
55
53
|
rescue StandardError => e
|
|
56
54
|
warn "Error in semanticTokens/full handler: #{e.message}"
|
|
57
55
|
{ data: [] }
|
|
@@ -69,18 +67,20 @@ module MilkTea
|
|
|
69
67
|
|
|
70
68
|
content = @workspace.get_content(uri)
|
|
71
69
|
cache_key = content.hash
|
|
72
|
-
|
|
70
|
+
facts = @workspace.peek_facts(uri)
|
|
71
|
+
facts_id = facts&.object_id
|
|
72
|
+
if cached[:content_hash] == cache_key && cached[:facts_id] == facts_id
|
|
73
73
|
return { resultId: cached[:result_id], edits: [] }
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
tokens = @workspace.get_tokens(uri) || []
|
|
77
|
-
facts = @workspace.get_facts(uri, allow_last_good_fallback: semantic_tokens_allow_last_good_fallback?(uri))
|
|
78
77
|
new_entries = build_semantic_token_entries(tokens, facts)
|
|
79
78
|
|
|
80
79
|
new_result_id = next_semantic_token_result_id(uri)
|
|
81
80
|
@semantic_tokens_delta_cache[uri] = {
|
|
82
81
|
result_id: new_result_id,
|
|
83
82
|
content_hash: cache_key,
|
|
83
|
+
facts_id: facts_id,
|
|
84
84
|
entries: new_entries,
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -110,7 +110,7 @@ module MilkTea
|
|
|
110
110
|
![:newline, :indent, :dedent, :eof].include?(t.type)
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
facts = @workspace.
|
|
113
|
+
facts = @workspace.peek_facts(uri)
|
|
114
114
|
semantic_entries = build_semantic_token_entries(range_tokens, facts)
|
|
115
115
|
|
|
116
116
|
entries_in_range = semantic_entries.select do |entry|
|
|
@@ -129,6 +129,7 @@ module MilkTea
|
|
|
129
129
|
# is O(1) per call instead of O(n), turning the overall build from O(n²) to O(n).
|
|
130
130
|
trivia_types = Set[:newline, :indent, :dedent, :eof]
|
|
131
131
|
@tokens_by_line_cache = Hash.new { |h, k| h[k] = [] }
|
|
132
|
+
@semantic_token_build_cache = {}
|
|
132
133
|
tokens.each { |t| @tokens_by_line_cache[t.line] << t unless trivia_types.include?(t.type) }
|
|
133
134
|
@attribute_name_semantic_overrides = build_attribute_name_semantic_overrides(tokens, facts)
|
|
134
135
|
|
|
@@ -153,6 +154,24 @@ module MilkTea
|
|
|
153
154
|
ensure
|
|
154
155
|
@tokens_by_line_cache = nil
|
|
155
156
|
@attribute_name_semantic_overrides = nil
|
|
157
|
+
@semantic_token_build_cache = nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Memoize facts-derived lookups for the duration of one semantic-token
|
|
161
|
+
# build. The build cache is scoped per build (set up and torn down in
|
|
162
|
+
# build_semantic_token_entries) so repeated per-token classifications do
|
|
163
|
+
# not re-scan frames/type scopes/types for every identifier token.
|
|
164
|
+
def semantic_build_memo?(key)
|
|
165
|
+
!@semantic_token_build_cache.nil? && @semantic_token_build_cache.key?(key)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def semantic_build_memo(key)
|
|
169
|
+
@semantic_token_build_cache[key][1] if @semantic_token_build_cache&.key?(key)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def store_semantic_build_memo(key, value)
|
|
173
|
+
@semantic_token_build_cache[key] = [:ok, value] if @semantic_token_build_cache
|
|
174
|
+
value
|
|
156
175
|
end
|
|
157
176
|
|
|
158
177
|
def fstring_interpolation_entries(fstring_tok, facts)
|
|
@@ -1053,11 +1072,23 @@ module MilkTea
|
|
|
1053
1072
|
|
|
1054
1073
|
def known_type_name?(facts, name)
|
|
1055
1074
|
return false unless facts
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1075
|
+
memo_key = [:known_type, facts.object_id, name]
|
|
1076
|
+
return semantic_build_memo(memo_key) if semantic_build_memo?(memo_key)
|
|
1077
|
+
|
|
1078
|
+
result = if facts.types.key?(name)
|
|
1079
|
+
true
|
|
1080
|
+
else
|
|
1081
|
+
found = false
|
|
1082
|
+
facts.types.each_value do |type|
|
|
1083
|
+
if type.respond_to?(:nested_types) && type.nested_types.key?(name)
|
|
1084
|
+
found = true
|
|
1085
|
+
break
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
found
|
|
1059
1089
|
end
|
|
1060
|
-
|
|
1090
|
+
|
|
1091
|
+
store_semantic_build_memo(memo_key, result)
|
|
1061
1092
|
end
|
|
1062
1093
|
|
|
1063
1094
|
def dot_nested_type_member?(tokens, index, facts)
|
|
@@ -1318,6 +1349,9 @@ module MilkTea
|
|
|
1318
1349
|
end
|
|
1319
1350
|
|
|
1320
1351
|
def type_parameter_names_in_scope(facts, line)
|
|
1352
|
+
memo_key = [:type_param_names, facts.object_id, line]
|
|
1353
|
+
return semantic_build_memo(memo_key) if semantic_build_memo?(memo_key)
|
|
1354
|
+
|
|
1321
1355
|
scopes = @type_parameter_scope_cache ||= {}
|
|
1322
1356
|
cached = scopes[facts.object_id]
|
|
1323
1357
|
unless cached
|
|
@@ -1331,11 +1365,15 @@ module MilkTea
|
|
|
1331
1365
|
scopes[facts.object_id] = cached
|
|
1332
1366
|
end
|
|
1333
1367
|
|
|
1368
|
+
result = []
|
|
1334
1369
|
cached.reverse_each do |scope|
|
|
1335
|
-
|
|
1370
|
+
if line >= scope[:start_line] && line <= scope[:end_line]
|
|
1371
|
+
result = scope[:names]
|
|
1372
|
+
break
|
|
1373
|
+
end
|
|
1336
1374
|
end
|
|
1337
1375
|
|
|
1338
|
-
|
|
1376
|
+
store_semantic_build_memo(memo_key, result)
|
|
1339
1377
|
end
|
|
1340
1378
|
|
|
1341
1379
|
def type_parameter_scopes_for_declaration(decl, end_line: Float::INFINITY)
|
|
@@ -1745,7 +1783,12 @@ module MilkTea
|
|
|
1745
1783
|
|
|
1746
1784
|
start_offset = prefix * 5
|
|
1747
1785
|
delete_count = old_mid * 5
|
|
1748
|
-
|
|
1786
|
+
|
|
1787
|
+
# The inserted segment must keep the relative encoding against the
|
|
1788
|
+
# last unchanged token preceding the edit, so encode the full token
|
|
1789
|
+
# stream from the document origin and slice out the middle segment.
|
|
1790
|
+
full_encoded = encode_semantic_tokens(new_entries)
|
|
1791
|
+
insert_tokens = full_encoded[start_offset...(new_entries.length - suffix) * 5]
|
|
1749
1792
|
|
|
1750
1793
|
[{ start: start_offset, deleteCount: delete_count, data: insert_tokens }]
|
|
1751
1794
|
end
|
|
@@ -10,7 +10,7 @@ module MilkTea
|
|
|
10
10
|
content = params['textDocument']['text']
|
|
11
11
|
source = @workspace.document_source(uri) || 'unknown'
|
|
12
12
|
open_start = monotonic_time
|
|
13
|
-
open_stats = @workspace.open_document(uri, content)
|
|
13
|
+
open_stats = @workspace.open_document(uri, content, warm_facts: false)
|
|
14
14
|
open_ms = elapsed_ms(open_start)
|
|
15
15
|
@semantic_tokens_cache.delete(uri)
|
|
16
16
|
@semantic_tokens_delta_cache.delete(uri)
|
|
@@ -38,7 +38,7 @@ module MilkTea
|
|
|
38
38
|
changes = params['contentChanges'] || []
|
|
39
39
|
previous_content = @workspace.get_content(uri)
|
|
40
40
|
|
|
41
|
-
@workspace.apply_incremental_changes(uri, changes)
|
|
41
|
+
@workspace.apply_incremental_changes(uri, changes, warm_facts: false)
|
|
42
42
|
|
|
43
43
|
invalidate_document_caches(uri)
|
|
44
44
|
current_content = @workspace.get_content(uri)
|
|
@@ -5,11 +5,11 @@ module MilkTea
|
|
|
5
5
|
class Server
|
|
6
6
|
module ServerTypeHierarchy
|
|
7
7
|
TYPE_KIND_MAP = {
|
|
8
|
-
struct:
|
|
9
|
-
enum:
|
|
10
|
-
flags:
|
|
11
|
-
variant:
|
|
12
|
-
union:
|
|
8
|
+
struct: 23,
|
|
9
|
+
enum: 10,
|
|
10
|
+
flags: 10,
|
|
11
|
+
variant: 23,
|
|
12
|
+
union: 23,
|
|
13
13
|
interface: 11,
|
|
14
14
|
}.freeze
|
|
15
15
|
|
|
@@ -115,12 +115,12 @@ module MilkTea
|
|
|
115
115
|
def find_type_info(facts, name)
|
|
116
116
|
if (t = facts.types[name])
|
|
117
117
|
ast = t.respond_to?(:ast_declaration) ? t.ast_declaration : nil
|
|
118
|
-
return { kind: type_kind_from_object(t), line: ast&.line, column: ast
|
|
118
|
+
return { kind: type_kind_from_object(t), line: ast&.line, column: ast&.column || 0 }
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
if (t = facts.interfaces[name])
|
|
122
122
|
ast = t.respond_to?(:ast_declaration) ? t.ast_declaration : nil
|
|
123
|
-
return { kind: :interface, line: ast&.line, column: ast
|
|
123
|
+
return { kind: :interface, line: ast&.line, column: ast&.column || 0 }
|
|
124
124
|
end
|
|
125
125
|
|
|
126
126
|
nil
|
|
@@ -177,6 +177,7 @@ module MilkTea
|
|
|
177
177
|
|
|
178
178
|
def handle_did_change_watched_files(params)
|
|
179
179
|
changes = params['changes'] || []
|
|
180
|
+
@workspace.apply_module_index_events(changes)
|
|
180
181
|
affected_uris = Set.new
|
|
181
182
|
changes.each do |change|
|
|
182
183
|
uri = change['uri']
|
|
@@ -198,6 +199,9 @@ module MilkTea
|
|
|
198
199
|
@semantic_tokens_cache.delete(uri)
|
|
199
200
|
@semantic_tokens_delta_cache.delete(uri)
|
|
200
201
|
@fixall_cache.delete(uri)
|
|
202
|
+
@document_symbol_cache.delete(uri)
|
|
203
|
+
@completion_docs_cache.clear
|
|
204
|
+
@completion_resolve_cache.clear
|
|
201
205
|
path = uri_to_path(uri)
|
|
202
206
|
if path
|
|
203
207
|
prefix = "#{path}:"
|
data/lib/milk_tea/lsp/server.rb
CHANGED
|
@@ -209,6 +209,15 @@ module MilkTea
|
|
|
209
209
|
@fixall_cache = {}
|
|
210
210
|
@definition_file_token_cache = {}
|
|
211
211
|
@definition_file_ast_cache = {}
|
|
212
|
+
# Completion session re-filtering pool keyed by [uri, line].
|
|
213
|
+
@completion_session_cache = {}
|
|
214
|
+
@completion_session_order = []
|
|
215
|
+
@completion_session_order_set = Set.new
|
|
216
|
+
# Server-scoped documentation / resolve memoization keyed by [uri, name].
|
|
217
|
+
@completion_docs_cache = {}
|
|
218
|
+
@completion_resolve_cache = {}
|
|
219
|
+
# Enriched documentSymbol outline keyed by uri -> content_hash.
|
|
220
|
+
@document_symbol_cache = {}
|
|
212
221
|
@diagnostics_perf = {
|
|
213
222
|
scheduled: 0,
|
|
214
223
|
skipped_unchanged: 0,
|
|
@@ -62,8 +62,11 @@ module MilkTea
|
|
|
62
62
|
nil
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
# Eagerly compute and cache facts for +uri+. The keystroke path
|
|
66
|
+
# (didChange) passes warm: false so the sema/import-resolution cost runs
|
|
67
|
+
# on the debounced diagnostics worker instead of the request thread;
|
|
68
|
+
# requests then serve last-good facts until the background pass lands.
|
|
69
|
+
def warm_document_facts(uri, content, warm: true)
|
|
67
70
|
stats = {
|
|
68
71
|
bytes: content.bytesize,
|
|
69
72
|
lines: content.count("\n") + 1,
|
|
@@ -71,7 +74,7 @@ module MilkTea
|
|
|
71
74
|
facts_mode: nil,
|
|
72
75
|
facts_ms: nil,
|
|
73
76
|
skip_reason: nil,
|
|
74
|
-
import_count:
|
|
77
|
+
import_count: 0,
|
|
75
78
|
shared_module_cache_size: @shared_module_cache.length,
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -80,6 +83,14 @@ module MilkTea
|
|
|
80
83
|
return stats
|
|
81
84
|
end
|
|
82
85
|
|
|
86
|
+
unless warm
|
|
87
|
+
stats[:skip_reason] = :deferred
|
|
88
|
+
return stats
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
ast = ast_for_content(uri, content)
|
|
92
|
+
stats[:import_count] = ast.respond_to?(:imports) ? ast.imports.length : 0
|
|
93
|
+
|
|
83
94
|
facts_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
84
95
|
facts_path = uri_to_path(uri)
|
|
85
96
|
get_facts(uri)
|
|
@@ -23,7 +23,12 @@ module MilkTea
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def get_ast(uri)
|
|
26
|
-
@ast_cache[uri] ||=
|
|
26
|
+
@ast_cache[uri] ||= begin
|
|
27
|
+
# Prefer the facts' own AST so consumers that iterate the AST and
|
|
28
|
+
# resolve bindings via facts.binding_resolution operate on the same
|
|
29
|
+
# node instances (binding IDs are keyed by AST node object_id).
|
|
30
|
+
peek_facts(uri)&.ast || parse_document(uri)
|
|
31
|
+
end
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def get_facts(uri, allow_last_good_fallback: true)
|
|
@@ -31,6 +36,17 @@ module MilkTea
|
|
|
31
36
|
snapshot&.facts
|
|
32
37
|
end
|
|
33
38
|
|
|
39
|
+
# Return the most recent facts already computed for +uri+ without ever
|
|
40
|
+
# running analysis or blocking on the analysis mutex. Used by hot request
|
|
41
|
+
# paths (semantic tokens) that must not stall the request thread on sema;
|
|
42
|
+
# they serve the latest cached/last-good facts and are told to re-fetch
|
|
43
|
+
# once fresh facts land in the background.
|
|
44
|
+
def peek_facts(uri)
|
|
45
|
+
@facts_cache_mutex.synchronize do
|
|
46
|
+
@facts_cache[uri] || @last_good_facts_cache[uri]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
34
50
|
def get_tooling_snapshot(uri, allow_last_good_fallback: true)
|
|
35
51
|
total_start = perf_logging? ? monotonic_time : nil
|
|
36
52
|
cache_state = 'miss'
|
|
@@ -67,6 +67,10 @@ module MilkTea
|
|
|
67
67
|
@facts_cache[uri] = facts if facts
|
|
68
68
|
@last_good_facts_cache[uri] = facts if facts
|
|
69
69
|
@document_module_names[uri] = facts.module_name if facts&.module_name
|
|
70
|
+
# Consumers resolve binding_resolution against get_ast(uri) node
|
|
71
|
+
# object_ids; keep the AST cache aligned with the facts that
|
|
72
|
+
# reference it so binding lookups line up across the worker path.
|
|
73
|
+
@ast_cache[uri] = snapshot.facts.ast if snapshot&.facts&.ast
|
|
70
74
|
update_dependency_index(uri, facts)
|
|
71
75
|
@diagnostics_cache[uri] = {
|
|
72
76
|
content_hash: hash,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MilkTea
|
|
4
|
+
module LSP
|
|
5
|
+
class Workspace
|
|
6
|
+
# Persistent module-name index for import-line completion.
|
|
7
|
+
#
|
|
8
|
+
# `import_completions` previously walked the entire module-root tree on
|
|
9
|
+
# every keystroke (stat'ing every directory entry). This index records, per
|
|
10
|
+
# module root, the importable module names reachable from each directory:
|
|
11
|
+
# `.mt` file basenames directly in a directory, plus subdirectory names that
|
|
12
|
+
# transitively contain a `.mt` file. It is built once per root and kept in
|
|
13
|
+
# sync with workspace/didChangeWatchedFiles events, so per-keystroke import
|
|
14
|
+
# completion becomes an in-memory hash lookup.
|
|
15
|
+
module WorkspaceModuleIndex
|
|
16
|
+
def reset_module_index
|
|
17
|
+
@module_index_mutex.synchronize { @module_index.clear }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Rebuild the index for every module root reachable from the workspace
|
|
21
|
+
# root. Called after initialization and after workspace-folder changes.
|
|
22
|
+
def refresh_module_index_for_workspace
|
|
23
|
+
root_path = @workspace_root_path
|
|
24
|
+
return unless root_path
|
|
25
|
+
|
|
26
|
+
module_roots_for_path(root_path).each { |root| ensure_module_index(root) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Ensure +root+ has an index entry, building it on first use. A race
|
|
30
|
+
# between two threads simply builds twice and the later write wins.
|
|
31
|
+
def ensure_module_index(root)
|
|
32
|
+
present = @module_index_mutex.synchronize { @module_index.key?(root) }
|
|
33
|
+
return if present
|
|
34
|
+
|
|
35
|
+
build_module_index(root)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Return importable module names for +fs_dir+ under +root+, excluding the
|
|
39
|
+
# currently-open +current_path+ file. Returns nil when +root+ is unknown.
|
|
40
|
+
def module_importable_names(root, fs_dir, current_path: nil)
|
|
41
|
+
ensure_module_index(root)
|
|
42
|
+
|
|
43
|
+
entry = @module_index_mutex.synchronize { @module_index[root] }
|
|
44
|
+
return nil unless entry
|
|
45
|
+
|
|
46
|
+
result = {}
|
|
47
|
+
files = entry[:mt_files][fs_dir]
|
|
48
|
+
if files
|
|
49
|
+
files.each do |mod_name, file_path|
|
|
50
|
+
next if current_path && file_path == current_path
|
|
51
|
+
|
|
52
|
+
result[mod_name] = true
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
entry[:mt_dirs][fs_dir]&.each { |subdir| result[subdir] = true }
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Apply workspace/didChangeWatchedFiles events to the index. Content
|
|
60
|
+
# changes (type 2) do not affect the name index; creates (1) and deletes
|
|
61
|
+
# (3) of `.mt` files rebuild their root once per event batch so a build
|
|
62
|
+
# producing many events triggers a single rescan. Open documents are
|
|
63
|
+
# source-of-truth and are ignored (skip_open: true), matching
|
|
64
|
+
# apply_watched_file_change; rename handling passes skip_open: false so
|
|
65
|
+
# the index never keeps a stale name for a renamed module.
|
|
66
|
+
def apply_module_index_events(changes, skip_open: true)
|
|
67
|
+
open_uris = @document_state_mutex.synchronize { @open_documents.keys }
|
|
68
|
+
roots_to_rebuild = Set.new
|
|
69
|
+
changes.each do |change|
|
|
70
|
+
uri = change.is_a?(Hash) ? change['uri'] : nil
|
|
71
|
+
change_type = change.is_a?(Hash) ? change['type'] : nil
|
|
72
|
+
next unless uri && [1, 3].include?(change_type&.to_i)
|
|
73
|
+
next if skip_open && open_uris.include?(uri)
|
|
74
|
+
|
|
75
|
+
path = uri_to_path(uri)
|
|
76
|
+
next unless path && path.end_with?('.mt')
|
|
77
|
+
|
|
78
|
+
root = module_index_root_for_path(path)
|
|
79
|
+
roots_to_rebuild << root if root
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
roots_to_rebuild.each { |root| build_module_index(root) }
|
|
83
|
+
roots_to_rebuild.length
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def module_index_roots
|
|
87
|
+
@module_index_mutex.synchronize { @module_index.keys }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def module_index_root_for_path(path)
|
|
93
|
+
expanded = File.expand_path(path)
|
|
94
|
+
@module_index_mutex.synchronize do
|
|
95
|
+
@module_index.each_key do |root|
|
|
96
|
+
return root if expanded == root || expanded.start_with?(root + File::SEPARATOR)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Scan +root+ once and record, for every directory, the importable module
|
|
103
|
+
# names: `.mt` file basenames in that directory and subdirectory names
|
|
104
|
+
# that transitively contain a `.mt` file.
|
|
105
|
+
def build_module_index(root)
|
|
106
|
+
mt_files = Hash.new { |hash, key| hash[key] = {} }
|
|
107
|
+
mt_dirs = Hash.new { |hash, key| hash[key] = Set.new }
|
|
108
|
+
|
|
109
|
+
files = Dir.glob(File.join(root, '**', '*.mt')).sort
|
|
110
|
+
prefix = root + File::SEPARATOR
|
|
111
|
+
files.each do |file_path|
|
|
112
|
+
rel = file_path.delete_prefix(prefix)
|
|
113
|
+
dir = File.dirname(rel)
|
|
114
|
+
dir = '' if dir == '.'
|
|
115
|
+
mod_name = File.basename(rel, '.mt')
|
|
116
|
+
|
|
117
|
+
mt_files[dir][mod_name] = File.expand_path(file_path)
|
|
118
|
+
|
|
119
|
+
acc = ''
|
|
120
|
+
dir.split(File::SEPARATOR).each do |segment|
|
|
121
|
+
mt_dirs[acc] << segment
|
|
122
|
+
acc = acc.empty? ? segment : "#{acc}#{File::SEPARATOR}#{segment}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@module_index_mutex.synchronize do
|
|
127
|
+
@module_index[root] = { mt_files: mt_files, mt_dirs: mt_dirs }
|
|
128
|
+
end
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -29,13 +29,13 @@ module MilkTea
|
|
|
29
29
|
|
|
30
30
|
# ── Document lifecycle ──────────────────────────────────────────────────
|
|
31
31
|
|
|
32
|
-
def open_document(uri, content)
|
|
32
|
+
def open_document(uri, content, warm_facts: true)
|
|
33
33
|
@document_state_mutex.synchronize do
|
|
34
34
|
@open_documents[uri] = content
|
|
35
35
|
end
|
|
36
36
|
invalidate_cache(uri)
|
|
37
37
|
enqueue_definition_warmup(uri) unless background_document?(uri)
|
|
38
|
-
warm_document_facts(uri, content)
|
|
38
|
+
warm_document_facts(uri, content, warm: warm_facts)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def close_document(uri)
|
|
@@ -56,7 +56,7 @@ module MilkTea
|
|
|
56
56
|
enqueue_definition_warmup(uri) unless background_document?(uri)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
def apply_incremental_changes(uri, changes)
|
|
59
|
+
def apply_incremental_changes(uri, changes, warm_facts: true)
|
|
60
60
|
content = get_content(uri)
|
|
61
61
|
edits = Array(changes)
|
|
62
62
|
|
|
@@ -79,7 +79,7 @@ module MilkTea
|
|
|
79
79
|
end
|
|
80
80
|
dependent_uris.each { |dep_uri| invalidate_cache(dep_uri) }
|
|
81
81
|
|
|
82
|
-
warm_document_facts(uri, new_content)
|
|
82
|
+
warm_document_facts(uri, new_content, warm: warm_facts)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def apply_incremental_change_to_content(content, change)
|
|
@@ -107,11 +107,16 @@ module MilkTea
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Index all .mt files under root_uri so they are available for workspace-wide queries.
|
|
110
|
+
# Skips the per-file sweep when the on-disk file set is unchanged since the last
|
|
111
|
+
# index, so repeated workspace/symbol calls do not re-walk and re-read the tree.
|
|
110
112
|
def index_workspace(root_uri, &progress)
|
|
111
113
|
root_path = uri_to_path(root_uri)
|
|
112
114
|
return unless root_path && File.directory?(root_path)
|
|
113
115
|
|
|
114
116
|
paths = Dir.glob(File.join(root_path, '**', '*.mt')).sort
|
|
117
|
+
return if @indexed_workspace_paths == paths
|
|
118
|
+
|
|
119
|
+
@indexed_workspace_paths = paths
|
|
115
120
|
total = paths.length
|
|
116
121
|
paths.each_with_index do |path, idx|
|
|
117
122
|
file_uri = path_to_uri(path)
|
|
@@ -11,6 +11,7 @@ require_relative 'workspace/analysis'
|
|
|
11
11
|
require_relative 'workspace/dependency_graph'
|
|
12
12
|
require_relative 'workspace/definition_index'
|
|
13
13
|
require_relative 'workspace/collection'
|
|
14
|
+
require_relative 'workspace/module_index'
|
|
14
15
|
require_relative 'workspace/utilities'
|
|
15
16
|
|
|
16
17
|
module MilkTea
|
|
@@ -81,6 +82,10 @@ module MilkTea
|
|
|
81
82
|
@identifier_index = {}
|
|
82
83
|
@identifier_index_mutex = Mutex.new
|
|
83
84
|
@indexed_uris = Set.new
|
|
85
|
+
# Module index: root path -> { mt_files: {dir => {name => path}}, mt_dirs: {dir => Set<subdir>} }
|
|
86
|
+
@module_index = {}
|
|
87
|
+
@module_index_mutex = Mutex.new
|
|
88
|
+
@indexed_workspace_paths = nil
|
|
84
89
|
end
|
|
85
90
|
|
|
86
91
|
def reset
|
|
@@ -113,6 +118,8 @@ module MilkTea
|
|
|
113
118
|
@reverse_import_dependents.clear
|
|
114
119
|
@diagnostics_cache.clear
|
|
115
120
|
@facts_generation.clear
|
|
121
|
+
@module_index_mutex.synchronize { @module_index.clear }
|
|
122
|
+
@indexed_workspace_paths = nil
|
|
116
123
|
end
|
|
117
124
|
|
|
118
125
|
include WorkspaceStore
|
|
@@ -121,6 +128,7 @@ module MilkTea
|
|
|
121
128
|
include WorkspaceDependencyGraph
|
|
122
129
|
include WorkspaceDefinitionIndex
|
|
123
130
|
include WorkspaceCollection
|
|
131
|
+
include WorkspaceModuleIndex
|
|
124
132
|
include WorkspaceUtilities
|
|
125
133
|
end
|
|
126
134
|
end
|
|
@@ -42,6 +42,15 @@ module MilkTea
|
|
|
42
42
|
new(path).load
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Best-effort manifest load for optional lookups: returns nil when no
|
|
46
|
+
# manifest applies to the path or when the manifest is invalid, instead
|
|
47
|
+
# of raising PackageManifestError.
|
|
48
|
+
def self.load_option(path)
|
|
49
|
+
load(path)
|
|
50
|
+
rescue PackageManifestError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
45
54
|
def self.manifest_exists_for?(path)
|
|
46
55
|
path = File.expand_path(path)
|
|
47
56
|
current = File.directory?(path) ? path : File.dirname(path)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mt-lang
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.39
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Long (Teefan) Tran
|
|
@@ -149,6 +149,7 @@ files:
|
|
|
149
149
|
- docs/index.html
|
|
150
150
|
- docs/language-design.md
|
|
151
151
|
- docs/language-manual.md
|
|
152
|
+
- docs/lsp-performance.md
|
|
152
153
|
- lib/milk_tea.rb
|
|
153
154
|
- lib/milk_tea/base.rb
|
|
154
155
|
- lib/milk_tea/bindings.rb
|
|
@@ -338,6 +339,7 @@ files:
|
|
|
338
339
|
- lib/milk_tea/lsp/workspace/collection.rb
|
|
339
340
|
- lib/milk_tea/lsp/workspace/definition_index.rb
|
|
340
341
|
- lib/milk_tea/lsp/workspace/dependency_graph.rb
|
|
342
|
+
- lib/milk_tea/lsp/workspace/module_index.rb
|
|
341
343
|
- lib/milk_tea/lsp/workspace/store.rb
|
|
342
344
|
- lib/milk_tea/lsp/workspace/utilities.rb
|
|
343
345
|
- lib/milk_tea/packages.rb
|
|
@@ -624,7 +626,7 @@ metadata:
|
|
|
624
626
|
homepage_uri: https://teefan.github.io/mt-lang/
|
|
625
627
|
source_code_uri: https://github.com/teefan/mt-lang
|
|
626
628
|
post_install_message: |
|
|
627
|
-
Milk Tea 0.3.
|
|
629
|
+
Milk Tea 0.3.39 installed!
|
|
628
630
|
|
|
629
631
|
System requirements:
|
|
630
632
|
- A C compiler (gcc or clang) must be available on PATH
|
|
@@ -646,7 +648,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
646
648
|
- !ruby/object:Gem::Version
|
|
647
649
|
version: '0'
|
|
648
650
|
requirements: []
|
|
649
|
-
rubygems_version: 4.0.
|
|
651
|
+
rubygems_version: 4.0.18
|
|
650
652
|
specification_version: 4
|
|
651
653
|
summary: The Milk Tea programming language compiler toolchain
|
|
652
654
|
test_files: []
|