ttsc 0.19.2 → 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.
Files changed (178) hide show
  1. package/README.md +7 -5
  2. package/cmd/graphdump/main.go +2 -4
  3. package/cmd/ttsc/filter.go +12 -4
  4. package/cmd/ttsc/filter_resolves_flag_names_case_insensitively_test.go +53 -0
  5. package/cmd/ttsc/flags_gen.go +3 -3
  6. package/cmd/ttscgraph/dump.go +5 -7
  7. package/cmd/ttscgraph/run_dump_emits_only_checker_verified_member_relations_test.go +118 -0
  8. package/cmd/ttscgraph/serve.go +58 -308
  9. package/cmd/ttscgraph/serve_session_keeps_base_url_fallback_unchanged_test.go +49 -0
  10. package/cmd/ttscgraph/serve_session_keeps_esm_directory_candidate_unchanged_test.go +61 -0
  11. package/cmd/ttscgraph/serve_session_keeps_exports_blocked_file_candidates_unchanged_test.go +64 -0
  12. package/cmd/ttscgraph/serve_session_keeps_inactive_conditional_export_unchanged_test.go +66 -0
  13. package/cmd/ttscgraph/serve_session_keeps_lower_priority_module_candidate_unchanged_test.go +67 -0
  14. package/cmd/ttscgraph/serve_session_keeps_lower_priority_paths_pattern_unchanged_test.go +58 -0
  15. package/cmd/ttscgraph/serve_session_keeps_nonlongest_root_dirs_candidate_unchanged_test.go +54 -0
  16. package/cmd/ttscgraph/serve_session_reloads_commonjs_package_entry_for_esm_test.go +50 -0
  17. package/cmd/ttscgraph/serve_session_reloads_module_suffix_candidate_test.go +41 -0
  18. package/cmd/ttscgraph/serve_session_reloads_superseding_module_candidates_test.go +216 -0
  19. package/cmd/ttscgraph/serve_session_retries_failed_dump_test.go +103 -0
  20. package/driver/graph.go +12 -6
  21. package/driver/lsp.go +30 -0
  22. package/driver/program.go +37 -1
  23. package/driver/resolution_candidates.go +819 -0
  24. package/driver/windowsjunction/windowsjunction.go +40 -0
  25. package/go.mod +2 -0
  26. package/internal/graph/build.go +45 -11
  27. package/internal/graph/dump.go +312 -108
  28. package/internal/graph/dump_checkout_paths_are_stable_test.go +210 -0
  29. package/internal/graph/dump_diagnostics_ride_the_generation_that_built_it_test.go +9 -3
  30. package/internal/graph/dump_paths.go +172 -0
  31. package/internal/graph/dump_paths_are_portable_and_injective_test.go +93 -0
  32. package/internal/graph/dump_schema_version_matches_the_typescript_client_test.go +1 -1
  33. package/internal/graph/dump_uses_ecma_line_terminators_test.go +87 -0
  34. package/internal/graph/duplicate_variable_object_members_keep_first_declaration_test.go +61 -0
  35. package/internal/graph/edges.go +220 -5
  36. package/internal/graph/enum_members_carry_their_names_on_the_enum_node_test.go +131 -0
  37. package/internal/graph/graph.go +177 -5
  38. package/internal/graph/line_model.go +122 -0
  39. package/internal/graph/literals.go +175 -0
  40. package/internal/graph/literals_are_absent_when_the_type_admits_more_than_it_can_name_test.go +74 -0
  41. package/internal/graph/literals_cover_enum_members_the_graph_has_no_nodes_for_test.go +79 -0
  42. package/internal/graph/literals_do_not_depend_on_declaration_line_wrapping_test.go +84 -0
  43. package/internal/graph/literals_render_every_value_in_typescript_source_form_test.go +77 -0
  44. package/internal/graph/literals_resolve_through_alias_indirection_test.go +64 -0
  45. package/internal/graph/member_relation_edges_follow_checker_validity_test.go +85 -0
  46. package/internal/graph/member_relation_edges_preserve_checker_boundaries_test.go +191 -0
  47. package/internal/graph/member_relation_edges_reject_checker_invalid_pairs_test.go +119 -0
  48. package/internal/graph/node_ids_preserve_hash_bearing_components_test.go +41 -0
  49. package/internal/graph/node_modifiers_emit_union_strings_test.go +4 -1
  50. package/internal/graph/object_literal_members_ride_on_the_snapshot_test.go +192 -0
  51. package/internal/graph/object_member_signatures_preserve_literal_whitespace_test.go +67 -0
  52. package/internal/graph/object_members.go +229 -0
  53. package/internal/graph/provenance.go +88 -30
  54. package/internal/graph/source_texts_cover_every_resident_program_source_test.go +73 -0
  55. package/internal/graphsymbols/position_helpers_follow_ecma_line_terminators_test.go +66 -0
  56. package/internal/graphsymbols/provider.go +26 -91
  57. package/internal/lspserver/lsp_clean_didopen_refreshes_compiler_state_test.go +144 -0
  58. package/internal/lspserver/lsp_completion.go +109 -0
  59. package/internal/lspserver/lsp_completion_gate_never_refuses_a_matching_line_test.go +50 -0
  60. package/internal/lspserver/lsp_completion_hints_longest_trigger_wins_test.go +149 -0
  61. package/internal/lspserver/lsp_completion_hints_nearest_trigger_wins_test.go +37 -0
  62. package/internal/lspserver/lsp_completion_merges_into_upstream_response_test.go +149 -0
  63. package/internal/lspserver/lsp_completion_nearest_hint_replaces_nearest_filter_test.go +43 -0
  64. package/internal/lspserver/lsp_completion_preserves_completion_list_extensions_test.go +99 -0
  65. package/internal/lspserver/lsp_completion_refuses_hints_inside_a_string_literal_test.go +69 -0
  66. package/internal/lspserver/lsp_completion_replaces_full_filter_in_rank_order_test.go +85 -0
  67. package/internal/lspserver/lsp_completion_request.go +455 -0
  68. package/internal/lspserver/lsp_completion_scope.go +530 -0
  69. package/internal/lspserver/lsp_completion_scope_bench_test.go +41 -0
  70. package/internal/lspserver/lsp_completion_scope_classifies_regex_statement_and_operator_boundaries_test.go +39 -0
  71. package/internal/lspserver/lsp_completion_scope_ignores_jsdoc_bytes_in_literals_test.go +108 -0
  72. package/internal/lspserver/lsp_completion_scope_refuses_regex_hints_test.go +52 -0
  73. package/internal/lspserver/lsp_completion_trigger_uses_last_rune_test.go +24 -0
  74. package/internal/lspserver/lsp_deleted_watched_file_withdraws_plugin_diagnostics_test.go +110 -0
  75. package/internal/lspserver/lsp_didsave_keeps_the_incremental_changed_uri_path_test.go +78 -0
  76. package/internal/lspserver/lsp_hints_absence_is_not_an_error_test.go +46 -0
  77. package/internal/lspserver/lsp_hints_refresh_coalesces_concurrent_requests_test.go +82 -0
  78. package/internal/lspserver/lsp_hints_refresh_ignores_a_superseded_generation_test.go +40 -0
  79. package/internal/lspserver/lsp_hints_refresh_keeps_each_producer_isolated_test.go +64 -0
  80. package/internal/lspserver/lsp_hints_refresh_replaces_the_session_snapshot_test.go +66 -0
  81. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +90 -0
  82. package/internal/lspserver/lsp_hints_refresh_serves_completion_while_running_test.go +91 -0
  83. package/internal/lspserver/lsp_index_byte_from_bounds_its_search_start_test.go +38 -0
  84. package/internal/lspserver/lsp_initialize_constrains_position_encoding_to_utf16_test.go +205 -0
  85. package/internal/lspserver/lsp_late_completion_trigger_is_reported_once_test.go +105 -0
  86. package/internal/lspserver/lsp_native_completion_hints_decode_flat_lint_wire_test.go +66 -0
  87. package/internal/lspserver/lsp_native_completion_hints_grouped_compatibility_boundary_test.go +60 -0
  88. package/internal/lspserver/lsp_native_plugin_source.go +315 -2
  89. package/internal/lspserver/lsp_native_resident.go +304 -0
  90. package/internal/lspserver/lsp_plugin.go +85 -8
  91. package/internal/lspserver/lsp_plugin_completion_resolve_stays_local_test.go +82 -0
  92. package/internal/lspserver/lsp_position_conversions_span_astral_and_combining_runes_test.go +119 -0
  93. package/internal/lspserver/lsp_proxy.go +601 -9
  94. package/internal/lspserver/lsp_refresh.go +70 -0
  95. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +43 -0
  96. package/internal/lspserver/lsp_resident_maps_verb_args_test.go +34 -0
  97. package/internal/lspserver/lsp_server.go +13 -1
  98. package/internal/lspserver/lsp_watched_file_changes_reach_compiler_caches_test.go +172 -0
  99. package/lib/TtscCompiler.d.ts +3 -1
  100. package/lib/TtscCompiler.js +5 -1
  101. package/lib/TtscCompiler.js.map +1 -1
  102. package/lib/TtscService.d.ts +13 -3
  103. package/lib/TtscService.js +6 -6
  104. package/lib/TtscService.js.map +1 -1
  105. package/lib/compiler/internal/project/readConfigJson.d.ts +30 -0
  106. package/lib/compiler/internal/project/readConfigJson.js +208 -0
  107. package/lib/compiler/internal/project/readConfigJson.js.map +1 -0
  108. package/lib/compiler/internal/project/readProjectConfig.js +53 -149
  109. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  110. package/lib/compiler/internal/residentTransformProcess.d.ts +15 -2
  111. package/lib/compiler/internal/residentTransformProcess.js +159 -39
  112. package/lib/compiler/internal/residentTransformProcess.js.map +1 -1
  113. package/lib/compiler/internal/runBuild.d.ts +6 -0
  114. package/lib/compiler/internal/runBuild.js +143 -44
  115. package/lib/compiler/internal/runBuild.js.map +1 -1
  116. package/lib/compiler/internal/startResidentTransform.d.ts +5 -1
  117. package/lib/compiler/internal/startResidentTransform.js +3 -1
  118. package/lib/compiler/internal/startResidentTransform.js.map +1 -1
  119. package/lib/compiler/internal/transformProjectInMemory.js +12 -2
  120. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  121. package/lib/flags/parser.d.ts +19 -0
  122. package/lib/flags/parser.js +78 -36
  123. package/lib/flags/parser.js.map +1 -1
  124. package/lib/flags/schema.d.ts +49 -11
  125. package/lib/flags/schema.js +94 -22
  126. package/lib/flags/schema.js.map +1 -1
  127. package/lib/internal/assertSafeExplicitCacheDirectory.d.ts +5 -0
  128. package/lib/internal/assertSafeExplicitCacheDirectory.js +26 -0
  129. package/lib/internal/assertSafeExplicitCacheDirectory.js.map +1 -0
  130. package/lib/launcher/internal/runTtsc.js +107 -69
  131. package/lib/launcher/internal/runTtsc.js.map +1 -1
  132. package/lib/launcher/internal/runTtsx.js +28 -41
  133. package/lib/launcher/internal/runTtsx.js.map +1 -1
  134. package/lib/launcher/internal/runtimeHooks.d.ts +15 -0
  135. package/lib/launcher/internal/runtimeHooks.js +18 -1
  136. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  137. package/lib/launcher/internal/watchTopology.d.ts +43 -0
  138. package/lib/launcher/internal/watchTopology.js +360 -0
  139. package/lib/launcher/internal/watchTopology.js.map +1 -0
  140. package/lib/plugin/internal/buildSourcePlugin.d.ts +14 -3
  141. package/lib/plugin/internal/buildSourcePlugin.js +247 -61
  142. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  143. package/lib/plugin/internal/loadProjectPlugins.js +63 -2
  144. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  145. package/lib/structures/ITtscCompilerTransformation.d.ts +16 -0
  146. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  147. package/package.json +8 -8
  148. package/shim/ast/shim.go +48 -3
  149. package/shim/astnav/go.mod +14 -0
  150. package/shim/astnav/go.sum +20 -0
  151. package/shim/astnav/shim.go +15 -0
  152. package/shim/checker/shim.go +113 -0
  153. package/shim/core/shim.go +47 -0
  154. package/shim/diagnosticwriter/lint.go +50 -0
  155. package/src/TtscCompiler.ts +5 -1
  156. package/src/TtscService.ts +37 -10
  157. package/src/compiler/internal/project/readConfigJson.ts +209 -0
  158. package/src/compiler/internal/project/readProjectConfig.ts +62 -158
  159. package/src/compiler/internal/residentTransformProcess.ts +197 -39
  160. package/src/compiler/internal/runBuild.ts +169 -47
  161. package/src/compiler/internal/startResidentTransform.ts +12 -1
  162. package/src/compiler/internal/transformProjectInMemory.ts +12 -1
  163. package/src/flags/parser.ts +113 -42
  164. package/src/flags/schema.ts +113 -31
  165. package/src/internal/assertSafeExplicitCacheDirectory.ts +29 -0
  166. package/src/launcher/internal/runTtsc.ts +117 -68
  167. package/src/launcher/internal/runTtsx.ts +29 -44
  168. package/src/launcher/internal/runtimeHooks.ts +30 -2
  169. package/src/launcher/internal/watchTopology.ts +466 -0
  170. package/src/plugin/internal/buildSourcePlugin.ts +326 -64
  171. package/src/plugin/internal/loadProjectPlugins.ts +80 -2
  172. package/src/structures/ITtscCompilerTransformation.ts +17 -0
  173. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  174. package/tools/shim_audit/baseline.json +0 -3
  175. package/tools/shim_audit/main.go +42 -14
  176. package/tools/shim_audit/shim_dir_coverage_test.go +25 -0
  177. package/utility/flags_gen.go +3 -3
  178. 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, &params) != 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
+ }