ttsc 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/cmd/ttscserver/main.go +79 -3
  2. package/cmd/ttscserver/run_lsp_uses_native_plugin_manifest_source_test.go +96 -16
  3. package/driver/lsp.go +4 -0
  4. package/go.mod +1 -1
  5. package/internal/lspserver/lsp_document_diagnostics_omits_cached_project_when_current_result_has_none_test.go +48 -0
  6. package/internal/lspserver/lsp_executable_project_input_change_requests_launcher_restart_test.go +98 -0
  7. package/internal/lspserver/lsp_external_watched_change_retains_program_test.go +53 -0
  8. package/internal/lspserver/lsp_hints_refresh_scheduled_by_editor_events_test.go +9 -0
  9. package/internal/lspserver/lsp_launcher_reload_directory_fingerprint_parity_windows_test.go +94 -0
  10. package/internal/lspserver/lsp_native_plugin_source.go +171 -55
  11. package/internal/lspserver/lsp_native_resident.go +110 -9
  12. package/internal/lspserver/lsp_plugin.go +6 -0
  13. package/internal/lspserver/lsp_project_context_transport_identity_test.go +43 -0
  14. package/internal/lspserver/lsp_project_diagnostics.go +235 -0
  15. package/internal/lspserver/lsp_project_diagnostics_capability_is_independent_test.go +67 -0
  16. package/internal/lspserver/lsp_project_diagnostics_program_input_overlap_refreshes_all_producers_test.go +78 -0
  17. package/internal/lspserver/lsp_project_diagnostics_refresh_accumulates_owner_freshness_test.go +65 -0
  18. package/internal/lspserver/lsp_project_diagnostics_refresh_keeps_each_producer_last_good_test.go +90 -0
  19. package/internal/lspserver/lsp_project_diagnostics_refreshes_only_input_owners_test.go +62 -0
  20. package/internal/lspserver/lsp_project_diagnostics_resident_unsupported_falls_back_test.go +123 -0
  21. package/internal/lspserver/lsp_project_diagnostics_resume_preserves_generation_test.go +52 -0
  22. package/internal/lspserver/lsp_project_diagnostics_translates_relative_client_project_test.go +90 -0
  23. package/internal/lspserver/lsp_project_input_glob_path_other.go +16 -0
  24. package/internal/lspserver/lsp_project_input_glob_path_windows.go +36 -0
  25. package/internal/lspserver/lsp_project_input_ownership_resolves_declared_spellings_test.go +95 -0
  26. package/internal/lspserver/lsp_project_input_path_key_other.go +17 -0
  27. package/internal/lspserver/lsp_project_input_path_key_windows.go +228 -0
  28. package/internal/lspserver/lsp_project_input_path_key_windows_test.go +460 -0
  29. package/internal/lspserver/lsp_project_input_registration.go +504 -0
  30. package/internal/lspserver/lsp_project_input_registration_rechecks_selection_baseline_test.go +71 -0
  31. package/internal/lspserver/lsp_project_input_registration_reconciles_rejected_pending_request_test.go +268 -0
  32. package/internal/lspserver/lsp_project_input_registration_shapes_patterns_test.go +94 -0
  33. package/internal/lspserver/lsp_project_input_watchers_anchor_missing_paths_test.go +48 -0
  34. package/internal/lspserver/lsp_project_inputs.go +1012 -0
  35. package/internal/lspserver/lsp_project_inputs_match_missing_exact_and_glob_test.go +129 -0
  36. package/internal/lspserver/lsp_project_inputs_reject_invalid_and_foreign_snapshots_test.go +110 -0
  37. package/internal/lspserver/lsp_proxy.go +591 -28
  38. package/internal/lspserver/lsp_reload_directories_compare_immediate_topology_test.go +188 -0
  39. package/internal/lspserver/lsp_reload_directories_separate_glob_territory_test.go +181 -0
  40. package/internal/lspserver/lsp_reload_file_fingerprint_preserves_raw_symlink_target_test.go +47 -0
  41. package/internal/lspserver/lsp_resident_falls_back_when_serve_unsupported_test.go +1 -1
  42. package/internal/lspserver/lsp_resident_invalidation_broadcasts_program_input_overlap_test.go +60 -0
  43. package/internal/lspserver/lsp_resident_invalidation_targets_project_input_owners_test.go +51 -0
  44. package/internal/lspserver/lsp_same_transport_entries_are_deduplicated_test.go +66 -0
  45. package/lib/compiler/internal/project/readProjectConfig.d.ts +5 -3
  46. package/lib/compiler/internal/project/readProjectConfig.js +46 -14
  47. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  48. package/lib/compiler/internal/residentCheckProcess.d.ts +46 -0
  49. package/lib/compiler/internal/residentCheckProcess.js +227 -0
  50. package/lib/compiler/internal/residentCheckProcess.js.map +1 -0
  51. package/lib/compiler/internal/resolveEmittedJavaScript.d.ts +2 -1
  52. package/lib/compiler/internal/resolveEmittedJavaScript.js +35 -18
  53. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  54. package/lib/compiler/internal/runBuild.d.ts +64 -1
  55. package/lib/compiler/internal/runBuild.js +484 -7
  56. package/lib/compiler/internal/runBuild.js.map +1 -1
  57. package/lib/compiler/internal/runSingleFileEmit.js +1 -0
  58. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  59. package/lib/flags/parser.js +13 -17
  60. package/lib/flags/parser.js.map +1 -1
  61. package/lib/flags/schema.d.ts +10 -3
  62. package/lib/flags/schema.js +99 -0
  63. package/lib/flags/schema.js.map +1 -1
  64. package/lib/internal/projectInputPathIdentity.d.ts +31 -0
  65. package/lib/internal/projectInputPathIdentity.js +276 -0
  66. package/lib/internal/projectInputPathIdentity.js.map +1 -0
  67. package/lib/internal/windowsDirectoryCaseSensitivity.d.ts +10 -0
  68. package/lib/internal/windowsDirectoryCaseSensitivity.js +34 -0
  69. package/lib/internal/windowsDirectoryCaseSensitivity.js.map +1 -0
  70. package/lib/launcher/internal/runTtsc.d.ts +17 -0
  71. package/lib/launcher/internal/runTtsc.js +126 -88
  72. package/lib/launcher/internal/runTtsc.js.map +1 -1
  73. package/lib/launcher/internal/runTtscserver.d.ts +13 -1
  74. package/lib/launcher/internal/runTtscserver.js +468 -47
  75. package/lib/launcher/internal/runTtscserver.js.map +1 -1
  76. package/lib/launcher/internal/runTtsx.js +61 -7
  77. package/lib/launcher/internal/runTtsx.js.map +1 -1
  78. package/lib/launcher/internal/runtimeHooks.d.ts +6 -5
  79. package/lib/launcher/internal/runtimeHooks.js +7 -15
  80. package/lib/launcher/internal/runtimeHooks.js.map +1 -1
  81. package/lib/launcher/internal/singleFileOutput.d.ts +16 -0
  82. package/lib/launcher/internal/singleFileOutput.js +103 -0
  83. package/lib/launcher/internal/singleFileOutput.js.map +1 -0
  84. package/lib/launcher/internal/watchTopology.d.ts +288 -2
  85. package/lib/launcher/internal/watchTopology.js +1921 -54
  86. package/lib/launcher/internal/watchTopology.js.map +1 -1
  87. package/lib/plugin/internal/descriptorProcessFailure.d.ts +32 -0
  88. package/lib/plugin/internal/descriptorProcessFailure.js +73 -0
  89. package/lib/plugin/internal/descriptorProcessFailure.js.map +1 -0
  90. package/lib/plugin/internal/loadProjectPlugins.d.ts +1 -0
  91. package/lib/plugin/internal/loadProjectPlugins.js +34 -8
  92. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  93. package/lib/structures/ITtscPluginCapabilities.d.ts +31 -0
  94. package/lib/structures/internal/ITtscProjectInputSnapshot.d.ts +29 -0
  95. package/lib/structures/internal/ITtscProjectInputSnapshot.js +3 -0
  96. package/lib/structures/internal/ITtscProjectInputSnapshot.js.map +1 -0
  97. package/lib/structures/internal/TtscSingleFileEmitOptions.d.ts +5 -0
  98. package/package.json +12 -8
  99. package/src/compiler/internal/project/readProjectConfig.ts +62 -8
  100. package/src/compiler/internal/residentCheckProcess.ts +279 -0
  101. package/src/compiler/internal/resolveEmittedJavaScript.ts +43 -21
  102. package/src/compiler/internal/runBuild.ts +728 -12
  103. package/src/compiler/internal/runSingleFileEmit.ts +1 -0
  104. package/src/flags/parser.ts +17 -17
  105. package/src/flags/schema.ts +112 -3
  106. package/src/internal/projectInputPathIdentity.ts +377 -0
  107. package/src/internal/windowsDirectoryCaseSensitivity.ts +33 -0
  108. package/src/launcher/internal/runTtsc.ts +148 -117
  109. package/src/launcher/internal/runTtscserver.ts +633 -56
  110. package/src/launcher/internal/runTtsx.ts +87 -10
  111. package/src/launcher/internal/runtimeHooks.ts +16 -16
  112. package/src/launcher/internal/singleFileOutput.ts +133 -0
  113. package/src/launcher/internal/watchTopology.ts +2494 -74
  114. package/src/plugin/internal/descriptorProcessFailure.ts +100 -0
  115. package/src/plugin/internal/loadProjectPlugins.ts +49 -8
  116. package/src/structures/ITtscPluginCapabilities.ts +34 -0
  117. package/src/structures/internal/ITtscProjectInputSnapshot.ts +29 -0
  118. package/src/structures/internal/TtscSingleFileEmitOptions.ts +5 -0
  119. package/tools/shim_audit/baseline.json +0 -9
