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.
- checksums.yaml +4 -4
- data/docs/lsp-performance.md +347 -0
- data/lib/milk_tea/base.rb +1 -1
- data/lib/milk_tea/lsp/diagnostics.rb +15 -5
- 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 +13 -3
- data/lib/milk_tea/lsp/server/hover.rb +321 -41
- 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/semantic_tokens.rb +55 -16
- data/lib/milk_tea/lsp/server/text_documents.rb +2 -2
- data/lib/milk_tea/lsp/server/type_hierarchy.rb +2 -2
- 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
- metadata +4 -2
|
@@ -12,8 +12,10 @@ 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,
|
|
@@ -25,10 +27,6 @@ module MilkTea
|
|
|
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)
|
|
@@ -38,17 +36,18 @@ module MilkTea
|
|
|
38
36
|
encode_ms = elapsed_ms(encode_start)
|
|
39
37
|
|
|
40
38
|
result_id = next_semantic_token_result_id(uri)
|
|
41
|
-
@semantic_tokens_cache[uri] = { content_hash: cache_key, data: data, result_id: result_id }
|
|
39
|
+
@semantic_tokens_cache[uri] = { content_hash: cache_key, data: data, result_id: result_id, facts_id: facts_id }
|
|
42
40
|
@semantic_tokens_delta_cache[uri] = {
|
|
43
41
|
result_id: result_id,
|
|
44
42
|
content_hash: cache_key,
|
|
43
|
+
facts_id: facts_id,
|
|
45
44
|
entries: semantic_entries,
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
elapsed = elapsed_ms(total_start)
|
|
49
48
|
short_uri = shorten_uri(uri) || uri
|
|
50
49
|
log_perf_breakdown('textDocument/semanticTokens/full', elapsed,
|
|
51
|
-
"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}")
|
|
52
51
|
|
|
53
52
|
{ resultId: result_id, data: data }
|
|
54
53
|
rescue StandardError => e
|
|
@@ -68,18 +67,20 @@ module MilkTea
|
|
|
68
67
|
|
|
69
68
|
content = @workspace.get_content(uri)
|
|
70
69
|
cache_key = content.hash
|
|
71
|
-
|
|
70
|
+
facts = @workspace.peek_facts(uri)
|
|
71
|
+
facts_id = facts&.object_id
|
|
72
|
+
if cached[:content_hash] == cache_key && cached[:facts_id] == facts_id
|
|
72
73
|
return { resultId: cached[:result_id], edits: [] }
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
tokens = @workspace.get_tokens(uri) || []
|
|
76
|
-
facts = @workspace.get_facts(uri, allow_last_good_fallback: semantic_tokens_allow_last_good_fallback?(uri))
|
|
77
77
|
new_entries = build_semantic_token_entries(tokens, facts)
|
|
78
78
|
|
|
79
79
|
new_result_id = next_semantic_token_result_id(uri)
|
|
80
80
|
@semantic_tokens_delta_cache[uri] = {
|
|
81
81
|
result_id: new_result_id,
|
|
82
82
|
content_hash: cache_key,
|
|
83
|
+
facts_id: facts_id,
|
|
83
84
|
entries: new_entries,
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -109,7 +110,7 @@ module MilkTea
|
|
|
109
110
|
![:newline, :indent, :dedent, :eof].include?(t.type)
|
|
110
111
|
end
|
|
111
112
|
|
|
112
|
-
facts = @workspace.
|
|
113
|
+
facts = @workspace.peek_facts(uri)
|
|
113
114
|
semantic_entries = build_semantic_token_entries(range_tokens, facts)
|
|
114
115
|
|
|
115
116
|
entries_in_range = semantic_entries.select do |entry|
|
|
@@ -128,6 +129,7 @@ module MilkTea
|
|
|
128
129
|
# is O(1) per call instead of O(n), turning the overall build from O(n²) to O(n).
|
|
129
130
|
trivia_types = Set[:newline, :indent, :dedent, :eof]
|
|
130
131
|
@tokens_by_line_cache = Hash.new { |h, k| h[k] = [] }
|
|
132
|
+
@semantic_token_build_cache = {}
|
|
131
133
|
tokens.each { |t| @tokens_by_line_cache[t.line] << t unless trivia_types.include?(t.type) }
|
|
132
134
|
@attribute_name_semantic_overrides = build_attribute_name_semantic_overrides(tokens, facts)
|
|
133
135
|
|
|
@@ -152,6 +154,24 @@ module MilkTea
|
|
|
152
154
|
ensure
|
|
153
155
|
@tokens_by_line_cache = nil
|
|
154
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
|
|
155
175
|
end
|
|
156
176
|
|
|
157
177
|
def fstring_interpolation_entries(fstring_tok, facts)
|
|
@@ -1052,11 +1072,23 @@ module MilkTea
|
|
|
1052
1072
|
|
|
1053
1073
|
def known_type_name?(facts, name)
|
|
1054
1074
|
return false unless facts
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
|
1058
1089
|
end
|
|
1059
|
-
|
|
1090
|
+
|
|
1091
|
+
store_semantic_build_memo(memo_key, result)
|
|
1060
1092
|
end
|
|
1061
1093
|
|
|
1062
1094
|
def dot_nested_type_member?(tokens, index, facts)
|
|
@@ -1317,6 +1349,9 @@ module MilkTea
|
|
|
1317
1349
|
end
|
|
1318
1350
|
|
|
1319
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
|
+
|
|
1320
1355
|
scopes = @type_parameter_scope_cache ||= {}
|
|
1321
1356
|
cached = scopes[facts.object_id]
|
|
1322
1357
|
unless cached
|
|
@@ -1330,11 +1365,15 @@ module MilkTea
|
|
|
1330
1365
|
scopes[facts.object_id] = cached
|
|
1331
1366
|
end
|
|
1332
1367
|
|
|
1368
|
+
result = []
|
|
1333
1369
|
cached.reverse_each do |scope|
|
|
1334
|
-
|
|
1370
|
+
if line >= scope[:start_line] && line <= scope[:end_line]
|
|
1371
|
+
result = scope[:names]
|
|
1372
|
+
break
|
|
1373
|
+
end
|
|
1335
1374
|
end
|
|
1336
1375
|
|
|
1337
|
-
|
|
1376
|
+
store_semantic_build_memo(memo_key, result)
|
|
1338
1377
|
end
|
|
1339
1378
|
|
|
1340
1379
|
def type_parameter_scopes_for_declaration(decl, end_line: Float::INFINITY)
|
|
@@ -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)
|
|
@@ -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
|
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
|