ttsc 0.19.3 → 0.20.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.
- package/README.md +7 -5
- package/cmd/graphdump/main.go +2 -4
- package/cmd/ttsc/filter.go +12 -4
- package/cmd/ttsc/filter_resolves_flag_names_case_insensitively_test.go +53 -0
- package/cmd/ttsc/flags_gen.go +3 -3
- package/cmd/ttscgraph/dump.go +5 -7
- package/cmd/ttscgraph/run_dump_emits_only_checker_verified_member_relations_test.go +118 -0
- package/cmd/ttscgraph/serve.go +58 -308
- package/cmd/ttscgraph/serve_session_keeps_base_url_fallback_unchanged_test.go +49 -0
- package/cmd/ttscgraph/serve_session_keeps_esm_directory_candidate_unchanged_test.go +61 -0
- package/cmd/ttscgraph/serve_session_keeps_exports_blocked_file_candidates_unchanged_test.go +64 -0
- package/cmd/ttscgraph/serve_session_keeps_inactive_conditional_export_unchanged_test.go +66 -0
- package/cmd/ttscgraph/serve_session_keeps_lower_priority_module_candidate_unchanged_test.go +67 -0
- package/cmd/ttscgraph/serve_session_keeps_lower_priority_paths_pattern_unchanged_test.go +58 -0
- package/cmd/ttscgraph/serve_session_keeps_nonlongest_root_dirs_candidate_unchanged_test.go +54 -0
- package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +50 -0
- package/cmd/ttscgraph/serve_session_reloads_module_suffix_candidate_test.go +41 -0
- package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +216 -0
- package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +103 -0
- package/driver/graph.go +12 -6
- package/driver/lsp.go +30 -0
- package/driver/program.go +37 -1
- package/driver/resolution_candidates.go +819 -0
- package/driver/windowsjunction/windowsjunction.go +40 -0
- package/go.mod +2 -0
- package/internal/graph/build.go +45 -11
- package/internal/graph/dump.go +276 -100
- package/internal/graph/dump_checkout_paths_are_stable_test.go +210 -0
- package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +9 -3
- package/internal/graph/dump_paths.go +172 -0
- package/internal/graph/dump_paths_are_portable_and_injective_test.go +93 -0
- package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
- package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
- package/internal/graph/edges.go +208 -0
- package/internal/graph/graph.go +143 -5
- package/internal/graph/line_model.go +122 -0
- package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
- package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
- package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
- package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
- package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
- package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
- package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
- package/internal/graph/object_members.go +229 -0
- package/internal/graph/provenance.go +88 -30
- package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
- package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
- package/internal/graphsymbols/provider.go +26 -91
- package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
- package/internal/lspserver/lsp_completion.go +109 -0
- package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
- package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
- package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
- package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
- package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
- package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
- package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
- package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
- package/internal/lspserver/lsp_completion_request.go +455 -0
- package/internal/lspserver/lsp_completion_scope.go +530 -0
- package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
- package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
- package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
- package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
- package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
- package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
- package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
- package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
- package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
- package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
- package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
- package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
- package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
- package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
- package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
- package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
- package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
- package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
- package/internal/lspserver/lsp_native_plugin_source.go +315 -2
- package/internal/lspserver/lsp_native_resident.go +304 -0
- package/internal/lspserver/lsp_plugin.go +85 -8
- package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
- package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
- package/internal/lspserver/lsp_proxy.go +601 -9
- package/internal/lspserver/lsp_refresh.go +70 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
- package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
- package/internal/lspserver/lsp_server.go +13 -1
- package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
- package/lib/TtscCompiler.d.ts +3 -1
- package/lib/TtscCompiler.js +5 -1
- package/lib/TtscCompiler.js.map +1 -1
- package/lib/TtscService.d.ts +13 -3
- package/lib/TtscService.js +6 -6
- package/lib/TtscService.js.map +1 -1
- package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
- package/lib/compiler/internal/project/readConfigJson.js +208 -0
- package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
- package/lib/compiler/internal/project/readProjectConfig.js +53 -149
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
- package/lib/compiler/internal/residentTransformProcess.js +159 -39
- package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +6 -0
- package/lib/compiler/internal/runBuild.js +143 -44
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
- package/lib/compiler/internal/startResidentTransform.js +3 -1
- package/lib/compiler/internal/startResidentTransform.js.map +1 -1
- package/lib/compiler/internal/transformProjectInMemory.js +12 -2
- package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
- package/lib/flags/parser.d.ts +19 -0
- package/lib/flags/parser.js +78 -36
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +49 -11
- package/lib/flags/schema.js +94 -22
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
- package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
- package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
- package/lib/launcher/internal/runTtsc.js +107 -69
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtsx.js +28 -41
- package/lib/launcher/internal/runTtsx.js.map +1 -1
- package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
- package/lib/launcher/internal/runtimeHooks.js +18 -1
- package/lib/launcher/internal/runtimeHooks.js.map +1 -1
- package/lib/launcher/internal/watchTopology.d.ts +43 -0
- package/lib/launcher/internal/watchTopology.js +360 -0
- package/lib/launcher/internal/watchTopology.js.map +1 -0
- package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
- package/lib/plugin/internal/buildSourcePlugin.js +247 -61
- package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.js +63 -2
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
- package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
- package/package.json +8 -8
- package/shim/ast/shim.go +48 -3
- package/shim/astnav/go.mod +14 -0
- package/shim/astnav/go.sum +20 -0
- package/shim/astnav/shim.go +15 -0
- package/shim/checker/shim.go +100 -0
- package/shim/core/shim.go +47 -0
- package/shim/diagnosticwriter/lint.go +50 -0
- package/src/TtscCompiler.ts +5 -1
- package/src/TtscService.ts +37 -10
- package/src/compiler/internal/project/readConfigJson.ts +209 -0
- package/src/compiler/internal/project/readProjectConfig.ts +62 -158
- package/src/compiler/internal/residentTransformProcess.ts +197 -39
- package/src/compiler/internal/runBuild.ts +169 -47
- package/src/compiler/internal/startResidentTransform.ts +12 -1
- package/src/compiler/internal/transformProjectInMemory.ts +12 -1
- package/src/flags/parser.ts +113 -42
- package/src/flags/schema.ts +113 -31
- package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
- package/src/launcher/internal/runTtsc.ts +117 -68
- package/src/launcher/internal/runTtsx.ts +29 -44
- package/src/launcher/internal/runtimeHooks.ts +30 -2
- package/src/launcher/internal/watchTopology.ts +466 -0
- package/src/plugin/internal/buildSourcePlugin.ts +326 -64
- package/src/plugin/internal/loadProjectPlugins.ts +80 -2
- package/src/structures/ITtscCompilerTransformation.ts +17 -0
- package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
- package/tools/shim_audit/baseline.json +0 -3
- package/tools/shim_audit/main.go +42 -14
- package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
- package/utility/flags_gen.go +3 -3
- package/utility/host.go +9 -3
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"reflect"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// TestNativeCompletionHintsKeepGroupedWireAndRejectInvalidEntries verifies the
|
|
9
|
+
// compatibility and validation boundary.
|
|
10
|
+
//
|
|
11
|
+
// Third-party sidecars may already publish the grouped shape documented by the
|
|
12
|
+
// first proxy implementation. Those responses remain valid, while empty items,
|
|
13
|
+
// empty triggers, and structurally malformed JSON must not enter the corpus.
|
|
14
|
+
//
|
|
15
|
+
// 1. Decode a grouped response containing valid and empty entries.
|
|
16
|
+
// 2. Assert the valid group's item order survives and empty entries are dropped.
|
|
17
|
+
// 3. Assert a field with the wrong JSON type rejects the response.
|
|
18
|
+
func TestNativeCompletionHintsKeepGroupedWireAndRejectInvalidEntries(t *testing.T) {
|
|
19
|
+
body := []byte(`[
|
|
20
|
+
{
|
|
21
|
+
"scope":"jsdoc",
|
|
22
|
+
"after":"@legacy ",
|
|
23
|
+
"items":[
|
|
24
|
+
{"insert":"legacy-first","detail":"kept"},
|
|
25
|
+
{"insert":""},
|
|
26
|
+
{"insert":"legacy-second"}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{"scope":"","after":"@missing-scope ","items":[{"insert":"drop"}]},
|
|
30
|
+
{"scope":"jsdoc","after":"","items":[{"insert":"drop"}]},
|
|
31
|
+
{"scope":"jsdoc","after":"@empty ","items":[]},
|
|
32
|
+
{"insert":"","trigger":{"scope":"jsdoc","after":"@flat-empty "}},
|
|
33
|
+
{"insert":"drop","trigger":{"scope":"","after":"@flat-empty-scope "}},
|
|
34
|
+
{"insert":"drop","trigger":{"scope":"jsdoc","after":""}},
|
|
35
|
+
{}
|
|
36
|
+
]`)
|
|
37
|
+
|
|
38
|
+
got, err := decodeNativeCompletionHints(body)
|
|
39
|
+
if err != nil {
|
|
40
|
+
t.Fatalf("decode grouped completion hints: %v", err)
|
|
41
|
+
}
|
|
42
|
+
want := []LSPCompletionHint{{
|
|
43
|
+
Scope: "jsdoc",
|
|
44
|
+
After: "@legacy ",
|
|
45
|
+
Items: []LSPCompletionItem{
|
|
46
|
+
{Insert: "legacy-first", Detail: "kept"},
|
|
47
|
+
{Insert: "legacy-second"},
|
|
48
|
+
},
|
|
49
|
+
}}
|
|
50
|
+
if !reflect.DeepEqual(got, want) {
|
|
51
|
+
t.Errorf("grouped compatibility result:\n got %#v\nwant %#v", got, want)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
malformed := []byte(`[
|
|
55
|
+
{"insert":42,"trigger":{"scope":"jsdoc","after":"@broken "}}
|
|
56
|
+
]`)
|
|
57
|
+
if _, err := decodeNativeCompletionHints(malformed); err == nil {
|
|
58
|
+
t.Error("wrongly typed flat hint was accepted")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -63,8 +63,38 @@ type NativePluginSource struct {
|
|
|
63
63
|
|
|
64
64
|
commandIDs []string
|
|
65
65
|
codeActionKinds []string
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
|
|
67
|
+
// completionHints is filled by a background fetch, so it needs a lock the
|
|
68
|
+
// static verbs do not: the proxy reads it from the completion path while
|
|
69
|
+
// discovery may still be writing it. It is the flattened publication-order
|
|
70
|
+
// view of pluginHints, materialized on every store so the completion path
|
|
71
|
+
// copies a ready slice instead of rebuilding one per keystroke.
|
|
72
|
+
hintsMu sync.RWMutex
|
|
73
|
+
completionHints []LSPCompletionHint
|
|
74
|
+
// pluginHints keeps each producer's corpus separately, keyed by plugin
|
|
75
|
+
// identity, so one plugin's refresh cannot disturb another's. A producer's
|
|
76
|
+
// entry changes only when that producer answers successfully: a refresh that
|
|
77
|
+
// failed to run leaves the last known-good corpus in place rather than
|
|
78
|
+
// blanking a working corpus over a transient spawn failure.
|
|
79
|
+
pluginHints map[string]completionHintRecord
|
|
80
|
+
// hintsObserver is told after every completed refresh cycle so the proxy can
|
|
81
|
+
// react to a corpus that changed mid-session. Nil for any host that did not
|
|
82
|
+
// register one.
|
|
83
|
+
hintsObserver func()
|
|
84
|
+
// hintsRefresh serializes and coalesces corpus refreshes. A refresh loads a
|
|
85
|
+
// Program per plugin, so scheduling one per editor event without coalescing
|
|
86
|
+
// would stack process spawns behind each other.
|
|
87
|
+
hintsRefresh coalescingRefresh
|
|
88
|
+
owners map[string]NativeLSPPluginEntry
|
|
89
|
+
logMu sync.Mutex
|
|
90
|
+
|
|
91
|
+
// residentMu guards the resident-daemon table below. A resident sidecar keeps
|
|
92
|
+
// a warm Program across verbs, so lsp-diagnostics / lsp-code-actions reuse it
|
|
93
|
+
// instead of respawning per verb; serveUnsupported remembers a sidecar that
|
|
94
|
+
// predates lsp-serve so the source stops retrying it and stays on exec.
|
|
95
|
+
residentMu sync.Mutex
|
|
96
|
+
residents map[string]*residentSidecar
|
|
97
|
+
serveUnsupported map[string]bool
|
|
68
98
|
}
|
|
69
99
|
|
|
70
100
|
type limitedBuffer struct {
|
|
@@ -140,6 +170,15 @@ func NewNativePluginSource(opts NativePluginSourceOptions) (*NativePluginSource,
|
|
|
140
170
|
owners: map[string]NativeLSPPluginEntry{},
|
|
141
171
|
}
|
|
142
172
|
source.discoverCommandIDs()
|
|
173
|
+
// The corpus fetch loads a Program, so it runs off the construction path.
|
|
174
|
+
// Blocking here would delay initialize — and therefore the editor's first
|
|
175
|
+
// response — for a feature most projects do not use. Until it lands,
|
|
176
|
+
// CompletionHints answers nil and the editor sees exactly what it sees today.
|
|
177
|
+
//
|
|
178
|
+
// The first fetch goes through the same scheduler every later refresh uses,
|
|
179
|
+
// so startup and mid-session rediscovery share one generation counter and one
|
|
180
|
+
// coalescing rule rather than racing as two independent writers.
|
|
181
|
+
source.RefreshCompletionHints()
|
|
143
182
|
return source, nil
|
|
144
183
|
}
|
|
145
184
|
|
|
@@ -314,6 +353,256 @@ func (s *NativePluginSource) CommandIDs() []string {
|
|
|
314
353
|
return out
|
|
315
354
|
}
|
|
316
355
|
|
|
356
|
+
// CompletionHints returns the corpus plugins published, or nil until it has
|
|
357
|
+
// been fetched.
|
|
358
|
+
//
|
|
359
|
+
// Nil while loading is deliberate and is the whole reason the fetch is
|
|
360
|
+
// asynchronous. Unlike lsp-command-ids and lsp-code-action-kinds — which ignore
|
|
361
|
+
// their arguments and never build a Program — lsp-hints must load one, because
|
|
362
|
+
// a corpus is a projection of what a project rule's Check found. Paying that on
|
|
363
|
+
// the initialize path would delay every editor session for a feature most
|
|
364
|
+
// projects do not use, and paying it on the first completion would freeze the
|
|
365
|
+
// popup. Answering "no hints yet" degrades honestly: the editor still gets
|
|
366
|
+
// tsgo's completion, and ours appear once they exist.
|
|
367
|
+
//
|
|
368
|
+
// The same reasoning carries to refresh. It answers the last known-good corpus
|
|
369
|
+
// while a rediscovery scheduled by RefreshCompletionHints is running, so
|
|
370
|
+
// completion never blocks on a producer and never observes a half-cleared
|
|
371
|
+
// corpus.
|
|
372
|
+
func (s *NativePluginSource) CompletionHints() []LSPCompletionHint {
|
|
373
|
+
if s == nil {
|
|
374
|
+
return nil
|
|
375
|
+
}
|
|
376
|
+
s.hintsMu.RLock()
|
|
377
|
+
defer s.hintsMu.RUnlock()
|
|
378
|
+
if len(s.completionHints) == 0 {
|
|
379
|
+
return nil
|
|
380
|
+
}
|
|
381
|
+
out := make([]LSPCompletionHint, len(s.completionHints))
|
|
382
|
+
copy(out, s.completionHints)
|
|
383
|
+
return out
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// RefreshCompletionHints schedules one asynchronous corpus rediscovery.
|
|
387
|
+
//
|
|
388
|
+
// The corpus is a projection of what a project rule's Check found, so it goes
|
|
389
|
+
// stale the moment the rule's inputs change: a saved contributor-indexed
|
|
390
|
+
// document, a rule enabled in `lint.config.*`, a watched file rewritten outside
|
|
391
|
+
// the editor. Without this the corpus stayed a session snapshot and only a
|
|
392
|
+
// language-server restart could replace it.
|
|
393
|
+
//
|
|
394
|
+
// Asynchronous for the same reason the first fetch is (see CompletionHints):
|
|
395
|
+
// the editor event that schedules a refresh must not wait for a Program load.
|
|
396
|
+
// Concurrent requests coalesce into at most one queued rerun, so a save storm
|
|
397
|
+
// costs one extra refresh rather than one per notification, and the previous
|
|
398
|
+
// corpus keeps answering completion until the new one lands.
|
|
399
|
+
func (s *NativePluginSource) RefreshCompletionHints() {
|
|
400
|
+
if s == nil || len(s.plugins) == 0 {
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
s.hintsRefresh.schedule(s.discoverCompletionHints)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// SetCompletionHintsObserver registers fn to run after each completed refresh
|
|
407
|
+
// cycle. The proxy uses it to notice a trigger character that appeared after
|
|
408
|
+
// the initialize response was already sent. A nil fn clears the observer.
|
|
409
|
+
func (s *NativePluginSource) SetCompletionHintsObserver(fn func()) {
|
|
410
|
+
if s == nil {
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
s.hintsMu.Lock()
|
|
414
|
+
defer s.hintsMu.Unlock()
|
|
415
|
+
s.hintsObserver = fn
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// completionHintRecord is one producer's corpus and the refresh generation that
|
|
419
|
+
// produced it. The generation is what keeps a slow refresh from overwriting a
|
|
420
|
+
// newer one: plugin fetches run sequentially inside a cycle, but two cycles can
|
|
421
|
+
// still be in flight when a scheduled refresh outlives its successor's start.
|
|
422
|
+
type completionHintRecord struct {
|
|
423
|
+
hints []LSPCompletionHint
|
|
424
|
+
generation uint64
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// discoverCompletionHints fetches every plugin's corpus for one generation.
|
|
428
|
+
//
|
|
429
|
+
// A separate pass from discoverCommandIDs rather than another step inside it.
|
|
430
|
+
// That loop abandons the rest of a plugin's discovery on any error, so folding
|
|
431
|
+
// hints in would mean a plugin whose corpus failed also lost its code action
|
|
432
|
+
// kinds — one optional feature taking down a working one.
|
|
433
|
+
//
|
|
434
|
+
// Each plugin's result is stored as it arrives instead of after the whole pass,
|
|
435
|
+
// so a fast producer's fresh corpus reaches the editor without waiting on a slow
|
|
436
|
+
// one, and a producer that failed keeps serving what it last published.
|
|
437
|
+
func (s *NativePluginSource) discoverCompletionHints(generation uint64) {
|
|
438
|
+
for _, plugin := range s.plugins {
|
|
439
|
+
body, err := s.run(plugin, "lsp-hints")
|
|
440
|
+
if err != nil {
|
|
441
|
+
// Silent, unlike every other discovery failure here. A plugin that does
|
|
442
|
+
// not know this verb rejects it as an unknown command, and that is the
|
|
443
|
+
// common case rather than the exceptional one: every plugin built before
|
|
444
|
+
// this channel existed, forever. Logging it would print an error per
|
|
445
|
+
// plugin per session for an optional feature nobody asked those plugins
|
|
446
|
+
// for — the same reasoning that makes CompletionHints an optional
|
|
447
|
+
// interface rather than a PluginSource method.
|
|
448
|
+
//
|
|
449
|
+
// The cost is that a plugin genuinely broken while producing hints also
|
|
450
|
+
// goes quiet. That is the right trade: its hints are absent either way,
|
|
451
|
+
// and the alternative is punishing every well-behaved old plugin to catch
|
|
452
|
+
// a rare new one. Refresh strengthens that trade rather than weakening it:
|
|
453
|
+
// a logged failure would now print per save instead of per session.
|
|
454
|
+
continue
|
|
455
|
+
}
|
|
456
|
+
published, err := decodeNativeCompletionHints(body)
|
|
457
|
+
if err != nil {
|
|
458
|
+
// Not silent. A plugin that answered and answered wrongly implements the
|
|
459
|
+
// verb and got it wrong, which is worth saying — unlike one that never
|
|
460
|
+
// implemented it at all.
|
|
461
|
+
s.log("ttscserver: %s lsp-hints returned invalid JSON: %v", pluginLabel(plugin), err)
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
s.storeCompletionHints(plugin, generation, published)
|
|
465
|
+
}
|
|
466
|
+
s.notifyCompletionHintsObserver()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// storeCompletionHints replaces one producer's corpus and rebuilds the flattened
|
|
470
|
+
// snapshot the completion path reads.
|
|
471
|
+
//
|
|
472
|
+
// A successful answer is the only thing that changes a producer's corpus, and an
|
|
473
|
+
// empty successful answer clears it — that is how a disabled rule's items stop
|
|
474
|
+
// being offered. An older generation is dropped: refresh cycles are scheduled by
|
|
475
|
+
// editor events, and the last event's answer is the one the user is waiting for.
|
|
476
|
+
func (s *NativePluginSource) storeCompletionHints(plugin NativeLSPPluginEntry, generation uint64, hints []LSPCompletionHint) {
|
|
477
|
+
key := pluginKey(plugin)
|
|
478
|
+
s.hintsMu.Lock()
|
|
479
|
+
defer s.hintsMu.Unlock()
|
|
480
|
+
if existing, ok := s.pluginHints[key]; ok && generation < existing.generation {
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
if s.pluginHints == nil {
|
|
484
|
+
s.pluginHints = map[string]completionHintRecord{}
|
|
485
|
+
}
|
|
486
|
+
s.pluginHints[key] = completionHintRecord{hints: hints, generation: generation}
|
|
487
|
+
s.completionHints = s.flattenCompletionHintsLocked()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// flattenCompletionHintsLocked concatenates every producer's corpus in manifest
|
|
491
|
+
// order. Order is load-bearing twice over: the proxy resolves overlapping hints
|
|
492
|
+
// by publication order, and the editor ranks the items it is handed. Iterating
|
|
493
|
+
// the manifest rather than the map keeps that order identical across refreshes,
|
|
494
|
+
// which a Go map's randomized range would not. The caller holds hintsMu.
|
|
495
|
+
func (s *NativePluginSource) flattenCompletionHintsLocked() []LSPCompletionHint {
|
|
496
|
+
hints := []LSPCompletionHint{}
|
|
497
|
+
seen := make(map[string]struct{}, len(s.plugins))
|
|
498
|
+
for _, plugin := range s.plugins {
|
|
499
|
+
key := pluginKey(plugin)
|
|
500
|
+
if _, duplicate := seen[key]; duplicate {
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
seen[key] = struct{}{}
|
|
504
|
+
hints = append(hints, s.pluginHints[key].hints...)
|
|
505
|
+
}
|
|
506
|
+
return hints
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// notifyCompletionHintsObserver runs the registered observer outside hintsMu:
|
|
510
|
+
// the observer reads the corpus back through CompletionHints, and holding the
|
|
511
|
+
// lock across it would deadlock on the RLock.
|
|
512
|
+
func (s *NativePluginSource) notifyCompletionHintsObserver() {
|
|
513
|
+
s.hintsMu.RLock()
|
|
514
|
+
observer := s.hintsObserver
|
|
515
|
+
s.hintsMu.RUnlock()
|
|
516
|
+
if observer != nil {
|
|
517
|
+
observer()
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// pluginKey identifies one manifest entry. It matches the identity
|
|
522
|
+
// pluginOwnsCommand compares, so a manifest carrying the same binary under two
|
|
523
|
+
// stages keeps two independent corpora instead of overwriting one with the
|
|
524
|
+
// other.
|
|
525
|
+
func pluginKey(plugin NativeLSPPluginEntry) string {
|
|
526
|
+
return plugin.Binary + "\x00" + plugin.Name + "\x00" + plugin.Stage
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// decodeNativeCompletionHints accepts both generations of the lsp-hints wire.
|
|
530
|
+
//
|
|
531
|
+
// @ttsc/lint publishes one flat rule.Hint per item, with the scope and trigger
|
|
532
|
+
// nested under `trigger`. The first proxy implementation instead documented a
|
|
533
|
+
// grouped response with `scope`, `after`, and `items` at the top level. Flat
|
|
534
|
+
// entries are grouped here by trigger so the proxy keeps its efficient matching
|
|
535
|
+
// shape, while grouped responses remain valid for existing third-party
|
|
536
|
+
// sidecars. The first occurrence of a trigger fixes its group position and each
|
|
537
|
+
// later occurrence appends in publication order, preserving rule ranking.
|
|
538
|
+
func decodeNativeCompletionHints(body []byte) ([]LSPCompletionHint, error) {
|
|
539
|
+
var entries []json.RawMessage
|
|
540
|
+
if err := json.Unmarshal(body, &entries); err != nil {
|
|
541
|
+
return nil, err
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
type triggerKey struct {
|
|
545
|
+
scope string
|
|
546
|
+
after string
|
|
547
|
+
}
|
|
548
|
+
flatGroups := map[triggerKey]int{}
|
|
549
|
+
hints := make([]LSPCompletionHint, 0, len(entries))
|
|
550
|
+
for _, entry := range entries {
|
|
551
|
+
var fields map[string]json.RawMessage
|
|
552
|
+
if err := json.Unmarshal(entry, &fields); err != nil {
|
|
553
|
+
return nil, err
|
|
554
|
+
}
|
|
555
|
+
if _, grouped := fields["items"]; grouped {
|
|
556
|
+
var hint LSPCompletionHint
|
|
557
|
+
if err := json.Unmarshal(entry, &hint); err != nil {
|
|
558
|
+
return nil, err
|
|
559
|
+
}
|
|
560
|
+
hint.Items = usableNativeCompletionItems(hint.Items)
|
|
561
|
+
if hint.Scope == "" || hint.After == "" || len(hint.Items) == 0 {
|
|
562
|
+
continue
|
|
563
|
+
}
|
|
564
|
+
hints = append(hints, hint)
|
|
565
|
+
continue
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
var flat struct {
|
|
569
|
+
LSPCompletionItem
|
|
570
|
+
Trigger struct {
|
|
571
|
+
Scope string `json:"scope"`
|
|
572
|
+
After string `json:"after"`
|
|
573
|
+
} `json:"trigger"`
|
|
574
|
+
}
|
|
575
|
+
if err := json.Unmarshal(entry, &flat); err != nil {
|
|
576
|
+
return nil, err
|
|
577
|
+
}
|
|
578
|
+
if flat.Insert == "" || flat.Trigger.Scope == "" || flat.Trigger.After == "" {
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
581
|
+
key := triggerKey{scope: flat.Trigger.Scope, after: flat.Trigger.After}
|
|
582
|
+
index, exists := flatGroups[key]
|
|
583
|
+
if !exists {
|
|
584
|
+
index = len(hints)
|
|
585
|
+
flatGroups[key] = index
|
|
586
|
+
hints = append(hints, LSPCompletionHint{
|
|
587
|
+
Scope: flat.Trigger.Scope,
|
|
588
|
+
After: flat.Trigger.After,
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
hints[index].Items = append(hints[index].Items, flat.LSPCompletionItem)
|
|
592
|
+
}
|
|
593
|
+
return hints, nil
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
func usableNativeCompletionItems(items []LSPCompletionItem) []LSPCompletionItem {
|
|
597
|
+
kept := make([]LSPCompletionItem, 0, len(items))
|
|
598
|
+
for _, item := range items {
|
|
599
|
+
if item.Insert != "" {
|
|
600
|
+
kept = append(kept, item)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return kept
|
|
604
|
+
}
|
|
605
|
+
|
|
317
606
|
// CodeActionKinds returns the action kinds discovered from LSP-capable sidecars.
|
|
318
607
|
func (s *NativePluginSource) CodeActionKinds() []string {
|
|
319
608
|
if s == nil || len(s.codeActionKinds) == 0 {
|
|
@@ -385,6 +674,30 @@ func (s *NativePluginSource) pluginOwnsCommand(plugin NativeLSPPluginEntry, comm
|
|
|
385
674
|
}
|
|
386
675
|
|
|
387
676
|
func (s *NativePluginSource) run(plugin NativeLSPPluginEntry, command string, args ...string) ([]byte, error) {
|
|
677
|
+
// Route the Program-loading read verbs through the plugin's resident daemon so
|
|
678
|
+
// a warm Program is reused across verbs. serveRun returns served=false for a
|
|
679
|
+
// sidecar that predates lsp-serve or a transport failure, falling back to the
|
|
680
|
+
// spawn-per-verb path below with no behavior change. The static discovery
|
|
681
|
+
// verbs (lsp-command-ids / lsp-code-action-kinds) and lsp-execute-command stay
|
|
682
|
+
// on exec by design.
|
|
683
|
+
if command == serveVerbDiagnostics || command == serveVerbCodeActions {
|
|
684
|
+
if body, served, err := s.serveRun(plugin, command, args); served {
|
|
685
|
+
return body, err
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if command == serveVerbHints {
|
|
689
|
+
// Hints join the daemon on a weaker condition than the other two: the
|
|
690
|
+
// answer is used only when it arrives without error. A sidecar built after
|
|
691
|
+
// the daemon landed but before this verb joined it answers lsp-serve and
|
|
692
|
+
// rejects lsp-hints as an unknown verb, which reaches this side as a
|
|
693
|
+
// nonzero code — indistinguishable over this protocol from a rule that
|
|
694
|
+
// failed. Falling back to the spawn path on any nonzero reply keeps that
|
|
695
|
+
// sidecar's corpus working, and a project whose hints genuinely fail pays
|
|
696
|
+
// one extra spawn to fail there too, exactly as it did before.
|
|
697
|
+
if body, served, err := s.serveRun(plugin, command, args); served && err == nil {
|
|
698
|
+
return body, nil
|
|
699
|
+
}
|
|
700
|
+
}
|
|
388
701
|
return s.runWithStdin(plugin, command, nil, args...)
|
|
389
702
|
}
|
|
390
703
|
|