@@ -0,0 +1,123 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "bytes"
5
+ "os"
6
+ "os/exec"
7
+ "path/filepath"
8
+ "runtime"
9
+ "strings"
10
+ "testing"
11
+ )
12
+
13
+ // TestLSPProjectDiagnosticsResidentUnsupportedFallsBack verifies a staged
14
+ // sidecar can serve its advertised direct command through an older daemon.
15
+ //
16
+ // `lsp-serve` and its accepted request verbs can ship at different times. Once
17
+ // a resident answers another verb, rejecting `lsp-project-diagnostics` is not a
18
+ // transport failure and used to suppress the working one-shot command.
19
+ //
20
+ // 1. Build a sidecar whose daemon rejects only project diagnostics.
21
+ // 2. Advertise the direct project-diagnostic capability.
22
+ // 3. Request a publication and assert the one-shot command answers it.
23
+ // 4. Assert both resident rejection and direct fallback occurred once.
24
+ func TestLSPProjectDiagnosticsResidentUnsupportedFallsBack(t *testing.T) {
25
+ dir := t.TempDir()
26
+ sidecarSource := filepath.Join(dir, "sidecar.go")
27
+ if err := os.WriteFile(
28
+ sidecarSource,
29
+ []byte(projectDiagnosticsFallbackSidecarSource),
30
+ 0644,
31
+ ); err != nil {
32
+ t.Fatal(err)
33
+ }
34
+ binary := filepath.Join(dir, "sidecar")
35
+ if runtime.GOOS == "windows" {
36
+ binary += ".exe"
37
+ }
38
+ build := exec.Command("go", "build", "-o", binary, sidecarSource)
39
+ if output, err := build.CombinedOutput(); err != nil {
40
+ t.Fatalf("go build sidecar failed: %v\n%s", err, output)
41
+ }
42
+ logPath := filepath.Join(dir, "calls.log")
43
+ t.Setenv("TTSC_PROJECT_DIAGNOSTICS_FALLBACK_LOG", logPath)
44
+ plugin := NativeLSPPluginEntry{
45
+ Binary: binary,
46
+ Name: "@ttsc/staged",
47
+ ProjectDiagnostics: true,
48
+ }
49
+ source := &NativePluginSource{
50
+ cwd: dir,
51
+ err: &bytes.Buffer{},
52
+ plugins: []NativeLSPPluginEntry{plugin},
53
+ pluginsJSON: "[]",
54
+ tsconfig: filepath.Join(dir, "tsconfig.json"),
55
+ }
56
+ t.Cleanup(source.shutdownResidents)
57
+
58
+ got := source.ProjectDiagnostics()
59
+
60
+ if got == nil || got.URI != "file:///project/tsconfig.json" ||
61
+ len(got.Diagnostics) != 1 ||
62
+ got.Diagnostics[0].Code != "direct" {
63
+ t.Fatalf("direct fallback publication = %#v", got)
64
+ }
65
+ calls, err := os.ReadFile(logPath)
66
+ if err != nil {
67
+ t.Fatal(err)
68
+ }
69
+ log := string(calls)
70
+ if strings.Count(log, "resident lsp-project-diagnostics") != 1 {
71
+ t.Fatalf("resident attempt count is wrong:\n%s", log)
72
+ }
73
+ if strings.Count(log, "direct lsp-project-diagnostics") != 1 {
74
+ t.Fatalf("direct fallback count is wrong:\n%s", log)
75
+ }
76
+ }
77
+
78
+ const projectDiagnosticsFallbackSidecarSource = `package main
79
+
80
+ import (
81
+ "bufio"
82
+ "encoding/json"
83
+ "fmt"
84
+ "os"
85
+ )
86
+
87
+ type request struct {
88
+ Verb string ` + "`json:\"verb\"`" + `
89
+ }
90
+
91
+ func logCall(message string) {
92
+ path := os.Getenv("TTSC_PROJECT_DIAGNOSTICS_FALLBACK_LOG")
93
+ file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
94
+ if err == nil {
95
+ _, _ = fmt.Fprintln(file, message)
96
+ _ = file.Close()
97
+ }
98
+ }
99
+
100
+ func main() {
101
+ if len(os.Args) < 2 {
102
+ os.Exit(2)
103
+ }
104
+ if os.Args[1] == "lsp-serve" {
105
+ scanner := bufio.NewScanner(os.Stdin)
106
+ for scanner.Scan() {
107
+ var req request
108
+ if json.Unmarshal(scanner.Bytes(), &req) != nil {
109
+ os.Exit(2)
110
+ }
111
+ logCall("resident " + req.Verb)
112
+ fmt.Println(` + "`" + `{"result":null,"code":2}` + "`" + `)
113
+ }
114
+ return
115
+ }
116
+ if os.Args[1] == "lsp-project-diagnostics" {
117
+ logCall("direct lsp-project-diagnostics")
118
+ fmt.Println(` + "`" + `{"uri":"file:///project/tsconfig.json","diagnostics":[{"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},"code":"direct","message":"direct"}]}` + "`" + `)
119
+ return
120
+ }
121
+ os.Exit(2)
122
+ }
123
+ `
@@ -0,0 +1,52 @@
1
+ package lspserver
2
+
3
+ import "testing"
4
+
5
+ type projectDiagnosticsResumeSource struct {
6
+ NullPluginSource
7
+ }
8
+
9
+ func (projectDiagnosticsResumeSource) ProjectDiagnostics() *LSPProjectDiagnostics {
10
+ return nil
11
+ }
12
+
13
+ // TestResumePendingProjectDiagnosticRefreshDoesNotReviveCompletedWork verifies
14
+ // a save or close advances and rearms pending work atomically, while a refresh
15
+ // completed before that transition remains completed.
16
+ func TestResumePendingProjectDiagnosticRefreshDoesNotReviveCompletedWork(
17
+ t *testing.T,
18
+ ) {
19
+ proxy := NewProxy(ProxyOptions{
20
+ Source: projectDiagnosticsResumeSource{},
21
+ })
22
+ defer proxy.stopProjectDiagnosticRefresh()
23
+
24
+ proxy.scheduleProjectDiagnosticRefresh(projectDiagnosticOwnerScope{all: true})
25
+ proxy.diagnosticsMu.Lock()
26
+ before := proxy.projectDiagnosticGeneration
27
+ proxy.diagnosticsMu.Unlock()
28
+
29
+ proxy.resumePendingProjectDiagnosticRefresh()
30
+
31
+ proxy.diagnosticsMu.Lock()
32
+ after := proxy.projectDiagnosticGeneration
33
+ proxy.diagnosticsMu.Unlock()
34
+ if after != before+1 {
35
+ t.Fatalf("resume advanced generation from %d to %d", before, after)
36
+ }
37
+ proxy.projectRefreshMu.Lock()
38
+ pending := proxy.projectDiagnosticRefreshPending
39
+ generation := proxy.pendingProjectDiagnosticGeneration
40
+ proxy.projectRefreshMu.Unlock()
41
+ if !pending {
42
+ t.Fatal("resume cleared the pending refresh")
43
+ }
44
+ proxy.completePendingProjectDiagnosticRefresh(generation)
45
+ proxy.resumePendingProjectDiagnosticRefresh()
46
+ proxy.diagnosticsMu.Lock()
47
+ final := proxy.projectDiagnosticGeneration
48
+ proxy.diagnosticsMu.Unlock()
49
+ if final != after {
50
+ t.Fatalf("completed refresh was revived at generation %d", final)
51
+ }
52
+ }
@@ -0,0 +1,90 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+ )
8
+
9
+ // TestLSPProjectDiagnosticsTranslatesRelativeClientProject verifies a project
10
+ // URI is restated in the client's spelling however the client named it.
11
+ //
12
+ // The editor is not required to pass an absolute `--tsconfig`; naming the
13
+ // project relative to the working directory it also passed is the ordinary
14
+ // spelling, and every ttsc CLI accepts it. The translator, however, needs an
15
+ // absolute path both to compare against the producer's URI and to build a URI
16
+ // from, so a relative spelling used to end the translation and let the sidecar's
17
+ // own spelling reach the editor unchanged — the precise failure this whole
18
+ // translation exists to prevent, surviving in the common case.
19
+ //
20
+ // A client that named no directory either is not a case to decline: this host
21
+ // was started from that directory and inherited it, so its own working
22
+ // directory is the same anchor reached another way.
23
+ //
24
+ // 1. Resolve a relative project against the directory the client named.
25
+ // 2. Resolve one against the directory the host inherited.
26
+ // 3. Leave an absolute one exactly as the previous behavior did.
27
+ func TestLSPProjectDiagnosticsTranslatesRelativeClientProject(t *testing.T) {
28
+ root := t.TempDir()
29
+ if err := os.WriteFile(
30
+ filepath.Join(root, "tsconfig.json"),
31
+ []byte("{}"),
32
+ 0o644,
33
+ ); err != nil {
34
+ t.Fatalf("write tsconfig: %v", err)
35
+ }
36
+ if err := os.Mkdir(filepath.Join(root, "src"), 0o755); err != nil {
37
+ t.Fatalf("make src: %v", err)
38
+ }
39
+
40
+ // Spelled through a directory and back out, so it addresses the same file the
41
+ // client does without being the same string.
42
+ detour := func(directory string) string {
43
+ return projectInputFileURI(
44
+ filepath.Join(directory, "src") +
45
+ string(os.PathSeparator) + ".." +
46
+ string(os.PathSeparator) + "tsconfig.json",
47
+ )
48
+ }
49
+ named := detour(root)
50
+ expected := projectInputFileURI(filepath.Join(root, "tsconfig.json"))
51
+ if named == expected {
52
+ t.Fatalf("the producer URI must differ from the client's to be a test")
53
+ }
54
+
55
+ source := &NativePluginSource{
56
+ clientTsconfig: "tsconfig.json",
57
+ clientCwd: root,
58
+ }
59
+ if got := source.clientProjectURI(named); got != expected {
60
+ t.Fatalf("relative project not translated: got %q, want %q", got, expected)
61
+ }
62
+
63
+ working, err := os.Getwd()
64
+ if err != nil {
65
+ t.Fatalf("working directory: %v", err)
66
+ }
67
+ inherited := &NativePluginSource{clientTsconfig: "tsconfig.json"}
68
+ inheritedWant := projectInputFileURI(filepath.Join(working, "tsconfig.json"))
69
+ if got := inherited.clientProjectURI(detour(working)); got != inheritedWant {
70
+ t.Fatalf(
71
+ "inherited directory not used: got %q, want %q",
72
+ got,
73
+ inheritedWant,
74
+ )
75
+ }
76
+
77
+ absolute := &NativePluginSource{
78
+ clientTsconfig: filepath.Join(root, "tsconfig.json"),
79
+ }
80
+ if got := absolute.clientProjectURI(named); got != expected {
81
+ t.Fatalf("absolute project no longer translated: got %q, want %q", got, expected)
82
+ }
83
+
84
+ // Nothing to translate to, so the producer keeps its own spelling rather than
85
+ // being addressed at a project the client never named.
86
+ unnamed := &NativePluginSource{}
87
+ if got := unnamed.clientProjectURI(named); got != named {
88
+ t.Fatalf("unnamed project was rewritten: got %q, want %q", got, named)
89
+ }
90
+ }
@@ -0,0 +1,16 @@
1
+ //go:build !windows
2
+
3
+ package lspserver
4
+
5
+ import "strings"
6
+
7
+ func projectInputGlobMatchesCandidate(
8
+ pattern string,
9
+ candidate string,
10
+ ) bool {
11
+ return matchProjectInputGlob(
12
+ strings.Split(projectInputPathKey(realProjectInputPath(pattern)), "/"),
13
+ strings.Split(projectInputPathKey(realProjectInputPath(candidate)), "/"),
14
+ nil,
15
+ )
16
+ }
@@ -0,0 +1,36 @@
1
+ //go:build windows
2
+
3
+ package lspserver
4
+
5
+ import (
6
+ "path/filepath"
7
+ "strings"
8
+ )
9
+
10
+ func projectInputGlobMatchesCandidate(
11
+ pattern string,
12
+ candidate string,
13
+ ) bool {
14
+ resolvedPattern := resolveWindowsProjectInputPath(pattern)
15
+ resolvedCandidate := resolveWindowsProjectInputPath(candidate)
16
+ if !resolvedPattern.found || !resolvedCandidate.found {
17
+ return false
18
+ }
19
+ patternVolume := strings.ToLower(
20
+ filepath.ToSlash(filepath.VolumeName(resolvedPattern.physical)),
21
+ )
22
+ candidateVolume := strings.ToLower(
23
+ filepath.ToSlash(filepath.VolumeName(resolvedCandidate.physical)),
24
+ )
25
+ if patternVolume != candidateVolume {
26
+ return false
27
+ }
28
+ patternSegments, _ := windowsProjectInputSegments(resolvedPattern)
29
+ candidateSegments, candidateSensitivities :=
30
+ windowsProjectInputSegments(resolvedCandidate)
31
+ return matchProjectInputGlob(
32
+ patternSegments,
33
+ candidateSegments,
34
+ candidateSensitivities,
35
+ )
36
+ }
@@ -0,0 +1,95 @@
1
+ package lspserver
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+ )
8
+
9
+ // TestLSPProjectInputOwnershipResolvesDeclaredSpellings verifies a declaration
10
+ // is matched against an event through one filesystem identity.
11
+ //
12
+ // The candidate always arrives from an editor URI and is resolved physically,
13
+ // so comparing it against a declaration that was only cleaned lexically can
14
+ // never match wherever the two spellings differ. A Windows short (8.3)
15
+ // component, which the system temporary directory routinely carries, and a
16
+ // symlinked ancestor such as macOS `/var` both produce exactly that, and the
17
+ // contributor that declared the file then receives no refresh at all.
18
+ //
19
+ // 1. Declare an exact file and a glob through a directory alias.
20
+ // 2. Ask for the owners of the same paths spelled physically.
21
+ // 3. Assert the declaring producer owns both.
22
+ // 4. Assert two adjacent paths one property away are owned by nobody.
23
+ func TestLSPProjectInputOwnershipResolvesDeclaredSpellings(t *testing.T) {
24
+ physical := t.TempDir()
25
+ alias := filepath.Join(t.TempDir(), "project")
26
+ if err := os.Symlink(physical, alias); err != nil {
27
+ t.Skipf("filesystem cannot express a directory alias: %v", err)
28
+ }
29
+ for _, directory := range []string{
30
+ filepath.Join(physical, "docs"),
31
+ filepath.Join(physical, "api"),
32
+ } {
33
+ if err := os.MkdirAll(directory, 0o755); err != nil {
34
+ t.Fatal(err)
35
+ }
36
+ }
37
+ exact := filepath.Join(physical, "docs", "spec.md")
38
+ matched := filepath.Join(physical, "api", "openapi.json")
39
+ for _, location := range []string{exact, matched} {
40
+ if err := os.WriteFile(location, []byte("{}\n"), 0o644); err != nil {
41
+ t.Fatal(err)
42
+ }
43
+ }
44
+
45
+ plugin := NativeLSPPluginEntry{
46
+ Binary: "ttsc-no-such-alias-sidecar",
47
+ Name: "@ttsc/alias",
48
+ ProjectDiagnostics: true,
49
+ ProjectInputs: true,
50
+ }
51
+ source := &NativePluginSource{plugins: []NativeLSPPluginEntry{plugin}}
52
+ source.storeProjectInputs(plugin, 1, LSPProjectInputSnapshot{
53
+ Root: filepath.ToSlash(alias),
54
+ Files: []string{filepath.ToSlash(filepath.Join(alias, "docs", "spec.md"))},
55
+ Globs: []string{
56
+ filepath.ToSlash(filepath.Join(alias, "api", "**", "*.json")),
57
+ },
58
+ })
59
+
60
+ for _, owned := range []struct {
61
+ label string
62
+ location string
63
+ }{
64
+ {label: "exact declaration", location: exact},
65
+ {label: "glob member", location: matched},
66
+ } {
67
+ owners := source.ProjectInputOwnersForURI(testFileURI(owned.location))
68
+ if len(owners) != 1 || owners[0] != pluginKey(plugin) {
69
+ t.Fatalf("%s owners = %#v", owned.label, owners)
70
+ }
71
+ }
72
+
73
+ // Resolving both sides collapses distinct spellings onto one physical path,
74
+ // so over-matching is the failure this lane can produce. Each negative sits
75
+ // one property away from an owned path: the same directory with another
76
+ // extension, and the same extension outside every declaration.
77
+ for _, unowned := range []struct {
78
+ label string
79
+ location string
80
+ }{
81
+ {
82
+ label: "a sibling the glob does not select",
83
+ location: filepath.Join(physical, "api", "openapi.txt"),
84
+ },
85
+ {
86
+ label: "a path outside every declaration",
87
+ location: filepath.Join(physical, "docs", "other.json"),
88
+ },
89
+ } {
90
+ owners := source.ProjectInputOwnersForURI(testFileURI(unowned.location))
91
+ if len(owners) != 0 {
92
+ t.Fatalf("%s owners = %#v", unowned.label, owners)
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,17 @@
1
+ //go:build !windows
2
+
3
+ package lspserver
4
+
5
+ import "path/filepath"
6
+
7
+ func physicalProjectInputPath(location string) (string, error) {
8
+ return filepath.EvalSymlinks(location)
9
+ }
10
+
11
+ func projectInputPhysicalPathKey(location string) string {
12
+ return filepath.ToSlash(realProjectInputPath(location))
13
+ }
14
+
15
+ func projectInputPathKey(location string) string {
16
+ return filepath.ToSlash(filepath.Clean(filepath.FromSlash(location)))
17
+ }
@@ -0,0 +1,228 @@
1
+ //go:build windows
2
+
3
+ package lspserver
4
+
5
+ import (
6
+ "path/filepath"
7
+ "strings"
8
+ "unsafe"
9
+
10
+ "golang.org/x/sys/windows"
11
+ )
12
+
13
+ type windowsProjectInputPath struct {
14
+ found bool
15
+ missing []string
16
+ physical string
17
+ }
18
+
19
+ // projectInputPathKey resolves existing spelling through the filesystem and
20
+ // applies case folding to each segment according to the directory that owns
21
+ // that name. Windows case semantics are per directory, not per volume or OS.
22
+ func projectInputPathKey(location string) string {
23
+ normalized := filepath.Clean(projectInputFilesystemPath(location))
24
+ if !filepath.IsAbs(normalized) {
25
+ return strings.ToLower(filepath.ToSlash(normalized))
26
+ }
27
+ volume := filepath.VolumeName(normalized)
28
+ segments := splitWindowsProjectInputSegments(
29
+ strings.TrimPrefix(normalized, volume),
30
+ )
31
+ current := windowsProjectInputVolumeRoot(volume)
32
+ sensitive := false
33
+ for index, segment := range segments {
34
+ if currentSensitivity, ok :=
35
+ tryQueryProjectInputDirectoryCaseSensitivity(current); ok {
36
+ sensitive = currentSensitivity
37
+ }
38
+ if !sensitive {
39
+ segments[index] = strings.ToLower(segments[index])
40
+ }
41
+ current = filepath.Join(current, segment)
42
+ }
43
+ return windowsProjectInputKey(normalized, segments)
44
+ }
45
+
46
+ func resolveWindowsProjectInputPath(location string) windowsProjectInputPath {
47
+ normalized := filepath.Clean(projectInputFilesystemPath(location))
48
+ if !filepath.IsAbs(normalized) {
49
+ return windowsProjectInputPath{physical: normalized}
50
+ }
51
+ probe := normalized
52
+ missing := []string{}
53
+ for {
54
+ physical, err := physicalProjectInputPath(probe)
55
+ if err == nil {
56
+ return windowsProjectInputPath{
57
+ found: true,
58
+ missing: reverseWindowsProjectInputSegments(missing),
59
+ physical: filepath.Clean(physical),
60
+ }
61
+ }
62
+ parent := filepath.Dir(probe)
63
+ if parent == probe {
64
+ return windowsProjectInputPath{physical: normalized}
65
+ }
66
+ missing = append(missing, filepath.Base(probe))
67
+ probe = parent
68
+ }
69
+ }
70
+
71
+ func physicalProjectInputPath(location string) (string, error) {
72
+ pointer, err := windows.UTF16PtrFromString(location)
73
+ if err != nil {
74
+ return "", err
75
+ }
76
+ handle, err := windows.CreateFile(
77
+ pointer,
78
+ windows.FILE_READ_ATTRIBUTES,
79
+ windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
80
+ nil,
81
+ windows.OPEN_EXISTING,
82
+ windows.FILE_FLAG_BACKUP_SEMANTICS,
83
+ 0,
84
+ )
85
+ if err != nil {
86
+ return "", err
87
+ }
88
+ defer windows.CloseHandle(handle)
89
+
90
+ buffer := make([]uint16, windows.MAX_PATH)
91
+ for {
92
+ length, err := windows.GetFinalPathNameByHandle(
93
+ handle,
94
+ &buffer[0],
95
+ uint32(len(buffer)),
96
+ 0,
97
+ )
98
+ if err != nil {
99
+ return "", err
100
+ }
101
+ if length < uint32(len(buffer)) {
102
+ return filepath.Clean(
103
+ projectInputFilesystemPath(
104
+ windows.UTF16ToString(buffer[:length]),
105
+ ),
106
+ ), nil
107
+ }
108
+ buffer = make([]uint16, length+1)
109
+ }
110
+ }
111
+
112
+ func projectInputPhysicalPathKey(location string) string {
113
+ resolved := resolveWindowsProjectInputPath(location)
114
+ volume := filepath.VolumeName(resolved.physical)
115
+ segments := splitWindowsProjectInputSegments(
116
+ strings.TrimPrefix(resolved.physical, volume),
117
+ )
118
+ if len(resolved.missing) != 0 {
119
+ missing := append([]string{}, resolved.missing...)
120
+ if !queryProjectInputDirectoryCaseSensitivity(resolved.physical) {
121
+ for index := range missing {
122
+ missing[index] = strings.ToLower(missing[index])
123
+ }
124
+ }
125
+ segments = append(segments, missing...)
126
+ }
127
+ prefix := filepath.ToSlash(volume)
128
+ if len(segments) == 0 {
129
+ return prefix + "/"
130
+ }
131
+ return prefix + "/" + strings.Join(segments, "/")
132
+ }
133
+
134
+ func windowsProjectInputSegments(
135
+ resolved windowsProjectInputPath,
136
+ ) ([]string, []bool) {
137
+ volume := filepath.VolumeName(resolved.physical)
138
+ existing := splitWindowsProjectInputSegments(
139
+ strings.TrimPrefix(resolved.physical, volume),
140
+ )
141
+ segments := append(append([]string{}, existing...), resolved.missing...)
142
+ sensitivities := make([]bool, 0, len(segments))
143
+ current := windowsProjectInputVolumeRoot(volume)
144
+ for _, segment := range existing {
145
+ sensitivities = append(
146
+ sensitivities,
147
+ queryProjectInputDirectoryCaseSensitivity(current),
148
+ )
149
+ current = filepath.Join(current, segment)
150
+ }
151
+ if len(resolved.missing) != 0 {
152
+ missingSensitivity := queryProjectInputDirectoryCaseSensitivity(
153
+ resolved.physical,
154
+ )
155
+ for range resolved.missing {
156
+ sensitivities = append(sensitivities, missingSensitivity)
157
+ }
158
+ }
159
+ return segments, sensitivities
160
+ }
161
+
162
+ func windowsProjectInputKey(location string, segments []string) string {
163
+ volume := filepath.VolumeName(location)
164
+ prefix := strings.ToLower(filepath.ToSlash(volume))
165
+ if len(segments) == 0 {
166
+ return prefix + "/"
167
+ }
168
+ return prefix + "/" + strings.Join(segments, "/")
169
+ }
170
+
171
+ func windowsProjectInputVolumeRoot(volume string) string {
172
+ if strings.HasSuffix(volume, `\`) || strings.HasSuffix(volume, "/") {
173
+ return volume
174
+ }
175
+ return volume + `\`
176
+ }
177
+
178
+ func splitWindowsProjectInputSegments(location string) []string {
179
+ return strings.FieldsFunc(location, func(character rune) bool {
180
+ return character == '/' || character == '\\'
181
+ })
182
+ }
183
+
184
+ func reverseWindowsProjectInputSegments(segments []string) []string {
185
+ reversed := make([]string, len(segments))
186
+ for index := range segments {
187
+ reversed[len(segments)-index-1] = segments[index]
188
+ }
189
+ return reversed
190
+ }
191
+
192
+ func queryProjectInputDirectoryCaseSensitivity(directory string) bool {
193
+ sensitive, _ := tryQueryProjectInputDirectoryCaseSensitivity(directory)
194
+ return sensitive
195
+ }
196
+
197
+ func tryQueryProjectInputDirectoryCaseSensitivity(
198
+ directory string,
199
+ ) (bool, bool) {
200
+ pointer, err := windows.UTF16PtrFromString(directory)
201
+ if err != nil {
202
+ return false, false
203
+ }
204
+ handle, err := windows.CreateFile(
205
+ pointer,
206
+ windows.FILE_READ_ATTRIBUTES,
207
+ windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
208
+ nil,
209
+ windows.OPEN_EXISTING,
210
+ windows.FILE_FLAG_BACKUP_SEMANTICS,
211
+ 0,
212
+ )
213
+ if err != nil {
214
+ return false, false
215
+ }
216
+ defer windows.CloseHandle(handle)
217
+
218
+ var flags uint32
219
+ if err := windows.GetFileInformationByHandleEx(
220
+ handle,
221
+ windows.FileCaseSensitiveInfo,
222
+ (*byte)(unsafe.Pointer(&flags)),
223
+ uint32(unsafe.Sizeof(flags)),
224
+ ); err != nil {
225
+ return false, false
226
+ }
227
+ return flags&windows.FILE_CS_FLAG_CASE_SENSITIVE_DIR != 0, true
228
+ }