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,304 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"os"
|
|
9
|
+
"os/exec"
|
|
10
|
+
"strings"
|
|
11
|
+
"sync"
|
|
12
|
+
"time"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// residentRequestTimeout bounds one resident verb round trip, mirroring the
|
|
16
|
+
// spawn-per-verb path's nativePluginCommandTimeout. A rule that hangs must not
|
|
17
|
+
// wedge the daemon's request mutex forever; on timeout the sidecar is killed and
|
|
18
|
+
// the caller falls back to a fresh spawn.
|
|
19
|
+
const residentRequestTimeout = nativePluginCommandTimeout
|
|
20
|
+
|
|
21
|
+
// serveVerbDiagnostics, serveVerbCodeActions, and serveVerbHints are the verbs
|
|
22
|
+
// routed to the resident daemon: the three that load a Program.
|
|
23
|
+
// lsp-command-ids / lsp-code-action-kinds run once at startup and never load one
|
|
24
|
+
// (routing them would spawn the daemon on the initialize path);
|
|
25
|
+
// lsp-execute-command is user-initiated and keeps its spawn-per-verb path.
|
|
26
|
+
const (
|
|
27
|
+
serveVerbDiagnostics = "lsp-diagnostics"
|
|
28
|
+
serveVerbCodeActions = "lsp-code-actions"
|
|
29
|
+
serveVerbHints = "lsp-hints"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// serveClientRequest mirrors linthost's serveLSPRequest: the base project
|
|
33
|
+
// options travel as the daemon's argv, only the per-verb fields per request.
|
|
34
|
+
type serveClientRequest struct {
|
|
35
|
+
Verb string `json:"verb"`
|
|
36
|
+
URI string `json:"uri,omitempty"`
|
|
37
|
+
RangeJSON string `json:"rangeJson,omitempty"`
|
|
38
|
+
ContextJSON string `json:"contextJson,omitempty"`
|
|
39
|
+
Invalidate bool `json:"invalidate,omitempty"`
|
|
40
|
+
Changed []string `json:"changed,omitempty"`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// serveClientResponse mirrors linthost's serveLSPResponse: the verb's JSON
|
|
44
|
+
// result verbatim and the exit code the one-shot verb would have returned.
|
|
45
|
+
type serveClientResponse struct {
|
|
46
|
+
Result json.RawMessage `json:"result"`
|
|
47
|
+
Code int `json:"code"`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// residentSidecar is one long-lived `@ttsc/lint lsp-serve` child. It answers a
|
|
51
|
+
// serialized stream of verb requests over stdin/stdout, holding a warm Program
|
|
52
|
+
// across them, instead of the source respawning the sidecar per verb.
|
|
53
|
+
type residentSidecar struct {
|
|
54
|
+
mu sync.Mutex
|
|
55
|
+
cmd *exec.Cmd
|
|
56
|
+
stdin io.WriteCloser
|
|
57
|
+
stdout *bufio.Reader
|
|
58
|
+
everServed bool
|
|
59
|
+
// invalidate piggybacks a full "drop the warm Program" onto the next request,
|
|
60
|
+
// set for a change the proxy cannot localize.
|
|
61
|
+
invalidate bool
|
|
62
|
+
// changed piggybacks the document URIs that changed on disk onto the next
|
|
63
|
+
// request, so the daemon updates the warm Program incrementally rather than
|
|
64
|
+
// rebuilding it.
|
|
65
|
+
changed []string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// serveRun routes a serve-able verb through the plugin's resident daemon.
|
|
69
|
+
// It returns served=false to tell the caller to fall back to the spawn-per-verb
|
|
70
|
+
// path: a sidecar that predates lsp-serve, a spawn failure, or a pipe that broke
|
|
71
|
+
// mid-session all degrade to exec rather than losing the verb. served=true with
|
|
72
|
+
// an error mirrors a nonzero one-shot exit — the daemon answered and the verb
|
|
73
|
+
// failed — so the caller logs and skips exactly as it does for exec.
|
|
74
|
+
func (s *NativePluginSource) serveRun(plugin NativeLSPPluginEntry, verb string, args []string) ([]byte, bool, error) {
|
|
75
|
+
if s == nil || strings.TrimSpace(plugin.Binary) == "" {
|
|
76
|
+
return nil, false, nil
|
|
77
|
+
}
|
|
78
|
+
s.residentMu.Lock()
|
|
79
|
+
if s.serveUnsupported[plugin.Binary] {
|
|
80
|
+
s.residentMu.Unlock()
|
|
81
|
+
return nil, false, nil
|
|
82
|
+
}
|
|
83
|
+
sc := s.residents[plugin.Binary]
|
|
84
|
+
if sc == nil {
|
|
85
|
+
sc = &residentSidecar{}
|
|
86
|
+
if s.residents == nil {
|
|
87
|
+
s.residents = map[string]*residentSidecar{}
|
|
88
|
+
}
|
|
89
|
+
s.residents[plugin.Binary] = sc
|
|
90
|
+
}
|
|
91
|
+
s.residentMu.Unlock()
|
|
92
|
+
|
|
93
|
+
body, code, err := sc.call(s, plugin, serveRequestFromArgs(verb, args))
|
|
94
|
+
if err != nil {
|
|
95
|
+
// Could not talk to the daemon. If it never once answered, treat lsp-serve
|
|
96
|
+
// as unsupported and stop trying — every sidecar built before this verb
|
|
97
|
+
// rejects it, and retrying would spawn-and-fail per request forever. If it
|
|
98
|
+
// had been answering, the pipe broke; leave it eligible to respawn next
|
|
99
|
+
// call. Either way this call falls back to a fresh spawn so the verb still
|
|
100
|
+
// works.
|
|
101
|
+
s.residentMu.Lock()
|
|
102
|
+
if !sc.everServed {
|
|
103
|
+
if s.serveUnsupported == nil {
|
|
104
|
+
s.serveUnsupported = map[string]bool{}
|
|
105
|
+
}
|
|
106
|
+
s.serveUnsupported[plugin.Binary] = true
|
|
107
|
+
}
|
|
108
|
+
s.residentMu.Unlock()
|
|
109
|
+
return nil, false, nil
|
|
110
|
+
}
|
|
111
|
+
if code != 0 {
|
|
112
|
+
return nil, true, fmt.Errorf("ttscserver: %s %s (resident) exit %d", pluginLabel(plugin), verb, code)
|
|
113
|
+
}
|
|
114
|
+
return body, true, nil
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// call sends one request to the daemon and reads its reply, serializing access
|
|
118
|
+
// to the single pipe. It spawns the child on first use or after a death, and
|
|
119
|
+
// enforces a per-request timeout so a hung rule cannot wedge the mutex.
|
|
120
|
+
func (sc *residentSidecar) call(s *NativePluginSource, plugin NativeLSPPluginEntry, req serveClientRequest) ([]byte, int, error) {
|
|
121
|
+
sc.mu.Lock()
|
|
122
|
+
defer sc.mu.Unlock()
|
|
123
|
+
if sc.cmd == nil {
|
|
124
|
+
if err := sc.spawn(s, plugin); err != nil {
|
|
125
|
+
return nil, 0, err
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if sc.invalidate {
|
|
129
|
+
req.Invalidate = true
|
|
130
|
+
sc.invalidate = false
|
|
131
|
+
}
|
|
132
|
+
if len(sc.changed) > 0 {
|
|
133
|
+
req.Changed = sc.changed
|
|
134
|
+
sc.changed = nil
|
|
135
|
+
}
|
|
136
|
+
line, err := json.Marshal(req)
|
|
137
|
+
if err != nil {
|
|
138
|
+
return nil, 0, err
|
|
139
|
+
}
|
|
140
|
+
line = append(line, '\n')
|
|
141
|
+
if _, err := sc.stdin.Write(line); err != nil {
|
|
142
|
+
sc.kill()
|
|
143
|
+
return nil, 0, err
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
type readResult struct {
|
|
147
|
+
line []byte
|
|
148
|
+
err error
|
|
149
|
+
}
|
|
150
|
+
done := make(chan readResult, 1)
|
|
151
|
+
go func() {
|
|
152
|
+
raw, err := sc.stdout.ReadBytes('\n')
|
|
153
|
+
done <- readResult{line: raw, err: err}
|
|
154
|
+
}()
|
|
155
|
+
select {
|
|
156
|
+
case <-time.After(residentRequestTimeout):
|
|
157
|
+
sc.kill()
|
|
158
|
+
return nil, 0, fmt.Errorf("ttscserver: %s %s (resident) timed out", pluginLabel(plugin), req.Verb)
|
|
159
|
+
case res := <-done:
|
|
160
|
+
if res.err != nil {
|
|
161
|
+
sc.kill()
|
|
162
|
+
return nil, 0, res.err
|
|
163
|
+
}
|
|
164
|
+
var resp serveClientResponse
|
|
165
|
+
if err := json.Unmarshal(res.line, &resp); err != nil {
|
|
166
|
+
sc.kill()
|
|
167
|
+
return nil, 0, err
|
|
168
|
+
}
|
|
169
|
+
sc.everServed = true
|
|
170
|
+
return resp.Result, resp.Code, nil
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// spawn starts the resident child with the base project args and the lsp-serve
|
|
175
|
+
// verb; per-verb fields travel per request. The caller holds sc.mu.
|
|
176
|
+
func (sc *residentSidecar) spawn(s *NativePluginSource, plugin NativeLSPPluginEntry) error {
|
|
177
|
+
allArgs := []string{
|
|
178
|
+
"lsp-serve",
|
|
179
|
+
"--cwd=" + s.cwd,
|
|
180
|
+
"--tsconfig=" + s.tsconfig,
|
|
181
|
+
"--plugins-json=" + s.pluginsJSON,
|
|
182
|
+
}
|
|
183
|
+
if plugin.ProjectContextArgs && strings.TrimSpace(s.projectContextJSON) != "" {
|
|
184
|
+
allArgs = append(allArgs, "--project-context-json="+s.projectContextJSON)
|
|
185
|
+
}
|
|
186
|
+
cmd := exec.Command(plugin.Binary, allArgs...)
|
|
187
|
+
cmd.Dir = s.cwd
|
|
188
|
+
cmd.Env = os.Environ()
|
|
189
|
+
// Drain the child's stderr to the source log so its pipe cannot fill and block
|
|
190
|
+
// the child, and so a resident rule panic is still visible.
|
|
191
|
+
cmd.Stderr = residentStderr{s: s, label: pluginLabel(plugin)}
|
|
192
|
+
stdin, err := cmd.StdinPipe()
|
|
193
|
+
if err != nil {
|
|
194
|
+
return err
|
|
195
|
+
}
|
|
196
|
+
stdout, err := cmd.StdoutPipe()
|
|
197
|
+
if err != nil {
|
|
198
|
+
_ = stdin.Close()
|
|
199
|
+
return err
|
|
200
|
+
}
|
|
201
|
+
if err := cmd.Start(); err != nil {
|
|
202
|
+
_ = stdin.Close()
|
|
203
|
+
return err
|
|
204
|
+
}
|
|
205
|
+
sc.cmd = cmd
|
|
206
|
+
sc.stdin = stdin
|
|
207
|
+
sc.stdout = bufio.NewReader(stdout)
|
|
208
|
+
return nil
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// kill terminates the child and clears its handles so the next call respawns.
|
|
212
|
+
// The caller holds sc.mu.
|
|
213
|
+
func (sc *residentSidecar) kill() {
|
|
214
|
+
if sc.stdin != nil {
|
|
215
|
+
_ = sc.stdin.Close()
|
|
216
|
+
}
|
|
217
|
+
if sc.cmd != nil && sc.cmd.Process != nil {
|
|
218
|
+
_ = sc.cmd.Process.Kill()
|
|
219
|
+
_ = sc.cmd.Wait()
|
|
220
|
+
}
|
|
221
|
+
sc.cmd = nil
|
|
222
|
+
sc.stdin = nil
|
|
223
|
+
sc.stdout = nil
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// InvalidateResidentPrograms tells every live resident daemon that documents
|
|
227
|
+
// changed on disk, so the next request refreshes the warm Program before
|
|
228
|
+
// serving. Given the changed URIs, the daemon updates those files incrementally;
|
|
229
|
+
// given none, it drops the whole Program (a change the proxy could not
|
|
230
|
+
// localize). It mirrors how the proxy already invalidates the symbol provider on
|
|
231
|
+
// the same editor signals.
|
|
232
|
+
func (s *NativePluginSource) InvalidateResidentPrograms(changedURIs ...string) {
|
|
233
|
+
if s == nil {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
s.residentMu.Lock()
|
|
237
|
+
residents := make([]*residentSidecar, 0, len(s.residents))
|
|
238
|
+
for _, sc := range s.residents {
|
|
239
|
+
residents = append(residents, sc)
|
|
240
|
+
}
|
|
241
|
+
s.residentMu.Unlock()
|
|
242
|
+
for _, sc := range residents {
|
|
243
|
+
sc.mu.Lock()
|
|
244
|
+
if len(changedURIs) > 0 {
|
|
245
|
+
sc.changed = append(sc.changed, changedURIs...)
|
|
246
|
+
} else {
|
|
247
|
+
sc.invalidate = true
|
|
248
|
+
}
|
|
249
|
+
sc.mu.Unlock()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// shutdownResidents kills every resident child. Called on server teardown; the
|
|
254
|
+
// children also exit on their own when the parent closes their stdin at process
|
|
255
|
+
// exit, so this is the graceful path, not the only one.
|
|
256
|
+
func (s *NativePluginSource) shutdownResidents() {
|
|
257
|
+
if s == nil {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
s.residentMu.Lock()
|
|
261
|
+
residents := s.residents
|
|
262
|
+
s.residents = map[string]*residentSidecar{}
|
|
263
|
+
s.residentMu.Unlock()
|
|
264
|
+
for _, sc := range residents {
|
|
265
|
+
sc.mu.Lock()
|
|
266
|
+
sc.kill()
|
|
267
|
+
sc.mu.Unlock()
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// serveRequestFromArgs rebuilds a serve request from the same `--flag=value`
|
|
272
|
+
// argv the spawn-per-verb path passes, so the two transports share one call
|
|
273
|
+
// shape at the source's verb methods.
|
|
274
|
+
func serveRequestFromArgs(verb string, args []string) serveClientRequest {
|
|
275
|
+
req := serveClientRequest{Verb: verb}
|
|
276
|
+
for _, arg := range args {
|
|
277
|
+
switch {
|
|
278
|
+
case strings.HasPrefix(arg, "--uri="):
|
|
279
|
+
req.URI = strings.TrimPrefix(arg, "--uri=")
|
|
280
|
+
case strings.HasPrefix(arg, "--range-json="):
|
|
281
|
+
req.RangeJSON = strings.TrimPrefix(arg, "--range-json=")
|
|
282
|
+
case strings.HasPrefix(arg, "--context-json="):
|
|
283
|
+
req.ContextJSON = strings.TrimPrefix(arg, "--context-json=")
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return req
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// residentStderr forwards a resident child's stderr to the source log line by
|
|
290
|
+
// line, so it interleaves cleanly with the source's own logging.
|
|
291
|
+
type residentStderr struct {
|
|
292
|
+
s *NativePluginSource
|
|
293
|
+
label string
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
func (w residentStderr) Write(p []byte) (int, error) {
|
|
297
|
+
for _, line := range strings.Split(strings.TrimRight(string(p), "\n"), "\n") {
|
|
298
|
+
if strings.TrimSpace(line) == "" {
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
w.s.log("%s (resident): %s", w.label, line)
|
|
302
|
+
}
|
|
303
|
+
return len(p), nil
|
|
304
|
+
}
|
|
@@ -34,15 +34,57 @@ const (
|
|
|
34
34
|
LSPDiagnosticSeverityHint LSPDiagnosticSeverity = 4
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
-
// LSPDiagnostic is the
|
|
38
|
-
//
|
|
39
|
-
//
|
|
37
|
+
// LSPDiagnostic is the subset of the LSP Diagnostic type ttscserver injects
|
|
38
|
+
// into outgoing publishDiagnostics. omitempty is set on optional fields so the
|
|
39
|
+
// merged JSON stays close to what tsgo emits.
|
|
40
|
+
//
|
|
41
|
+
// The proxy decodes each sidecar diagnostic into this struct and re-encodes it,
|
|
42
|
+
// so a field absent here is silently dropped on the way to the editor. Every
|
|
43
|
+
// LSP Diagnostic field a producer might set must therefore appear, or the proxy
|
|
44
|
+
// truncates it.
|
|
40
45
|
type LSPDiagnostic struct {
|
|
41
|
-
Range
|
|
42
|
-
Severity
|
|
43
|
-
Code
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
Range LSPRange `json:"range"`
|
|
47
|
+
Severity LSPDiagnosticSeverity `json:"severity,omitempty"`
|
|
48
|
+
Code any `json:"code,omitempty"`
|
|
49
|
+
CodeDescription *LSPCodeDescription `json:"codeDescription,omitempty"`
|
|
50
|
+
// Tags classify the diagnostic (1 = unnecessary, 2 = deprecated). Carried
|
|
51
|
+
// through so a plugin's tag is not silently dropped when the proxy re-encodes
|
|
52
|
+
// the diagnostic — the same truncation codeDescription had to be rescued from.
|
|
53
|
+
Tags []int `json:"tags,omitempty"`
|
|
54
|
+
// Data is opaque state the producer attaches to the diagnostic. The editor
|
|
55
|
+
// preserves it and hands it back on a codeAction request whose context
|
|
56
|
+
// includes this diagnostic, so a rule can recover what it computed without
|
|
57
|
+
// recomputing it. Carried through unread — like the other optional fields, an
|
|
58
|
+
// absent one it did not round-trip would be a silent truncation.
|
|
59
|
+
Data json.RawMessage `json:"data,omitempty"`
|
|
60
|
+
// RelatedInformation are secondary locations the diagnostic points at, each
|
|
61
|
+
// with its own message — the editor renders them as clickable lines under the
|
|
62
|
+
// diagnostic. Carried through so a sidecar's related locations survive the
|
|
63
|
+
// proxy's re-encode, the same truncation the other optional fields had to be
|
|
64
|
+
// rescued from.
|
|
65
|
+
RelatedInformation []LSPDiagnosticRelatedInformation `json:"relatedInformation,omitempty"`
|
|
66
|
+
Source string `json:"source,omitempty"`
|
|
67
|
+
Message string `json:"message"`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// LSPDiagnosticRelatedInformation is one entry of a diagnostic's
|
|
71
|
+
// relatedInformation: a secondary location with a message. It reuses the
|
|
72
|
+
// package's LSPLocation (a URI plus a range). The proxy does not read it — it
|
|
73
|
+
// exists so the field is not dropped on re-encode.
|
|
74
|
+
type LSPDiagnosticRelatedInformation struct {
|
|
75
|
+
Location LSPLocation `json:"location"`
|
|
76
|
+
Message string `json:"message"`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// LSPCodeDescription is the LSP CodeDescription type: a documentation URL for a
|
|
80
|
+
// diagnostic's Code. Editors render the Code as a link to Href, so a rule name
|
|
81
|
+
// in the Problems panel can lead to the rule's docs. The proxy carries whatever
|
|
82
|
+
// a sidecar supplies; it does not synthesize the URL, because only the producer
|
|
83
|
+
// knows what its own Code values mean. @ttsc/lint derives one per rule family
|
|
84
|
+
// in packages/lint/linthost/rule_docs.go and leaves it unset where no vetted
|
|
85
|
+
// page exists.
|
|
86
|
+
type LSPCodeDescription struct {
|
|
87
|
+
Href string `json:"href"`
|
|
46
88
|
}
|
|
47
89
|
|
|
48
90
|
// LSPCodeAction is the minimal Code Action shape ttscserver returns from
|
|
@@ -150,6 +192,41 @@ type PluginSource interface {
|
|
|
150
192
|
// LSP server is hosted without any ttsc plugin pipeline (smoke tests,
|
|
151
193
|
// docs build). It returns no diagnostics/actions and no command ids so the
|
|
152
194
|
// proxy still exercises its merge paths even with no plugin activity.
|
|
195
|
+
// LSPCompletionHint is one group of completion items a plugin offers, together
|
|
196
|
+
// with the declarative rule saying where they apply.
|
|
197
|
+
//
|
|
198
|
+
// The rule has to be data rather than a callback because the plugin that
|
|
199
|
+
// produced it is a subprocess that has already exited. Asking it per keystroke
|
|
200
|
+
// would mean a process spawn and a Program reload per character; the corpus
|
|
201
|
+
// therefore travels once and the proxy answers from memory.
|
|
202
|
+
type LSPCompletionHint struct {
|
|
203
|
+
// Scope names the syntactic region the cursor must sit in.
|
|
204
|
+
Scope string `json:"scope"`
|
|
205
|
+
// After is a literal the line prefix must contain. The text following its
|
|
206
|
+
// LAST occurrence is what the editor filters on and what Insert replaces.
|
|
207
|
+
//
|
|
208
|
+
// Deliberately a literal and not a pattern. A regex would be unvalidatable at
|
|
209
|
+
// discovery time, and the one shipped example of plugin-supplied regex driving
|
|
210
|
+
// completion — Tailwind's experimental.classRegex — is a documented source of
|
|
211
|
+
// editor hangs. When several hints match one line, the occurrence nearest the
|
|
212
|
+
// cursor wins; at that occurrence the longest After wins, and only the same
|
|
213
|
+
// trigger merges. That is enough to layer a corpus without hiding a later
|
|
214
|
+
// trigger behind an earlier one.
|
|
215
|
+
After string `json:"after"`
|
|
216
|
+
// Items are offered in slice order; the proxy derives the sort key from it.
|
|
217
|
+
Items []LSPCompletionItem `json:"items"`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// LSPCompletionItem is one plugin-contributed completion.
|
|
221
|
+
//
|
|
222
|
+
// Fully resolved on arrival: there is no completionItem/resolve round trip,
|
|
223
|
+
// because resolving would require asking a rule that no longer exists.
|
|
224
|
+
type LSPCompletionItem struct {
|
|
225
|
+
Insert string `json:"insert"`
|
|
226
|
+
Label string `json:"label,omitempty"`
|
|
227
|
+
Detail string `json:"detail,omitempty"`
|
|
228
|
+
}
|
|
229
|
+
|
|
153
230
|
type NullPluginSource struct{}
|
|
154
231
|
|
|
155
232
|
// Diagnostics returns no plugin diagnostics.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"testing"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
// TestLSPPluginCompletionResolveStaysLocal verifies completion ownership.
|
|
10
|
+
//
|
|
11
|
+
// TypeScript-Go advertises resolve support but dereferences its private data
|
|
12
|
+
// payload for every resolved item. A plugin hint has no such payload and is
|
|
13
|
+
// already complete, so forwarding it upstream can fault the language server.
|
|
14
|
+
//
|
|
15
|
+
// 1. Encode a plugin completion and send that item back as a resolve request.
|
|
16
|
+
// 2. Assert the proxy answers locally with the item unchanged.
|
|
17
|
+
// 3. Assert an upstream-owned item remains unhandled and is still forwarded.
|
|
18
|
+
func TestLSPPluginCompletionResolveStaysLocal(t *testing.T) {
|
|
19
|
+
merged := mergeCompletionResponse(
|
|
20
|
+
[]byte(`{"jsonrpc":"2.0","id":1,"result":null}`),
|
|
21
|
+
[]LSPCompletionItem{{Insert: "evidence"}},
|
|
22
|
+
)
|
|
23
|
+
var completion struct {
|
|
24
|
+
Result struct {
|
|
25
|
+
Items []json.RawMessage `json:"items"`
|
|
26
|
+
} `json:"result"`
|
|
27
|
+
}
|
|
28
|
+
if err := json.Unmarshal(merged, &completion); err != nil || len(completion.Result.Items) != 1 {
|
|
29
|
+
t.Fatalf("decode plugin completion item: %v\n%s", err, merged)
|
|
30
|
+
}
|
|
31
|
+
item := completion.Result.Items[0]
|
|
32
|
+
|
|
33
|
+
var editor bytes.Buffer
|
|
34
|
+
proxy := &Proxy{editorOut: &editor}
|
|
35
|
+
handled, err := proxy.handleEditorEnvelope(Envelope{
|
|
36
|
+
JSONRPC: "2.0",
|
|
37
|
+
ID: json.RawMessage("7"),
|
|
38
|
+
Method: methodCompletionResolve,
|
|
39
|
+
Params: item,
|
|
40
|
+
}, nil)
|
|
41
|
+
if err != nil {
|
|
42
|
+
t.Fatalf("resolve plugin completion: %v", err)
|
|
43
|
+
}
|
|
44
|
+
if !handled {
|
|
45
|
+
t.Fatal("plugin completion resolve was forwarded upstream")
|
|
46
|
+
}
|
|
47
|
+
_, body, err := NewFrameReader(bytes.NewReader(editor.Bytes())).Read()
|
|
48
|
+
if err != nil {
|
|
49
|
+
t.Fatalf("read local resolve response: %v", err)
|
|
50
|
+
}
|
|
51
|
+
response, err := ParseEnvelope(body)
|
|
52
|
+
if err != nil {
|
|
53
|
+
t.Fatalf("parse local resolve response: %v", err)
|
|
54
|
+
}
|
|
55
|
+
var got any
|
|
56
|
+
var want any
|
|
57
|
+
if err := json.Unmarshal(response.Result, &got); err != nil {
|
|
58
|
+
t.Fatalf("decode local resolve result: %v", err)
|
|
59
|
+
}
|
|
60
|
+
if err := json.Unmarshal(item, &want); err != nil {
|
|
61
|
+
t.Fatalf("decode original plugin item: %v", err)
|
|
62
|
+
}
|
|
63
|
+
gotJSON, _ := json.Marshal(got)
|
|
64
|
+
wantJSON, _ := json.Marshal(want)
|
|
65
|
+
if !bytes.Equal(gotJSON, wantJSON) {
|
|
66
|
+
t.Errorf("local resolve changed the plugin item:\n got %s\nwant %s", gotJSON, wantJSON)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
editor.Reset()
|
|
70
|
+
handled, err = proxy.handleEditorEnvelope(Envelope{
|
|
71
|
+
JSONRPC: "2.0",
|
|
72
|
+
ID: json.RawMessage("8"),
|
|
73
|
+
Method: methodCompletionResolve,
|
|
74
|
+
Params: json.RawMessage(`{"label":"upstream","data":{"fileName":"/project/main.ts"}}`),
|
|
75
|
+
}, nil)
|
|
76
|
+
if err != nil {
|
|
77
|
+
t.Fatalf("route upstream completion resolve: %v", err)
|
|
78
|
+
}
|
|
79
|
+
if handled || editor.Len() != 0 {
|
|
80
|
+
t.Error("upstream completion resolve was intercepted instead of forwarded")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
// TestLSPPositionConversionsSpanAstralAndCombiningRunes verifies that both
|
|
6
|
+
// proxy-side position converters spend a UTF-16 budget, not a byte or rune one,
|
|
7
|
+
// across every width a line can hold.
|
|
8
|
+
//
|
|
9
|
+
// UTF-16 is the encoding ttscserver pins for the whole session
|
|
10
|
+
// (constrainInitializePositionEncoding), so these two helpers are the contract
|
|
11
|
+
// for every feature that maps an editor column onto the cached buffer:
|
|
12
|
+
// incremental didChange splicing and plugin completion. An astral rune is the
|
|
13
|
+
// boundary that separates the three counting schemes at once — four bytes, one
|
|
14
|
+
// rune, two UTF-16 units — and a combining mark is the boundary where two runes
|
|
15
|
+
// render as one grapheme but still cost two units.
|
|
16
|
+
//
|
|
17
|
+
// 1. Convert the column just past the identifier on lines built from ASCII, BMP
|
|
18
|
+
// CJK, an astral rune, and a combining sequence.
|
|
19
|
+
// 2. Assert both converters return the same byte offset for each.
|
|
20
|
+
// 3. Assert the line walk, the end-of-line column, and an out-of-range column.
|
|
21
|
+
func TestLSPPositionConversionsSpanAstralAndCombiningRunes(t *testing.T) {
|
|
22
|
+
cases := []struct {
|
|
23
|
+
name string
|
|
24
|
+
text string
|
|
25
|
+
line int
|
|
26
|
+
character int
|
|
27
|
+
want int
|
|
28
|
+
wantOK bool
|
|
29
|
+
}{
|
|
30
|
+
{
|
|
31
|
+
name: "ascii counts one unit per byte",
|
|
32
|
+
text: "const value = 1;",
|
|
33
|
+
character: 11,
|
|
34
|
+
want: 11,
|
|
35
|
+
wantOK: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "bmp cjk is one unit and three bytes",
|
|
39
|
+
text: "const 变量 = 1;",
|
|
40
|
+
character: 8,
|
|
41
|
+
want: 12,
|
|
42
|
+
wantOK: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "an astral rune is two units and four bytes",
|
|
46
|
+
text: "const \U0001D499 = 1;",
|
|
47
|
+
character: 8,
|
|
48
|
+
want: 10,
|
|
49
|
+
wantOK: true,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "a combining mark costs its own unit",
|
|
53
|
+
text: "const e\u0301 = 1;",
|
|
54
|
+
character: 8,
|
|
55
|
+
want: 9,
|
|
56
|
+
wantOK: true,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "the column before an astral rune stops before its bytes",
|
|
60
|
+
text: "const \U0001D499 = 1;",
|
|
61
|
+
character: 6,
|
|
62
|
+
want: 6,
|
|
63
|
+
wantOK: true,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "the walk reaches the target line first",
|
|
67
|
+
text: "a\nconst \U0001D499 = 1;",
|
|
68
|
+
line: 1,
|
|
69
|
+
character: 8,
|
|
70
|
+
want: 12,
|
|
71
|
+
wantOK: true,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "the column at end of line is in range",
|
|
75
|
+
text: "const \U0001D499",
|
|
76
|
+
character: 8,
|
|
77
|
+
want: 10,
|
|
78
|
+
wantOK: true,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for _, testCase := range cases {
|
|
83
|
+
t.Run(testCase.name, func(t *testing.T) {
|
|
84
|
+
got, ok := lspPositionToByteOffset(testCase.text, lspPositionWire{
|
|
85
|
+
Line: testCase.line,
|
|
86
|
+
Character: testCase.character,
|
|
87
|
+
})
|
|
88
|
+
if got != testCase.want || ok != testCase.wantOK {
|
|
89
|
+
t.Errorf("lspPositionToByteOffset = (%d, %t), want (%d, %t)",
|
|
90
|
+
got, ok, testCase.want, testCase.wantOK)
|
|
91
|
+
}
|
|
92
|
+
completionOffset, completionOK := offsetForPosition(
|
|
93
|
+
testCase.text,
|
|
94
|
+
testCase.line,
|
|
95
|
+
testCase.character,
|
|
96
|
+
)
|
|
97
|
+
if completionOffset != testCase.want || !completionOK {
|
|
98
|
+
t.Errorf("offsetForPosition = (%d, %t), want (%d, true)",
|
|
99
|
+
completionOffset, completionOK, testCase.want)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A column past the end of the line is a cache/editor divergence, not a
|
|
105
|
+
// clamped position: the buffer cache drops its entry rather than splice at a
|
|
106
|
+
// guessed byte.
|
|
107
|
+
if got, ok := lspPositionToByteOffset("const \U0001D499", lspPositionWire{Character: 99}); ok {
|
|
108
|
+
t.Errorf("lspPositionToByteOffset past end = (%d, true), want ok=false", got)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The completion replace range is measured in the same units, so the filter
|
|
112
|
+
// width of a mixed-width prefix must agree with the columns above.
|
|
113
|
+
if units := utf16Length("é\U0001D499"); units != 4 {
|
|
114
|
+
t.Errorf("utf16Length = %d, want 4 (e + combining acute + astral pair)", units)
|
|
115
|
+
}
|
|
116
|
+
if units := utf16Length(""); units != 0 {
|
|
117
|
+
t.Errorf("utf16Length of the empty filter = %d, want 0", units)
|
|
118
|
+
}
|
|
119
|
+
}
|