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,149 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "encoding/json"
5
+ "strings"
6
+ "testing"
7
+ )
8
+
9
+ // TestLSPCompletionMergesIntoUpstreamResponse pins the response merge.
10
+ //
11
+ // tsgo answers completion in three shapes — a bare item array, a CompletionList,
12
+ // or null — and picks per request. Each has to be handled rather than assumed,
13
+ // because guessing wrong does not error: it silently drops either the plugin's
14
+ // items or the compiler's, and the user simply sees a shorter list than they
15
+ // should. That is the failure this whole channel is most likely to ship with.
16
+ //
17
+ // 1. Merge into each of the three shapes.
18
+ // 2. Assert upstream's items survive alongside the plugin's in every one.
19
+ // 3. Assert isIncomplete stays upstream's answer, not ours.
20
+ func TestLSPCompletionMergesIntoUpstreamResponse(t *testing.T) {
21
+ items := []LSPCompletionItem{{Insert: "pricing", Detail: "Pricing"}}
22
+
23
+ cases := []struct {
24
+ name string
25
+ body string
26
+ wantLabels []string
27
+ incomplete bool
28
+ }{
29
+ {
30
+ name: "bare array",
31
+ body: `{"jsonrpc":"2.0","id":1,"result":[{"label":"toString"}]}`,
32
+ wantLabels: []string{"toString", "pricing"},
33
+ },
34
+ {
35
+ name: "completion list preserves isIncomplete",
36
+ body: `{"jsonrpc":"2.0","id":1,"result":{"isIncomplete":true,"items":[{"label":"toString"}]}}`,
37
+ wantLabels: []string{"toString", "pricing"},
38
+ incomplete: true,
39
+ },
40
+ {
41
+ // Not an edge case: tsgo returns null for completion in JSDoc prose,
42
+ // which is exactly where these hints live. This is the common shape.
43
+ name: "null result",
44
+ body: `{"jsonrpc":"2.0","id":1,"result":null}`,
45
+ wantLabels: []string{"pricing"},
46
+ },
47
+ }
48
+ for _, entry := range cases {
49
+ merged := mergeCompletionResponse([]byte(entry.body), items)
50
+ var decoded struct {
51
+ Result struct {
52
+ IsIncomplete bool `json:"isIncomplete"`
53
+ Items []struct {
54
+ Label string `json:"label"`
55
+ InsertText string `json:"insertText"`
56
+ Detail string `json:"detail"`
57
+ } `json:"items"`
58
+ } `json:"result"`
59
+ }
60
+ if err := json.Unmarshal(merged, &decoded); err != nil {
61
+ t.Fatalf("%s: merged body is not valid JSON: %v\n%s", entry.name, err, merged)
62
+ }
63
+ labels := []string{}
64
+ for _, item := range decoded.Result.Items {
65
+ labels = append(labels, item.Label)
66
+ }
67
+ if !equalStrings(labels, entry.wantLabels) {
68
+ t.Errorf("%s: merged labels %v, want %v", entry.name, labels, entry.wantLabels)
69
+ }
70
+ if decoded.Result.IsIncomplete != entry.incomplete {
71
+ t.Errorf(
72
+ "%s: isIncomplete = %v, want %v — the flag is upstream's to own",
73
+ entry.name, decoded.Result.IsIncomplete, entry.incomplete,
74
+ )
75
+ }
76
+ }
77
+
78
+ // A hint with no Label falls back to Insert, because the editor lists Label
79
+ // and an empty one renders as a blank row.
80
+ merged := mergeCompletionResponse([]byte(`{"jsonrpc":"2.0","id":1,"result":null}`), items)
81
+ if !strings.Contains(string(merged), `"label":"pricing"`) {
82
+ t.Errorf("an item without a Label must fall back to Insert:\n%s", merged)
83
+ }
84
+ if !strings.Contains(string(merged), `"insertText":"pricing"`) {
85
+ t.Errorf("insertText must carry Insert:\n%s", merged)
86
+ }
87
+ }
88
+
89
+ // TestLSPCompletionLeavesUpstreamErrorsAlone pins the negative twin.
90
+ //
91
+ // An upstream error is upstream's to report. Appending completions to it would
92
+ // turn a failure into a half-answer that looks like it worked, which is worse
93
+ // than the error the user was supposed to see.
94
+ func TestLSPCompletionLeavesUpstreamErrorsAlone(t *testing.T) {
95
+ body := `{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"boom"}}`
96
+ if got := string(mergeCompletionResponse([]byte(body), []LSPCompletionItem{{Insert: "x"}})); got != body {
97
+ t.Errorf("an upstream error was rewritten:\n%s", got)
98
+ }
99
+
100
+ // Nothing to add is not a reason to rewrite a body either.
101
+ plain := `{"jsonrpc":"2.0","id":1,"result":[]}`
102
+ if got := string(mergeCompletionResponse([]byte(plain), nil)); got != plain {
103
+ t.Errorf("an empty contribution rewrote the body:\n%s", got)
104
+ }
105
+ }
106
+
107
+ // TestOffsetForPositionCountsUTF16 pins the position conversion.
108
+ //
109
+ // LSP counts a position's character in UTF-16 code units, not bytes. A line
110
+ // holding an emoji or CJK text would otherwise land the cursor mid-token, and
111
+ // the line prefix — which every trigger matches against — would be cut in the
112
+ // wrong place. The failure is silent: completion just stops appearing on lines
113
+ // with non-ASCII text.
114
+ //
115
+ // 1. Convert an ASCII position.
116
+ // 2. Convert past CJK text, which is one UTF-16 unit but three bytes.
117
+ // 3. Convert past an emoji, which is a surrogate pair — two units, four bytes.
118
+ func TestOffsetForPositionCountsUTF16(t *testing.T) {
119
+ cases := []struct {
120
+ text string
121
+ line int
122
+ character int
123
+ want string
124
+ }{
125
+ {"abc", 0, 2, "ab"},
126
+ {"/**\n * @evi", 1, 7, "/**\n * @evi"},
127
+ // 가격 is two runes, two UTF-16 units, six bytes.
128
+ {"// 가격 x", 0, 5, "// 가격"},
129
+ // An emoji past the BMP costs two units.
130
+ {"// 🚀 x", 0, 5, "// 🚀"},
131
+ }
132
+ for _, entry := range cases {
133
+ offset, ok := offsetForPosition(entry.text, entry.line, entry.character)
134
+ if !ok {
135
+ t.Errorf("offsetForPosition(%q, %d, %d) failed", entry.text, entry.line, entry.character)
136
+ continue
137
+ }
138
+ if got := entry.text[:offset]; got != entry.want {
139
+ t.Errorf(
140
+ "offsetForPosition(%q, %d, %d) cut at %q, want %q",
141
+ entry.text, entry.line, entry.character, got, entry.want,
142
+ )
143
+ }
144
+ }
145
+
146
+ if _, ok := offsetForPosition("one line", 5, 0); ok {
147
+ t.Error("a line past the end must fail rather than clamp silently")
148
+ }
149
+ }
@@ -0,0 +1,43 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+ )
7
+
8
+ type nearestCompletionHintSource struct{ NullPluginSource }
9
+
10
+ func (nearestCompletionHintSource) CompletionHints() []LSPCompletionHint {
11
+ return []LSPCompletionHint{
12
+ {Scope: "jsdoc", After: "@evidence", Items: []LSPCompletionItem{{Insert: "long"}}},
13
+ {Scope: "jsdoc", After: "@", Items: []LSPCompletionItem{{Insert: "near"}}},
14
+ }
15
+ }
16
+
17
+ // TestLSPCompletionNearestHintReplacesNearestFilter exercises the request path:
18
+ // a later short trigger beats an earlier long trigger and replaces only the
19
+ // filter immediately after the winning occurrence.
20
+ func TestLSPCompletionNearestHintReplacesNearestFilter(t *testing.T) {
21
+ const uri = "file:///project/src/main.ts"
22
+ const text = "/** @evidence first @pa"
23
+ proxy := &Proxy{
24
+ source: nearestCompletionHintSource{},
25
+ documentText: map[string]string{uri: text},
26
+ }
27
+ params, _ := json.Marshal(map[string]any{
28
+ "textDocument": map[string]any{"uri": uri},
29
+ "position": map[string]any{"line": 0, "character": 23},
30
+ })
31
+
32
+ pending := proxy.completionItemsFor(Envelope{Params: params})
33
+ if len(pending.items) != 1 || pending.items[0].Insert != "near" {
34
+ t.Fatalf("items = %#v, want only the nearest trigger item", pending.items)
35
+ }
36
+ if !pending.hasRange {
37
+ t.Fatal("nearest trigger did not produce a replacement range")
38
+ }
39
+ if pending.replaceRange.Start != (LSPPosition{Line: 0, Character: 21}) ||
40
+ pending.replaceRange.End != (LSPPosition{Line: 0, Character: 23}) {
41
+ t.Errorf("replacement range = %+v, want character 21..23", pending.replaceRange)
42
+ }
43
+ }
@@ -0,0 +1,99 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "encoding/json"
5
+ "reflect"
6
+ "testing"
7
+ )
8
+
9
+ // TestLSPCompletionPreservesCompletionListExtensions verifies lossless merging.
10
+ //
11
+ // TypeScript-Go uses CompletionList.itemDefaults, and newer protocol versions
12
+ // may add more list-level fields. Decoding into a closed local struct silently
13
+ // removed those fields whenever a plugin contributed even one completion.
14
+ //
15
+ // 1. Build an upstream CompletionList with itemDefaults, applyKind, and a future field.
16
+ // 2. Merge one plugin completion.
17
+ // 3. Assert every upstream list field and both items survive.
18
+ func TestLSPCompletionPreservesCompletionListExtensions(t *testing.T) {
19
+ body := []byte(`{
20
+ "jsonrpc":"2.0",
21
+ "id":1,
22
+ "result":{
23
+ "isIncomplete":true,
24
+ "itemDefaults":{"commitCharacters":["."],"insertTextFormat":2,"data":{"session":"upstream","shared":"default"}},
25
+ "applyKind":{"commitCharacters":2,"data":2},
26
+ "x-future":{"nested":[1,2,3]},
27
+ "items":[{
28
+ "label":"upstream",
29
+ "labelDetails":{"detail":" kept"},
30
+ "commitCharacters":["("],
31
+ "data":{"shared":"item","token":42}
32
+ }]
33
+ }
34
+ }`)
35
+ merged := mergeCompletionResponse(body, []LSPCompletionItem{{Insert: "plugin"}})
36
+ var response struct {
37
+ Result map[string]json.RawMessage `json:"result"`
38
+ }
39
+ if err := json.Unmarshal(merged, &response); err != nil {
40
+ t.Fatalf("decode merged completion response: %v\n%s", err, merged)
41
+ }
42
+
43
+ assertJSON := func(field string, want string) {
44
+ t.Helper()
45
+ raw, exists := response.Result[field]
46
+ if !exists {
47
+ t.Errorf("CompletionList field %q was dropped", field)
48
+ return
49
+ }
50
+ var gotValue any
51
+ var wantValue any
52
+ if err := json.Unmarshal(raw, &gotValue); err != nil {
53
+ t.Errorf("decode field %q: %v", field, err)
54
+ return
55
+ }
56
+ if err := json.Unmarshal([]byte(want), &wantValue); err != nil {
57
+ t.Fatalf("decode expected field %q: %v", field, err)
58
+ }
59
+ if !reflect.DeepEqual(gotValue, wantValue) {
60
+ t.Errorf("CompletionList field %q = %s, want %s", field, raw, want)
61
+ }
62
+ }
63
+ assertJSON("itemDefaults", `{"commitCharacters":["."],"insertTextFormat":2,"data":{"session":"upstream","shared":"default"}}`)
64
+ assertJSON("applyKind", `{"commitCharacters":1,"data":1}`)
65
+ assertJSON("x-future", `{"nested":[1,2,3]}`)
66
+
67
+ var items []map[string]json.RawMessage
68
+ if err := json.Unmarshal(response.Result["items"], &items); err != nil {
69
+ t.Fatalf("decode merged items: %v", err)
70
+ }
71
+ if len(items) != 2 {
72
+ t.Fatalf("merged %d completion items, want upstream plus plugin", len(items))
73
+ }
74
+ assertItemJSON := func(index int, field string, want string) {
75
+ t.Helper()
76
+ raw, exists := items[index][field]
77
+ if !exists {
78
+ t.Errorf("item %d field %q was dropped", index, field)
79
+ return
80
+ }
81
+ var gotValue any
82
+ var wantValue any
83
+ if err := json.Unmarshal(raw, &gotValue); err != nil {
84
+ t.Errorf("decode item %d field %q: %v", index, field, err)
85
+ return
86
+ }
87
+ if err := json.Unmarshal([]byte(want), &wantValue); err != nil {
88
+ t.Fatalf("decode expected item %d field %q: %v", index, field, err)
89
+ }
90
+ if !reflect.DeepEqual(gotValue, wantValue) {
91
+ t.Errorf("item %d field %q = %s, want %s", index, field, raw, want)
92
+ }
93
+ }
94
+ assertItemJSON(0, "commitCharacters", `["(","."]`)
95
+ assertItemJSON(0, "data", `{"session":"upstream","shared":"item","token":42}`)
96
+ assertItemJSON(1, "commitCharacters", `[]`)
97
+ assertItemJSON(1, "data", `{"$ttsc":"ttsc/completion-hint/v1"}`)
98
+ assertJSON("isIncomplete", "true")
99
+ }
@@ -0,0 +1,69 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+ )
7
+
8
+ type jsdocTagCompletionHintSource struct{ NullPluginSource }
9
+
10
+ func (jsdocTagCompletionHintSource) CompletionHints() []LSPCompletionHint {
11
+ return []LSPCompletionHint{{
12
+ Scope: "jsdoc",
13
+ After: "@",
14
+ Items: []LSPCompletionItem{{Insert: "param"}},
15
+ }}
16
+ }
17
+
18
+ // TestLSPCompletionRefusesHintsInsideAStringLiteral verifies the scope decision
19
+ // where it is observable.
20
+ //
21
+ // The lexical table proves the scanner; this proves the request path still asks
22
+ // it. A refactor that recomputed scope from the line prefix, or that passed the
23
+ // wrong offset, would put `jsdoc/check-tag-names` tags into ordinary source
24
+ // text — a position the owning rule never inspects — and no unit test of the
25
+ // matcher alone would notice.
26
+ //
27
+ // 1. Ask for completion after `@par` inside a string literal that contains the
28
+ // JSDoc opener.
29
+ // 2. Ask again at the same tag inside a real doc comment.
30
+ // 3. Assert silence for the first and the published item for the second.
31
+ func TestLSPCompletionRefusesHintsInsideAStringLiteral(t *testing.T) {
32
+ const literalURI = "file:///project/src/literal.ts"
33
+ const blockURI = "file:///project/src/block.ts"
34
+ proxy := &Proxy{
35
+ source: jsdocTagCompletionHintSource{},
36
+ documentText: map[string]string{
37
+ literalURI: "const example = \"/** @par\";\n",
38
+ blockURI: "/**\n * @par\n */\nexport const value = 1;\n",
39
+ },
40
+ }
41
+
42
+ literalParams, err := json.Marshal(map[string]any{
43
+ "textDocument": map[string]any{"uri": literalURI},
44
+ "position": map[string]any{"line": 0, "character": 25},
45
+ })
46
+ if err != nil {
47
+ t.Fatalf("encode completion params: %v", err)
48
+ }
49
+ if pending := proxy.completionItemsFor(Envelope{Params: literalParams}); len(pending.items) != 0 {
50
+ t.Errorf("a string literal was offered %v, want nothing", inserts(pending.items))
51
+ }
52
+
53
+ blockParams, err := json.Marshal(map[string]any{
54
+ "textDocument": map[string]any{"uri": blockURI},
55
+ "position": map[string]any{"line": 1, "character": 7},
56
+ })
57
+ if err != nil {
58
+ t.Fatalf("encode completion params: %v", err)
59
+ }
60
+ pending := proxy.completionItemsFor(Envelope{Params: blockParams})
61
+ if got := inserts(pending.items); !equalStrings(got, []string{"param"}) {
62
+ t.Fatalf("a real doc comment was offered %v, want [param]", got)
63
+ }
64
+ // The refusal must not have come from a broken filter range either.
65
+ if pending.replaceRange.Start != (LSPPosition{Line: 1, Character: 4}) ||
66
+ pending.replaceRange.End != (LSPPosition{Line: 1, Character: 7}) {
67
+ t.Errorf("replacement range = %+v, want character 4..7", pending.replaceRange)
68
+ }
69
+ }
@@ -0,0 +1,85 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+ )
7
+
8
+ type rankedCompletionHintSource struct{ NullPluginSource }
9
+
10
+ func (rankedCompletionHintSource) CompletionHints() []LSPCompletionHint {
11
+ return []LSPCompletionHint{{
12
+ Scope: "jsdoc",
13
+ After: "@evidence ",
14
+ Items: []LSPCompletionItem{
15
+ {Insert: "docs/stable.md", Label: "stable"},
16
+ {Insert: "docs/derived.md"},
17
+ },
18
+ }}
19
+ }
20
+
21
+ // TestLSPCompletionReplacesFullFilterInRankOrder verifies path completion edits.
22
+ //
23
+ // A completion label containing slashes cannot rely on the editor's default
24
+ // word range: replacing only the last segment of `文档/sp` would duplicate the
25
+ // path prefix. The range is measured in UTF-16 units, and explicit sort keys
26
+ // preserve the publisher's ranking instead of letting the client alphabetize it.
27
+ //
28
+ // 1. Match two ranked hints after a non-BMP character and a CJK path prefix.
29
+ // 2. Merge them into the null completion response tsgo uses in JSDoc prose.
30
+ // 3. Assert both text edits replace the full filter and sort in slice order.
31
+ func TestLSPCompletionReplacesFullFilterInRankOrder(t *testing.T) {
32
+ const uri = "file:///project/src/main.ts"
33
+ const text = "/** 😀 @evidence 文档/sp"
34
+ proxy := &Proxy{
35
+ source: rankedCompletionHintSource{},
36
+ documentText: map[string]string{uri: text},
37
+ }
38
+ params, _ := json.Marshal(map[string]any{
39
+ "textDocument": map[string]any{"uri": uri},
40
+ "position": map[string]any{"line": 0, "character": 22},
41
+ })
42
+ pending := proxy.completionItemsFor(Envelope{Params: params})
43
+ if len(pending.items) != 2 {
44
+ t.Fatalf("matched %d completion items, want 2", len(pending.items))
45
+ }
46
+
47
+ merged := mergeCompletionResponseWithRequest(
48
+ []byte(`{"jsonrpc":"2.0","id":1,"result":null}`),
49
+ pending,
50
+ )
51
+ var response struct {
52
+ Result struct {
53
+ Items []struct {
54
+ FilterText string `json:"filterText"`
55
+ SortText string `json:"sortText"`
56
+ TextEdit struct {
57
+ Range LSPRange `json:"range"`
58
+ NewText string `json:"newText"`
59
+ } `json:"textEdit"`
60
+ } `json:"items"`
61
+ } `json:"result"`
62
+ }
63
+ if err := json.Unmarshal(merged, &response); err != nil {
64
+ t.Fatalf("decode merged completion response: %v\n%s", err, merged)
65
+ }
66
+ if len(response.Result.Items) != 2 {
67
+ t.Fatalf("merged %d completion items, want 2", len(response.Result.Items))
68
+ }
69
+ for index, item := range response.Result.Items {
70
+ if item.TextEdit.Range.Start != (LSPPosition{Line: 0, Character: 17}) ||
71
+ item.TextEdit.Range.End != (LSPPosition{Line: 0, Character: 22}) {
72
+ t.Errorf("item %d replacement range = %+v, want character 17..22", index, item.TextEdit.Range)
73
+ }
74
+ if item.FilterText != item.TextEdit.NewText {
75
+ t.Errorf("item %d filterText = %q, want inserted text %q", index, item.FilterText, item.TextEdit.NewText)
76
+ }
77
+ }
78
+ if response.Result.Items[0].SortText >= response.Result.Items[1].SortText {
79
+ t.Errorf(
80
+ "sort keys %q, %q do not preserve publisher order",
81
+ response.Result.Items[0].SortText,
82
+ response.Result.Items[1].SortText,
83
+ )
84
+ }
85
+ }