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.
- 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 +312 -108
- 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_schema_version_matches_the_typescript_client_test.go +1 -1
- 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 +220 -5
- package/internal/graph/enum_members_carry_their_names_on_the_enum_node_test.go +131 -0
- package/internal/graph/graph.go +177 -5
- package/internal/graph/line_model.go +122 -0
- package/internal/graph/literals.go +175 -0
- package/internal/graph/literals_are_absent_when_the_type_admits_more_than_it_can_name_test.go +74 -0
- package/internal/graph/literals_cover_enum_members_the_graph_has_no_nodes_for_test.go +79 -0
- package/internal/graph/literals_do_not_depend_on_declaration_line_wrapping_test.go +84 -0
- package/internal/graph/literals_render_every_value_in_typescript_source_form_test.go +77 -0
- package/internal/graph/literals_resolve_through_alias_indirection_test.go +64 -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 +113 -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,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
|
+
}
|