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,455 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
"unicode/utf16"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
const pluginCompletionDataMarker = "ttsc/completion-hint/v1"
|
|
11
|
+
|
|
12
|
+
type pendingCompletionRequest struct {
|
|
13
|
+
items []LSPCompletionItem
|
|
14
|
+
replaceRange LSPRange
|
|
15
|
+
hasRange bool
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// handleCompletionRequest computes the plugin's contribution for a cursor and
|
|
19
|
+
// remembers it for the response.
|
|
20
|
+
//
|
|
21
|
+
// It always forwards. tsgo owns completion everywhere a plugin does not, and
|
|
22
|
+
// answering locally would replace the compiler's suggestions with a rule's — so
|
|
23
|
+
// the request goes upstream and the plugin's items are appended to whatever
|
|
24
|
+
// comes back.
|
|
25
|
+
//
|
|
26
|
+
// It does NOT bail on a dirty buffer, unlike codeAction. A dirty buffer is
|
|
27
|
+
// precisely when completion is wanted: the user is mid-word. The live text the
|
|
28
|
+
// proxy already splices per didChange is the right source, and the only one
|
|
29
|
+
// that exists mid-edit.
|
|
30
|
+
func (p *Proxy) handleCompletionRequest(env Envelope) (bool, error) {
|
|
31
|
+
pending := p.completionItemsFor(env)
|
|
32
|
+
if len(pending.items) == 0 {
|
|
33
|
+
return false, nil
|
|
34
|
+
}
|
|
35
|
+
key := env.IDKey()
|
|
36
|
+
if key == "" {
|
|
37
|
+
return false, nil
|
|
38
|
+
}
|
|
39
|
+
p.pendingMu.Lock()
|
|
40
|
+
if p.pendingCompletions == nil {
|
|
41
|
+
p.pendingCompletions = map[string]pendingCompletionRequest{}
|
|
42
|
+
}
|
|
43
|
+
p.pendingCompletions[key] = pending
|
|
44
|
+
p.pendingMu.Unlock()
|
|
45
|
+
return false, nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// handleCompletionResolveRequest completes plugin-owned items locally.
|
|
49
|
+
//
|
|
50
|
+
// tsgo advertises completionItem/resolve for its own items and expects its
|
|
51
|
+
// private data payload on every request. Plugin hints are already fully
|
|
52
|
+
// resolved, so forwarding one to tsgo would hand it an item it did not create.
|
|
53
|
+
// The private marker added while merging items is the ownership boundary.
|
|
54
|
+
func (p *Proxy) handleCompletionResolveRequest(env Envelope) (bool, error) {
|
|
55
|
+
if !env.IsRequest() || !isPluginCompletionItem(env.Params) {
|
|
56
|
+
return false, nil
|
|
57
|
+
}
|
|
58
|
+
return true, p.writeResult(env.ID, json.RawMessage(env.Params))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func isPluginCompletionItem(params json.RawMessage) bool {
|
|
62
|
+
var item struct {
|
|
63
|
+
Data struct {
|
|
64
|
+
Marker string `json:"$ttsc"`
|
|
65
|
+
} `json:"data"`
|
|
66
|
+
}
|
|
67
|
+
return len(params) > 0 &&
|
|
68
|
+
json.Unmarshal(params, &item) == nil &&
|
|
69
|
+
item.Data.Marker == pluginCompletionDataMarker
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// completionItemsFor resolves the cursor to a line prefix and asks the corpus.
|
|
73
|
+
//
|
|
74
|
+
// Returns nothing when the document's text is unknown. Failing open — matching
|
|
75
|
+
// against an empty prefix — would offer every broad trigger's items at a
|
|
76
|
+
// position the proxy cannot see, which is worse than silence.
|
|
77
|
+
func (p *Proxy) completionItemsFor(env Envelope) pendingCompletionRequest {
|
|
78
|
+
hints := p.pluginCompletionHints()
|
|
79
|
+
if len(hints) == 0 {
|
|
80
|
+
return pendingCompletionRequest{}
|
|
81
|
+
}
|
|
82
|
+
var params struct {
|
|
83
|
+
TextDocument struct {
|
|
84
|
+
URI string `json:"uri"`
|
|
85
|
+
} `json:"textDocument"`
|
|
86
|
+
Position struct {
|
|
87
|
+
Line int `json:"line"`
|
|
88
|
+
Character int `json:"character"`
|
|
89
|
+
} `json:"position"`
|
|
90
|
+
}
|
|
91
|
+
if len(env.Params) == 0 || json.Unmarshal(env.Params, ¶ms) != nil {
|
|
92
|
+
return pendingCompletionRequest{}
|
|
93
|
+
}
|
|
94
|
+
text, ok := p.cachedDocumentText(params.TextDocument.URI)
|
|
95
|
+
if !ok {
|
|
96
|
+
return pendingCompletionRequest{}
|
|
97
|
+
}
|
|
98
|
+
offset, ok := offsetForPosition(text, params.Position.Line, params.Position.Character)
|
|
99
|
+
if !ok {
|
|
100
|
+
return pendingCompletionRequest{}
|
|
101
|
+
}
|
|
102
|
+
linePrefix := linePrefixAt(text, offset)
|
|
103
|
+
if !anyCompletionHintCouldApply(hints, linePrefix) {
|
|
104
|
+
// Every hint is already refused on grounds the cursor's scope cannot
|
|
105
|
+
// rescue, so returning here skips cursorInJSDoc — which scans the document
|
|
106
|
+
// from byte zero, is the dominant cost of this request, and is pure waste
|
|
107
|
+
// on every keystroke that is not inside a trigger.
|
|
108
|
+
return pendingCompletionRequest{}
|
|
109
|
+
}
|
|
110
|
+
items, filter := matchCompletionHints(
|
|
111
|
+
hints,
|
|
112
|
+
linePrefix,
|
|
113
|
+
cursorInJSDoc(text, offset),
|
|
114
|
+
)
|
|
115
|
+
if len(items) == 0 {
|
|
116
|
+
return pendingCompletionRequest{}
|
|
117
|
+
}
|
|
118
|
+
filterUnits := utf16Length(filter)
|
|
119
|
+
if filterUnits > params.Position.Character {
|
|
120
|
+
return pendingCompletionRequest{}
|
|
121
|
+
}
|
|
122
|
+
return pendingCompletionRequest{
|
|
123
|
+
items: items,
|
|
124
|
+
replaceRange: LSPRange{
|
|
125
|
+
Start: LSPPosition{
|
|
126
|
+
Line: params.Position.Line,
|
|
127
|
+
Character: params.Position.Character - filterUnits,
|
|
128
|
+
},
|
|
129
|
+
End: LSPPosition(params.Position),
|
|
130
|
+
},
|
|
131
|
+
hasRange: true,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// utf16Length measures a filter in the units the replace range is expressed in.
|
|
136
|
+
//
|
|
137
|
+
// The range travels back to the client as `Position.character`, counted in the
|
|
138
|
+
// session's negotiated PositionEncodingKind, which
|
|
139
|
+
// constrainInitializePositionEncoding pins to UTF-16 for every ttscserver
|
|
140
|
+
// session.
|
|
141
|
+
func utf16Length(text string) int {
|
|
142
|
+
units := 0
|
|
143
|
+
for _, symbol := range text {
|
|
144
|
+
width := utf16.RuneLen(symbol)
|
|
145
|
+
if width <= 0 {
|
|
146
|
+
width = 1
|
|
147
|
+
}
|
|
148
|
+
units += width
|
|
149
|
+
}
|
|
150
|
+
return units
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// offsetForPosition converts an LSP line/character to a byte offset.
|
|
154
|
+
//
|
|
155
|
+
// LSP counts characters in the session's negotiated PositionEncodingKind, which
|
|
156
|
+
// constrainInitializePositionEncoding pins to UTF-16 for every ttscserver
|
|
157
|
+
// session, so a line holding an emoji or a CJK character past the BMP would land
|
|
158
|
+
// the cursor mid-token if counted as bytes. The conversion walks the line's runes
|
|
159
|
+
// and spends the position's UTF-16 budget as it goes.
|
|
160
|
+
func offsetForPosition(text string, line int, character int) (int, bool) {
|
|
161
|
+
offset := 0
|
|
162
|
+
for current := 0; current < line; current++ {
|
|
163
|
+
next := indexByteFrom(text, offset, '\n')
|
|
164
|
+
if next == -1 {
|
|
165
|
+
return 0, false
|
|
166
|
+
}
|
|
167
|
+
offset = next + 1
|
|
168
|
+
}
|
|
169
|
+
units := 0
|
|
170
|
+
for index, symbol := range text[offset:] {
|
|
171
|
+
if units >= character {
|
|
172
|
+
return offset + index, true
|
|
173
|
+
}
|
|
174
|
+
if symbol == '\n' || symbol == '\r' {
|
|
175
|
+
return offset + index, true
|
|
176
|
+
}
|
|
177
|
+
if symbol > 0xFFFF {
|
|
178
|
+
units += 2
|
|
179
|
+
} else {
|
|
180
|
+
units++
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return len(text), true
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// indexByteFrom returns the offset of the first target byte at or after from, or
|
|
187
|
+
// -1. It delegates to strings.IndexByte rather than looping: this runs once per
|
|
188
|
+
// line walked on the completion path, and the standard library's scan is the
|
|
189
|
+
// platform's vectorized one where a hand-rolled loop is a byte at a time.
|
|
190
|
+
func indexByteFrom(text string, from int, target byte) int {
|
|
191
|
+
if from < 0 {
|
|
192
|
+
from = 0
|
|
193
|
+
}
|
|
194
|
+
if from >= len(text) {
|
|
195
|
+
return -1
|
|
196
|
+
}
|
|
197
|
+
found := strings.IndexByte(text[from:], target)
|
|
198
|
+
if found < 0 {
|
|
199
|
+
return -1
|
|
200
|
+
}
|
|
201
|
+
return from + found
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// mergeCompletionResponse appends the plugin's items to upstream's answer.
|
|
205
|
+
//
|
|
206
|
+
// The response may be a bare item array, a CompletionList, or null — all three
|
|
207
|
+
// are legal, and tsgo picks per request. Each is handled rather than assumed:
|
|
208
|
+
// guessing wrong here does not error, it silently drops either the plugin's
|
|
209
|
+
// items or the compiler's.
|
|
210
|
+
//
|
|
211
|
+
// `isIncomplete` is preserved when upstream set it and left false when
|
|
212
|
+
// synthesizing over a null. That flag is upstream's to own: it says whether the
|
|
213
|
+
// list must be recomputed as the user types, and only tsgo knows that about its
|
|
214
|
+
// own items. Ours are complete by construction — the corpus is already in
|
|
215
|
+
// memory — so re-merging per keystroke is free either way.
|
|
216
|
+
func mergeCompletionResponse(body []byte, items []LSPCompletionItem) []byte {
|
|
217
|
+
return mergeCompletionResponseWithRequest(body, pendingCompletionRequest{items: items})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func mergeCompletionResponseWithRequest(body []byte, pending pendingCompletionRequest) []byte {
|
|
221
|
+
if len(pending.items) == 0 {
|
|
222
|
+
return body
|
|
223
|
+
}
|
|
224
|
+
var envelope struct {
|
|
225
|
+
JSONRPC string `json:"jsonrpc"`
|
|
226
|
+
ID json.RawMessage `json:"id"`
|
|
227
|
+
Result json.RawMessage `json:"result"`
|
|
228
|
+
Error json.RawMessage `json:"error,omitempty"`
|
|
229
|
+
}
|
|
230
|
+
if json.Unmarshal(body, &envelope) != nil || len(envelope.Error) > 0 {
|
|
231
|
+
// An upstream error is upstream's to report. Appending completions to it
|
|
232
|
+
// would turn a failure into a half-answer.
|
|
233
|
+
return body
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
encoded := make([]json.RawMessage, 0, len(pending.items))
|
|
237
|
+
for index, item := range pending.items {
|
|
238
|
+
label := item.Label
|
|
239
|
+
if label == "" {
|
|
240
|
+
label = item.Insert
|
|
241
|
+
}
|
|
242
|
+
entry := map[string]any{"label": label}
|
|
243
|
+
entry["insertText"] = item.Insert
|
|
244
|
+
// Matching uses Insert, so keep client-side filtering on the same value.
|
|
245
|
+
// A friendlier Label may omit a path prefix that the user already typed.
|
|
246
|
+
entry["filterText"] = item.Insert
|
|
247
|
+
entry["sortText"] = fmt.Sprintf("ttsc:%010d", index)
|
|
248
|
+
entry["data"] = map[string]string{"$ttsc": pluginCompletionDataMarker}
|
|
249
|
+
// CompletionList.itemDefaults belong to upstream items. State every
|
|
250
|
+
// defaultable value a plugin item owns so an upstream snippet or commit
|
|
251
|
+
// character default cannot silently change a plain-text hint.
|
|
252
|
+
entry["commitCharacters"] = []string{}
|
|
253
|
+
entry["insertTextFormat"] = 1
|
|
254
|
+
entry["insertTextMode"] = 1
|
|
255
|
+
if pending.hasRange {
|
|
256
|
+
entry["textEdit"] = map[string]any{
|
|
257
|
+
"range": pending.replaceRange,
|
|
258
|
+
"newText": item.Insert,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if item.Detail != "" {
|
|
262
|
+
entry["detail"] = item.Detail
|
|
263
|
+
}
|
|
264
|
+
raw, err := json.Marshal(entry)
|
|
265
|
+
if err != nil {
|
|
266
|
+
return body
|
|
267
|
+
}
|
|
268
|
+
encoded = append(encoded, raw)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
list := map[string]json.RawMessage{}
|
|
272
|
+
existing := []json.RawMessage{}
|
|
273
|
+
switch {
|
|
274
|
+
case len(envelope.Result) == 0 || string(envelope.Result) == "null":
|
|
275
|
+
list["isIncomplete"] = json.RawMessage("false")
|
|
276
|
+
case envelope.Result[0] == '[':
|
|
277
|
+
if json.Unmarshal(envelope.Result, &existing) != nil {
|
|
278
|
+
return body
|
|
279
|
+
}
|
|
280
|
+
list["isIncomplete"] = json.RawMessage("false")
|
|
281
|
+
default:
|
|
282
|
+
if json.Unmarshal(envelope.Result, &list) != nil {
|
|
283
|
+
return body
|
|
284
|
+
}
|
|
285
|
+
if rawItems, exists := list["items"]; exists && json.Unmarshal(rawItems, &existing) != nil {
|
|
286
|
+
return body
|
|
287
|
+
}
|
|
288
|
+
normalized, ok := normalizeCompletionListDefaults(list, existing)
|
|
289
|
+
if !ok {
|
|
290
|
+
return body
|
|
291
|
+
}
|
|
292
|
+
existing = normalized
|
|
293
|
+
}
|
|
294
|
+
existing = append(existing, encoded...)
|
|
295
|
+
mergedItems, err := json.Marshal(existing)
|
|
296
|
+
if err != nil {
|
|
297
|
+
return body
|
|
298
|
+
}
|
|
299
|
+
list["items"] = mergedItems
|
|
300
|
+
|
|
301
|
+
result, err := json.Marshal(list)
|
|
302
|
+
if err != nil {
|
|
303
|
+
return body
|
|
304
|
+
}
|
|
305
|
+
merged, err := json.Marshal(map[string]any{
|
|
306
|
+
"jsonrpc": envelope.JSONRPC,
|
|
307
|
+
"id": json.RawMessage(envelope.ID),
|
|
308
|
+
"result": json.RawMessage(result),
|
|
309
|
+
})
|
|
310
|
+
if err != nil {
|
|
311
|
+
return body
|
|
312
|
+
}
|
|
313
|
+
return merged
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// normalizeCompletionListDefaults materializes merge-kind defaults into the
|
|
317
|
+
// upstream items before plugin items are appended. Otherwise a CompletionList
|
|
318
|
+
// default such as data or commitCharacters would also merge into plugin items
|
|
319
|
+
// even though those defaults belong to tsgo's producer. Unknown defaults fail
|
|
320
|
+
// closed because a future protocol field could change plugin item semantics.
|
|
321
|
+
func normalizeCompletionListDefaults(
|
|
322
|
+
list map[string]json.RawMessage,
|
|
323
|
+
items []json.RawMessage,
|
|
324
|
+
) ([]json.RawMessage, bool) {
|
|
325
|
+
rawDefaults, hasDefaults := list["itemDefaults"]
|
|
326
|
+
if !hasDefaults {
|
|
327
|
+
return items, true
|
|
328
|
+
}
|
|
329
|
+
defaults := map[string]json.RawMessage{}
|
|
330
|
+
if json.Unmarshal(rawDefaults, &defaults) != nil {
|
|
331
|
+
return nil, false
|
|
332
|
+
}
|
|
333
|
+
supportedDefaults := map[string]struct{}{
|
|
334
|
+
"commitCharacters": {},
|
|
335
|
+
"data": {},
|
|
336
|
+
"editRange": {},
|
|
337
|
+
"insertTextFormat": {},
|
|
338
|
+
"insertTextMode": {},
|
|
339
|
+
}
|
|
340
|
+
for field := range defaults {
|
|
341
|
+
if _, supported := supportedDefaults[field]; !supported {
|
|
342
|
+
return nil, false
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
rawApplyKind, hasApplyKind := list["applyKind"]
|
|
347
|
+
if !hasApplyKind {
|
|
348
|
+
return items, true
|
|
349
|
+
}
|
|
350
|
+
applyKind := map[string]json.RawMessage{}
|
|
351
|
+
if json.Unmarshal(rawApplyKind, &applyKind) != nil {
|
|
352
|
+
return nil, false
|
|
353
|
+
}
|
|
354
|
+
for field, rawKind := range applyKind {
|
|
355
|
+
var kind int
|
|
356
|
+
if json.Unmarshal(rawKind, &kind) != nil || (kind != 1 && kind != 2) {
|
|
357
|
+
return nil, false
|
|
358
|
+
}
|
|
359
|
+
if field != "commitCharacters" && field != "data" {
|
|
360
|
+
if kind == 2 {
|
|
361
|
+
return nil, false
|
|
362
|
+
}
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
if kind != 2 {
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
defaultValue, exists := defaults[field]
|
|
369
|
+
if exists {
|
|
370
|
+
materialized, ok := materializeCompletionDefault(items, field, defaultValue)
|
|
371
|
+
if !ok {
|
|
372
|
+
return nil, false
|
|
373
|
+
}
|
|
374
|
+
items = materialized
|
|
375
|
+
}
|
|
376
|
+
applyKind[field] = json.RawMessage("1")
|
|
377
|
+
}
|
|
378
|
+
normalizedApplyKind, err := json.Marshal(applyKind)
|
|
379
|
+
if err != nil {
|
|
380
|
+
return nil, false
|
|
381
|
+
}
|
|
382
|
+
list["applyKind"] = normalizedApplyKind
|
|
383
|
+
return items, true
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
func materializeCompletionDefault(
|
|
387
|
+
items []json.RawMessage,
|
|
388
|
+
field string,
|
|
389
|
+
defaultValue json.RawMessage,
|
|
390
|
+
) ([]json.RawMessage, bool) {
|
|
391
|
+
materialized := make([]json.RawMessage, 0, len(items))
|
|
392
|
+
for _, rawItem := range items {
|
|
393
|
+
item := map[string]json.RawMessage{}
|
|
394
|
+
if json.Unmarshal(rawItem, &item) != nil {
|
|
395
|
+
return nil, false
|
|
396
|
+
}
|
|
397
|
+
if itemValue, exists := item[field]; exists {
|
|
398
|
+
merged, ok := mergeCompletionDefault(field, itemValue, defaultValue)
|
|
399
|
+
if !ok {
|
|
400
|
+
return nil, false
|
|
401
|
+
}
|
|
402
|
+
item[field] = merged
|
|
403
|
+
} else {
|
|
404
|
+
item[field] = defaultValue
|
|
405
|
+
}
|
|
406
|
+
rawMaterialized, err := json.Marshal(item)
|
|
407
|
+
if err != nil {
|
|
408
|
+
return nil, false
|
|
409
|
+
}
|
|
410
|
+
materialized = append(materialized, rawMaterialized)
|
|
411
|
+
}
|
|
412
|
+
return materialized, true
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
func mergeCompletionDefault(
|
|
416
|
+
field string,
|
|
417
|
+
itemValue json.RawMessage,
|
|
418
|
+
defaultValue json.RawMessage,
|
|
419
|
+
) (json.RawMessage, bool) {
|
|
420
|
+
switch field {
|
|
421
|
+
case "commitCharacters":
|
|
422
|
+
var itemCharacters []string
|
|
423
|
+
var defaultCharacters []string
|
|
424
|
+
if json.Unmarshal(itemValue, &itemCharacters) != nil ||
|
|
425
|
+
json.Unmarshal(defaultValue, &defaultCharacters) != nil {
|
|
426
|
+
return nil, false
|
|
427
|
+
}
|
|
428
|
+
seen := make(map[string]struct{}, len(itemCharacters)+len(defaultCharacters))
|
|
429
|
+
merged := make([]string, 0, len(itemCharacters)+len(defaultCharacters))
|
|
430
|
+
for _, characters := range [][]string{itemCharacters, defaultCharacters} {
|
|
431
|
+
for _, character := range characters {
|
|
432
|
+
if _, exists := seen[character]; exists {
|
|
433
|
+
continue
|
|
434
|
+
}
|
|
435
|
+
seen[character] = struct{}{}
|
|
436
|
+
merged = append(merged, character)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
rawMerged, err := json.Marshal(merged)
|
|
440
|
+
return rawMerged, err == nil
|
|
441
|
+
case "data":
|
|
442
|
+
defaults := map[string]json.RawMessage{}
|
|
443
|
+
item := map[string]json.RawMessage{}
|
|
444
|
+
if json.Unmarshal(defaultValue, &defaults) != nil || json.Unmarshal(itemValue, &item) != nil {
|
|
445
|
+
return nil, false
|
|
446
|
+
}
|
|
447
|
+
for key, value := range item {
|
|
448
|
+
defaults[key] = value
|
|
449
|
+
}
|
|
450
|
+
rawMerged, err := json.Marshal(defaults)
|
|
451
|
+
return rawMerged, err == nil
|
|
452
|
+
default:
|
|
453
|
+
return nil, false
|
|
454
|
+
}
|
|
455
|
+
}
|