ttsc 0.21.0 → 0.22.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/cmd/ttscserver/main.go +79 -3
- package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
- package/driver/lsp.go +4 -0
- package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
- package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
- package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
- package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
- package/internal/lspserver/lsp_native_plugin_source.go +171 -55
- package/internal/lspserver/lsp_native_resident.go +110 -9
- package/internal/lspserver/lsp_plugin.go +6 -0
- package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
- package/internal/lspserver/lsp_project_diagnostics.go +235 -0
- package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
- package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
- package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
- package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
- package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
- package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
- package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
- package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
- package/internal/lspserver/lsp_project_input_registration.go +504 -0
- package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
- package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
- package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
- package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
- package/internal/lspserver/lsp_project_inputs.go +944 -0
- package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
- package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
- package/internal/lspserver/lsp_proxy.go +591 -28
- package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
- package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
- package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
- package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
- package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
- package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
- package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
- package/lib/compiler/internal/project/readProjectConfig.js +46 -14
- package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
- package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
- package/lib/compiler/internal/residentCheckProcess.js +227 -0
- package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
- package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
- package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
- package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
- package/lib/compiler/internal/runBuild.d.ts +64 -1
- package/lib/compiler/internal/runBuild.js +484 -7
- package/lib/compiler/internal/runBuild.js.map +1 -1
- package/lib/compiler/internal/runSingleFileEmit.js +1 -0
- package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
- package/lib/flags/parser.js +13 -17
- package/lib/flags/parser.js.map +1 -1
- package/lib/flags/schema.d.ts +10 -3
- package/lib/flags/schema.js +99 -0
- package/lib/flags/schema.js.map +1 -1
- package/lib/internal/projectInputPathIdentity.d.ts +23 -0
- package/lib/internal/projectInputPathIdentity.js +222 -0
- package/lib/internal/projectInputPathIdentity.js.map +1 -0
- package/lib/launcher/internal/runTtsc.d.ts +17 -0
- package/lib/launcher/internal/runTtsc.js +121 -88
- package/lib/launcher/internal/runTtsc.js.map +1 -1
- package/lib/launcher/internal/runTtscserver.d.ts +13 -1
- package/lib/launcher/internal/runTtscserver.js +415 -45
- package/lib/launcher/internal/runTtscserver.js.map +1 -1
- package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
- package/lib/launcher/internal/singleFileOutput.js +103 -0
- package/lib/launcher/internal/singleFileOutput.js.map +1 -0
- package/lib/launcher/internal/watchTopology.d.ts +214 -2
- package/lib/launcher/internal/watchTopology.js +1560 -41
- package/lib/launcher/internal/watchTopology.js.map +1 -1
- package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
- package/lib/plugin/internal/loadProjectPlugins.js +5 -0
- package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
- package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
- package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
- package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
- package/package.json +8 -8
- package/src/compiler/internal/project/readProjectConfig.ts +62 -8
- package/src/compiler/internal/residentCheckProcess.ts +279 -0
- package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
- package/src/compiler/internal/runBuild.ts +728 -12
- package/src/compiler/internal/runSingleFileEmit.ts +1 -0
- package/src/flags/parser.ts +17 -17
- package/src/flags/schema.ts +111 -3
- package/src/internal/projectInputPathIdentity.ts +273 -0
- package/src/launcher/internal/runTtsc.ts +141 -117
- package/src/launcher/internal/runTtscserver.ts +561 -54
- package/src/launcher/internal/singleFileOutput.ts +133 -0
- package/src/launcher/internal/watchTopology.ts +2008 -59
- package/src/plugin/internal/loadProjectPlugins.ts +8 -0
- package/src/structures/ITtscPluginCapabilities.ts +34 -0
- package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
- package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
package/cmd/ttscserver/main.go
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Command ttscserver is the Go LSP host shipped by ttsc. It wraps the
|
|
2
2
|
// project-selected TypeScript-Go LSP server process and proxies traffic
|
|
3
3
|
// between the editor and that server. The JavaScript launcher resolves project
|
|
4
|
-
// plugins first and passes
|
|
5
|
-
// diagnostics, code actions, and ttsc-owned executeCommand
|
|
4
|
+
// plugins first and passes a private LSP manifest file so this command can
|
|
5
|
+
// merge plugin diagnostics, code actions, and ttsc-owned executeCommand
|
|
6
|
+
// handling.
|
|
6
7
|
//
|
|
7
8
|
// The JavaScript launcher (`packages/ttsc/src/launcher/ttscserver.ts`)
|
|
8
9
|
// resolves the native binary and forwards stdio so editors can spawn
|
|
@@ -29,6 +30,8 @@ import (
|
|
|
29
30
|
"github.com/samchon/ttsc/packages/ttsc/internal/lspserver"
|
|
30
31
|
)
|
|
31
32
|
|
|
33
|
+
const lspPluginManifestMaxBytes = 64 * 1024 * 1024
|
|
34
|
+
|
|
32
35
|
// Build metadata; overwritten via -ldflags in release builds.
|
|
33
36
|
var (
|
|
34
37
|
version = "0.0.0-dev"
|
|
@@ -94,6 +97,12 @@ func runLSP(args []string) int {
|
|
|
94
97
|
suppressExecuteCommandProviderFlag := fs.Bool("suppress-execute-command-provider", false, "do not advertise ttsc executeCommand ids during initialize")
|
|
95
98
|
suppressExecuteCommandIDsFlag := fs.String("suppress-execute-command-ids", "", "comma-separated ttsc executeCommand ids to omit during initialize")
|
|
96
99
|
executeCommandIDPrefixFlag := fs.String("execute-command-id-prefix", "", "prefix to apply to advertised executeCommand ids")
|
|
100
|
+
// The launcher passes this flag only when it actually resolved LSP-capable
|
|
101
|
+
// plugins, so a host too old to know it refuses to start instead of running
|
|
102
|
+
// silently without every plugin the project declared. Version skew between
|
|
103
|
+
// the launcher and a TTSCSERVER_BINARY override is therefore visible at the
|
|
104
|
+
// first request rather than diagnosed from missing diagnostics.
|
|
105
|
+
lspPluginsFileFlag := fs.String("lsp-plugins-file", "", "path to the private LSP plugin manifest written by the ttsc launcher")
|
|
97
106
|
_ = fs.String("clientProcessId", "", "ignored VSCode language-client compatibility flag")
|
|
98
107
|
if err := fs.Parse(args); err != nil {
|
|
99
108
|
return 2
|
|
@@ -121,10 +130,15 @@ func runLSP(args []string) int {
|
|
|
121
130
|
tsgoBinary = strings.TrimSpace(os.Getenv("TTSC_TSGO_BINARY"))
|
|
122
131
|
}
|
|
123
132
|
|
|
133
|
+
manifestJSON, err := lspPluginManifestJSON(strings.TrimSpace(*lspPluginsFileFlag))
|
|
134
|
+
if err != nil {
|
|
135
|
+
fmt.Fprintf(stderr, "ttscserver: %v\n", err)
|
|
136
|
+
return 2
|
|
137
|
+
}
|
|
124
138
|
source, err := lspserver.NewNativePluginSource(lspserver.NativePluginSourceOptions{
|
|
125
139
|
Cwd: cwd,
|
|
126
140
|
Err: stderr,
|
|
127
|
-
ManifestJSON:
|
|
141
|
+
ManifestJSON: manifestJSON,
|
|
128
142
|
Tsconfig: strings.TrimSpace(*tsconfigFlag),
|
|
129
143
|
})
|
|
130
144
|
if err != nil {
|
|
@@ -160,6 +174,63 @@ func runLSP(args []string) int {
|
|
|
160
174
|
return 0
|
|
161
175
|
}
|
|
162
176
|
|
|
177
|
+
// lspPluginManifestJSON reads the private plugin manifest prepared for this
|
|
178
|
+
// process and takes ownership of it.
|
|
179
|
+
//
|
|
180
|
+
// The manifest names every resolved project plugin and its launch context, so
|
|
181
|
+
// the copy this process was given to own is consumed exactly once: the file the
|
|
182
|
+
// launcher named with the flag is removed as soon as it has been read, which
|
|
183
|
+
// means a forcibly terminated launcher cannot leave the payload on disk. Both
|
|
184
|
+
// transport variables are cleared from this process either way, so no plugin
|
|
185
|
+
// sidecar spawned later inherits the payload or a path to it. The environment
|
|
186
|
+
// forms remain accepted because an editor pointed straight at a native binary
|
|
187
|
+
// has no launcher to pass the flag, and a manifest supplied that way is read
|
|
188
|
+
// without being removed, because it belongs to whoever wrote it.
|
|
189
|
+
func lspPluginManifestJSON(flagLocation string) (string, error) {
|
|
190
|
+
defer func() {
|
|
191
|
+
os.Unsetenv("TTSC_LSP_PLUGINS_FILE")
|
|
192
|
+
os.Unsetenv("TTSC_LSP_PLUGINS_JSON")
|
|
193
|
+
}()
|
|
194
|
+
if flagLocation != "" {
|
|
195
|
+
body, err := readLSPPluginManifestFile("--lsp-plugins-file", flagLocation)
|
|
196
|
+
if err != nil {
|
|
197
|
+
return "", err
|
|
198
|
+
}
|
|
199
|
+
// Only the flag names a file the launcher created for this process, so
|
|
200
|
+
// only that one is consumed here. Removing it is what keeps a forcibly
|
|
201
|
+
// terminated launcher from leaving the payload on disk. A path supplied
|
|
202
|
+
// out of band belongs to whoever wrote it and must survive the read, or
|
|
203
|
+
// the next session would start without the plugins it declared.
|
|
204
|
+
os.Remove(flagLocation)
|
|
205
|
+
return body, nil
|
|
206
|
+
}
|
|
207
|
+
location := strings.TrimSpace(os.Getenv("TTSC_LSP_PLUGINS_FILE"))
|
|
208
|
+
if location == "" {
|
|
209
|
+
return os.Getenv("TTSC_LSP_PLUGINS_JSON"), nil
|
|
210
|
+
}
|
|
211
|
+
return readLSPPluginManifestFile("TTSC_LSP_PLUGINS_FILE", location)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func readLSPPluginManifestFile(source, location string) (string, error) {
|
|
215
|
+
input, err := os.Open(location)
|
|
216
|
+
if err != nil {
|
|
217
|
+
return "", fmt.Errorf("read %s: %w", source, err)
|
|
218
|
+
}
|
|
219
|
+
defer input.Close()
|
|
220
|
+
body, err := io.ReadAll(io.LimitReader(input, lspPluginManifestMaxBytes+1))
|
|
221
|
+
if err != nil {
|
|
222
|
+
return "", fmt.Errorf("read %s: %w", source, err)
|
|
223
|
+
}
|
|
224
|
+
if len(body) > lspPluginManifestMaxBytes {
|
|
225
|
+
return "", fmt.Errorf(
|
|
226
|
+
"%s exceeds %d bytes",
|
|
227
|
+
source,
|
|
228
|
+
lspPluginManifestMaxBytes,
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
return string(body), nil
|
|
232
|
+
}
|
|
233
|
+
|
|
163
234
|
func printVersion(w io.Writer) {
|
|
164
235
|
fmt.Fprintf(
|
|
165
236
|
w,
|
|
@@ -187,6 +258,11 @@ Options:
|
|
|
187
258
|
--cwd <dir> Project root used as the tsgo server working directory.
|
|
188
259
|
--tsconfig <path> Project config path used by ttsc plugin sidecars.
|
|
189
260
|
--tsgo <path> Absolute tsgo binary path (defaults to TTSC_TSGO_BINARY).
|
|
261
|
+
--lsp-plugins-file <path>
|
|
262
|
+
Private plugin manifest written by the ttsc launcher; it is
|
|
263
|
+
consumed and deleted at startup. Editors that spawn this
|
|
264
|
+
binary directly supply their own manifest through
|
|
265
|
+
TTSC_LSP_PLUGINS_FILE instead, which is never deleted.
|
|
190
266
|
--suppress-execute-command-provider
|
|
191
267
|
Do not advertise ttsc executeCommand ids during initialize.
|
|
192
268
|
--suppress-execute-command-ids <ids>
|
|
@@ -3,25 +3,39 @@ package main
|
|
|
3
3
|
import (
|
|
4
4
|
"bytes"
|
|
5
5
|
"context"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
6
9
|
"testing"
|
|
7
10
|
|
|
8
11
|
"github.com/samchon/ttsc/packages/ttsc/internal/lspserver"
|
|
9
12
|
)
|
|
10
13
|
|
|
11
14
|
// TestRunLSPUsesNativePluginManifestSource verifies the command wires the
|
|
12
|
-
// launcher-provided plugin manifest into the proxy source.
|
|
15
|
+
// launcher-provided plugin manifest into the proxy source and then owns it.
|
|
13
16
|
//
|
|
14
17
|
// A previous implementation always passed NullPluginSource, so editor sessions
|
|
15
|
-
// could never receive ttsc plugin diagnostics or commands.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
+
// could never receive ttsc plugin diagnostics or commands. The manifest also
|
|
19
|
+
// names every resolved plugin and its launch context, so leaving it readable
|
|
20
|
+
// for the process lifetime exposes it to any sidecar this host later spawns.
|
|
21
|
+
// This test pins both halves of that seam without starting tsgo.
|
|
18
22
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
+
// 1. Set the legacy TTSC_LSP_PLUGINS_JSON fallback to a valid manifest.
|
|
24
|
+
// 2. Substitute runLSPServer and capture its LSPServerOptions.
|
|
25
|
+
// 3. Run `ttscserver --stdio` and assert the native source is selected, and
|
|
26
|
+
// that the legacy payload no longer reaches a spawned sidecar.
|
|
27
|
+
// 4. Put a valid manifest in TTSC_LSP_PLUGINS_FILE while making the legacy
|
|
28
|
+
// environment payload invalid, and prove the bounded file transport wins,
|
|
29
|
+
// leaves neither variable behind, and survives the read because an editor
|
|
30
|
+
// that supplies it out of band must be able to launch twice.
|
|
31
|
+
// 5. Pass a manifest through --lsp-plugins-file while both environment forms
|
|
32
|
+
// are invalid, and prove the flag transport takes precedence and is
|
|
33
|
+
// consumed, because the launcher created that file for this process alone.
|
|
34
|
+
// 6. Point the flag at a missing manifest and prove the command fails instead
|
|
35
|
+
// of silently serving a project without its declared plugins.
|
|
23
36
|
func TestRunLSPUsesNativePluginManifestSource(t *testing.T) {
|
|
24
37
|
t.Setenv("TTSC_LSP_PLUGINS_JSON", `{"plugins":[],"lspPlugins":[]}`)
|
|
38
|
+
t.Setenv("TTSC_LSP_PLUGINS_FILE", "")
|
|
25
39
|
|
|
26
40
|
prev := runLSPServer
|
|
27
41
|
var captured lspserver.LSPServerOptions
|
|
@@ -31,15 +45,81 @@ func TestRunLSPUsesNativePluginManifestSource(t *testing.T) {
|
|
|
31
45
|
}
|
|
32
46
|
defer func() { runLSPServer = prev }()
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
invoke := func(extra ...string) (int, string) {
|
|
49
|
+
t.Helper()
|
|
50
|
+
outBuf := &bytes.Buffer{}
|
|
51
|
+
errBuf := &bytes.Buffer{}
|
|
52
|
+
code := 0
|
|
53
|
+
withIO(t, outBuf, errBuf, nil, func() {
|
|
54
|
+
code = runLSP(append(append([]string{}, extra...), []string{
|
|
55
|
+
"--stdio",
|
|
56
|
+
"--cwd",
|
|
57
|
+
t.TempDir(),
|
|
58
|
+
"--tsconfig",
|
|
59
|
+
"tsconfig.app.json",
|
|
60
|
+
}...))
|
|
61
|
+
})
|
|
62
|
+
return code, errBuf.String()
|
|
63
|
+
}
|
|
64
|
+
run := func(label string, extra ...string) {
|
|
65
|
+
t.Helper()
|
|
66
|
+
code, stderrText := invoke(extra...)
|
|
67
|
+
if code != 0 {
|
|
68
|
+
t.Fatalf("%s: expected exit 0, got %d (stderr=%q)", label, code, stderrText)
|
|
69
|
+
}
|
|
70
|
+
if _, ok := captured.Source.(*lspserver.NativePluginSource); !ok {
|
|
71
|
+
t.Fatalf("%s: expected NativePluginSource, got %T", label, captured.Source)
|
|
39
72
|
}
|
|
40
|
-
|
|
73
|
+
for _, name := range []string{
|
|
74
|
+
"TTSC_LSP_PLUGINS_FILE",
|
|
75
|
+
"TTSC_LSP_PLUGINS_JSON",
|
|
76
|
+
} {
|
|
77
|
+
if value := os.Getenv(name); value != "" {
|
|
78
|
+
t.Fatalf("%s: %s survived startup as %q", label, name, value)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
writeManifest := func() string {
|
|
83
|
+
t.Helper()
|
|
84
|
+
location := filepath.Join(t.TempDir(), "plugins.json")
|
|
85
|
+
if err := os.WriteFile(
|
|
86
|
+
location,
|
|
87
|
+
[]byte(`{"plugins":[],"lspPlugins":[]}`),
|
|
88
|
+
0o600,
|
|
89
|
+
); err != nil {
|
|
90
|
+
t.Fatal(err)
|
|
91
|
+
}
|
|
92
|
+
return location
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
run("legacy JSON")
|
|
41
96
|
|
|
42
|
-
|
|
43
|
-
|
|
97
|
+
environmentManifest := writeManifest()
|
|
98
|
+
t.Setenv("TTSC_LSP_PLUGINS_JSON", "{invalid")
|
|
99
|
+
t.Setenv("TTSC_LSP_PLUGINS_FILE", environmentManifest)
|
|
100
|
+
run("manifest file")
|
|
101
|
+
if _, err := os.Stat(environmentManifest); err != nil {
|
|
102
|
+
t.Fatalf("out-of-band manifest was consumed by its reader: %v", err)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
flagManifest := writeManifest()
|
|
106
|
+
t.Setenv("TTSC_LSP_PLUGINS_JSON", "{invalid")
|
|
107
|
+
t.Setenv("TTSC_LSP_PLUGINS_FILE", filepath.Join(t.TempDir(), "absent.json"))
|
|
108
|
+
run("manifest flag", "--lsp-plugins-file", flagManifest)
|
|
109
|
+
if _, err := os.Stat(flagManifest); !os.IsNotExist(err) {
|
|
110
|
+
t.Fatalf("flag manifest survived startup: %v", err)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
t.Setenv("TTSC_LSP_PLUGINS_JSON", `{"plugins":[],"lspPlugins":[]}`)
|
|
114
|
+
t.Setenv("TTSC_LSP_PLUGINS_FILE", "")
|
|
115
|
+
code, stderrText := invoke(
|
|
116
|
+
"--lsp-plugins-file",
|
|
117
|
+
filepath.Join(t.TempDir(), "absent.json"),
|
|
118
|
+
)
|
|
119
|
+
if code == 0 {
|
|
120
|
+
t.Fatal("a missing manifest must not start the host")
|
|
121
|
+
}
|
|
122
|
+
if !strings.Contains(stderrText, "--lsp-plugins-file") {
|
|
123
|
+
t.Fatalf("expected the failing transport to be named, got %q", stderrText)
|
|
44
124
|
}
|
|
45
125
|
}
|
package/driver/lsp.go
CHANGED
|
@@ -52,6 +52,10 @@ type LSPDocumentVersion = lspserver.LSPDocumentVersion
|
|
|
52
52
|
// LSPProjectDiagnostics is the driver-level alias for a project publication.
|
|
53
53
|
type LSPProjectDiagnostics = lspserver.LSPProjectDiagnostics
|
|
54
54
|
|
|
55
|
+
// LSPProjectInputSnapshot is the driver-level alias for the set of paths a
|
|
56
|
+
// producer declares its answers depend on.
|
|
57
|
+
type LSPProjectInputSnapshot = lspserver.LSPProjectInputSnapshot
|
|
58
|
+
|
|
55
59
|
// LSPDiagnosticsResult separates document and project plugin diagnostics.
|
|
56
60
|
type LSPDiagnosticsResult = lspserver.LSPDiagnosticsResult
|
|
57
61
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
// TestLSPDocumentDiagnosticsOmitsCachedProjectWhenCurrentResultHasNone
|
|
6
|
+
// verifies last-good cache state is not presented as a current document-cycle
|
|
7
|
+
// project result.
|
|
8
|
+
//
|
|
9
|
+
// A parse failure can make lsp-diagnostics omit project data. Returning the
|
|
10
|
+
// prior cache in that response lets the proxy complete a newer pending external
|
|
11
|
+
// refresh with stale evidence.
|
|
12
|
+
//
|
|
13
|
+
// 1. Seed one producer's last-good project publication.
|
|
14
|
+
// 2. Make the current document diagnostic invocation fail before publication.
|
|
15
|
+
// 3. Assert document diagnostics omit Project while the cache remains intact.
|
|
16
|
+
func TestLSPDocumentDiagnosticsOmitsCachedProjectWhenCurrentResultHasNone(
|
|
17
|
+
t *testing.T,
|
|
18
|
+
) {
|
|
19
|
+
plugin := NativeLSPPluginEntry{
|
|
20
|
+
Binary: "ttsc-no-such-document-diagnostics-sidecar",
|
|
21
|
+
Name: "@ttsc/cached",
|
|
22
|
+
}
|
|
23
|
+
source := &NativePluginSource{plugins: []NativeLSPPluginEntry{plugin}}
|
|
24
|
+
source.storeProjectDiagnostics(
|
|
25
|
+
plugin,
|
|
26
|
+
1,
|
|
27
|
+
&LSPProjectDiagnostics{
|
|
28
|
+
URI: "file:///project/tsconfig.json",
|
|
29
|
+
Diagnostics: []LSPDiagnostic{{
|
|
30
|
+
Code: "stale",
|
|
31
|
+
Message: "stale",
|
|
32
|
+
}},
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
got := source.Diagnostics(LSPDocumentVersion{
|
|
37
|
+
URI: "file:///project/src/main.ts",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if got.Project != nil {
|
|
41
|
+
t.Fatalf("current omitted project result reused cache: %#v", got.Project)
|
|
42
|
+
}
|
|
43
|
+
cached := source.projectDiagnosticsSnapshot()
|
|
44
|
+
if cached == nil || len(cached.Diagnostics) != 1 ||
|
|
45
|
+
cached.Diagnostics[0].Code != "stale" {
|
|
46
|
+
t.Fatalf("last-good cache was not retained: %#v", cached)
|
|
47
|
+
}
|
|
48
|
+
}
|
package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"errors"
|
|
7
|
+
"io"
|
|
8
|
+
"testing"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
type executableProjectInputSource struct {
|
|
12
|
+
NullPluginSource
|
|
13
|
+
reloadURI string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func (s executableProjectInputSource) ProjectInputReloadMatchesChange(
|
|
17
|
+
uri string,
|
|
18
|
+
_ *int,
|
|
19
|
+
) bool {
|
|
20
|
+
return uri == s.reloadURI
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// TestLSPExecutableProjectInputChangeRequestsLauncherRestart verifies an input
|
|
24
|
+
// that can alter the descriptor's contributor set cannot be handled as an
|
|
25
|
+
// ordinary native resident-program refresh.
|
|
26
|
+
//
|
|
27
|
+
// The JavaScript launcher owns descriptor evaluation and native contributor
|
|
28
|
+
// compilation, so the already-running Go process cannot safely mutate this
|
|
29
|
+
// selection in place.
|
|
30
|
+
//
|
|
31
|
+
// 1. Send a watched change for the source's executable reload URI.
|
|
32
|
+
// 2. Assert the proxy announces the expected lifecycle transition, then
|
|
33
|
+
// returns its stable restart sentinel before forwarding.
|
|
34
|
+
// 3. Send an unrelated change and assert it follows the ordinary forwarded
|
|
35
|
+
// invalidation path without another lifecycle notification.
|
|
36
|
+
func TestLSPExecutableProjectInputChangeRequestsLauncherRestart(t *testing.T) {
|
|
37
|
+
reloadURI := "file:///project/lint.config.ts"
|
|
38
|
+
var editor bytes.Buffer
|
|
39
|
+
proxy := NewProxy(ProxyOptions{
|
|
40
|
+
EditorOut: &editor,
|
|
41
|
+
UpstreamIn: io.Discard,
|
|
42
|
+
Source: executableProjectInputSource{
|
|
43
|
+
reloadURI: reloadURI,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
handled, err := proxy.handleEditorEnvelope(
|
|
47
|
+
watchedFilesEnvelope(
|
|
48
|
+
t,
|
|
49
|
+
`{"changes":[{"uri":"`+reloadURI+`","type":2}]}`,
|
|
50
|
+
),
|
|
51
|
+
nil,
|
|
52
|
+
)
|
|
53
|
+
if handled {
|
|
54
|
+
t.Fatal("executable reload notification was marked locally handled")
|
|
55
|
+
}
|
|
56
|
+
if !errors.Is(err, ErrLSPPluginSelectionChanged) {
|
|
57
|
+
t.Fatalf("reload error = %v, want %v", err, ErrLSPPluginSelectionChanged)
|
|
58
|
+
}
|
|
59
|
+
_, body, err := NewFrameReader(&editor).Read()
|
|
60
|
+
if err != nil {
|
|
61
|
+
t.Fatalf("read plugin-selection notification: %v", err)
|
|
62
|
+
}
|
|
63
|
+
notification, err := ParseEnvelope(body)
|
|
64
|
+
if err != nil {
|
|
65
|
+
t.Fatalf("parse plugin-selection notification: %v", err)
|
|
66
|
+
}
|
|
67
|
+
if notification.Method != methodPluginSelectionChanged {
|
|
68
|
+
t.Fatalf(
|
|
69
|
+
"notification method = %q, want %q",
|
|
70
|
+
notification.Method,
|
|
71
|
+
methodPluginSelectionChanged,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
var params map[string]string
|
|
75
|
+
if err := json.Unmarshal(notification.Params, ¶ms); err != nil {
|
|
76
|
+
t.Fatalf("parse plugin-selection notification params: %v", err)
|
|
77
|
+
}
|
|
78
|
+
if params["reason"] != "projectInputChanged" {
|
|
79
|
+
t.Fatalf("notification params = %#v", params)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
handled, err = proxy.handleEditorEnvelope(
|
|
83
|
+
watchedFilesEnvelope(
|
|
84
|
+
t,
|
|
85
|
+
`{"changes":[{"uri":"file:///project/src/main.ts","type":2}]}`,
|
|
86
|
+
),
|
|
87
|
+
nil,
|
|
88
|
+
)
|
|
89
|
+
if err != nil {
|
|
90
|
+
t.Fatalf("ordinary watched change: %v", err)
|
|
91
|
+
}
|
|
92
|
+
if handled {
|
|
93
|
+
t.Fatal("ordinary watched change was swallowed instead of forwarded")
|
|
94
|
+
}
|
|
95
|
+
if editor.Len() != 0 {
|
|
96
|
+
t.Fatalf("ordinary change wrote an unexpected editor frame: %q", editor.String())
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package lspserver
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
// TestExternalWatchedChangeRetainsProgram verifies external data events preserve
|
|
6
|
+
// the warm Program without hiding TypeScript root-set changes.
|
|
7
|
+
//
|
|
8
|
+
// Project-input globs may overlap source files even though Markdown and
|
|
9
|
+
// OpenAPI are the common case, but a JSON path may also be a resolveJsonModule
|
|
10
|
+
// source. JSON create/delete changes possible Program topology, while in-place
|
|
11
|
+
// JSON edits and data-only reference topology preserve the current root graph,
|
|
12
|
+
// except package metadata whose content participates in module resolution.
|
|
13
|
+
//
|
|
14
|
+
// 1. Retain changed Markdown and Swagger inputs.
|
|
15
|
+
// 2. Retain an in-place TypeScript edit for incremental Program update.
|
|
16
|
+
// 3. Reload package metadata, created/deleted modules, and project configs.
|
|
17
|
+
func TestExternalWatchedChangeRetainsProgram(t *testing.T) {
|
|
18
|
+
changed := fileChangeTypeChanged
|
|
19
|
+
created := fileChangeTypeCreated
|
|
20
|
+
deleted := fileChangeTypeDeleted
|
|
21
|
+
cases := []struct {
|
|
22
|
+
uri string
|
|
23
|
+
changeType *int
|
|
24
|
+
want bool
|
|
25
|
+
}{
|
|
26
|
+
{uri: "file:///project/docs/spec.md", changeType: &changed, want: true},
|
|
27
|
+
{uri: "file:///project/api/openapi.json", changeType: &changed, want: true},
|
|
28
|
+
{uri: "file:///project/package.json", changeType: &changed, want: false},
|
|
29
|
+
{uri: "file:///project/PACKAGE.JSON", changeType: &changed, want: false},
|
|
30
|
+
{uri: "file:///project/api/openapi.json", changeType: &created, want: false},
|
|
31
|
+
{uri: "file:///project/api/openapi.json", changeType: &deleted, want: false},
|
|
32
|
+
{uri: "file:///project/api/openapi.JSON", changeType: &created, want: false},
|
|
33
|
+
{uri: "file:///project/api/openapi.yaml", changeType: &deleted, want: true},
|
|
34
|
+
{uri: "file:///project/src/main.ts", changeType: &changed, want: true},
|
|
35
|
+
{uri: "file:///project/src/main.ts", changeType: &created, want: false},
|
|
36
|
+
{uri: "file:///project/src/main.ts", changeType: &deleted, want: false},
|
|
37
|
+
{uri: "file:///project/tsconfig.json", changeType: &changed, want: false},
|
|
38
|
+
{uri: "https://example.com/openapi.json", changeType: &changed, want: false},
|
|
39
|
+
{uri: "", changeType: &changed, want: false},
|
|
40
|
+
{uri: "file:///project/docs/spec.md", changeType: nil, want: false},
|
|
41
|
+
}
|
|
42
|
+
for _, tc := range cases {
|
|
43
|
+
if got := externalWatchedChangeRetainsProgram(tc.uri, tc.changeType); got != tc.want {
|
|
44
|
+
t.Fatalf(
|
|
45
|
+
"externalWatchedChangeRetainsProgram(%q, %v) = %v, want %v",
|
|
46
|
+
tc.uri,
|
|
47
|
+
tc.changeType,
|
|
48
|
+
got,
|
|
49
|
+
tc.want,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -44,6 +44,15 @@ func TestLSPHintsRefreshScheduledByEditorEvents(t *testing.T) {
|
|
|
44
44
|
`{"jsonrpc":"2.0","method":"textDocument/didSave","params":{"textDocument":{"uri":"file:///a.ts"}}}`,
|
|
45
45
|
`{"jsonrpc":"2.0","method":"workspace/didChangeConfiguration","params":{"settings":{}}}`,
|
|
46
46
|
`{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///lint.config.ts","type":2}]}}`,
|
|
47
|
+
// JSON can be a resolveJsonModule source even when no ProjectRule declares
|
|
48
|
+
// it. Content and membership changes therefore invalidate the Program-wide
|
|
49
|
+
// hint corpus just like TypeScript changes do.
|
|
50
|
+
`{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///src/data.json","type":1}]}}`,
|
|
51
|
+
`{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///src/data.json","type":2}]}}`,
|
|
52
|
+
`{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///src/data.json","type":3}]}}`,
|
|
53
|
+
// Package metadata participates in module resolution independently of
|
|
54
|
+
// ProjectRule ownership.
|
|
55
|
+
`{"jsonrpc":"2.0","method":"workspace/didChangeWatchedFiles","params":{"changes":[{"uri":"file:///package.json","type":2}]}}`,
|
|
47
56
|
// Params the proxy cannot read take the conservative branch: it cannot tell
|
|
48
57
|
// which inputs changed, so it assumes they all did — the same answer it
|
|
49
58
|
// gives the resident daemon there.
|