ttsc 0.19.3 → 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 +276 -100
- 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_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 +208 -0
- package/internal/graph/graph.go +143 -5
- package/internal/graph/line_model.go +122 -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 +100 -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
|
@@ -12,6 +12,7 @@ import (
|
|
|
12
12
|
"path/filepath"
|
|
13
13
|
"strings"
|
|
14
14
|
"sync"
|
|
15
|
+
"sync/atomic"
|
|
15
16
|
"unicode/utf16"
|
|
16
17
|
"unicode/utf8"
|
|
17
18
|
)
|
|
@@ -36,8 +37,29 @@ const (
|
|
|
36
37
|
methodFormatting = "textDocument/formatting"
|
|
37
38
|
methodDocumentSymbol = "textDocument/documentSymbol"
|
|
38
39
|
methodReferences = "textDocument/references"
|
|
40
|
+
methodCompletion = "textDocument/completion"
|
|
41
|
+
methodCompletionResolve = "completionItem/resolve"
|
|
42
|
+
methodExit = "exit"
|
|
43
|
+
|
|
44
|
+
// methodDidChangeWatchedFiles is the only notification an editor sends for a
|
|
45
|
+
// file it does not have open: a tsconfig edit, a generated file, a branch
|
|
46
|
+
// switch. The repository's own VS Code client registers a watcher for
|
|
47
|
+
// `**/{tsconfig,jsconfig}*.json`, which its documentSelector excludes, so
|
|
48
|
+
// those edits can reach ttsc through no other notification.
|
|
49
|
+
methodDidChangeWatchedFiles = "workspace/didChangeWatchedFiles"
|
|
50
|
+
// methodDidChangeConfiguration is the editor's own settings changing. The
|
|
51
|
+
// proxy forwards it untouched and reads it only as a signal that a plugin's
|
|
52
|
+
// completion corpus may have gone stale, since a rule can be enabled there.
|
|
53
|
+
methodDidChangeConfiguration = "workspace/didChangeConfiguration"
|
|
54
|
+
// methodLogMessage carries server-side notices to the editor's output
|
|
55
|
+
// channel. The proxy uses it for state the user has to act on and no other
|
|
56
|
+
// LSP message can express.
|
|
57
|
+
methodLogMessage = "window/logMessage"
|
|
39
58
|
)
|
|
40
59
|
|
|
60
|
+
// lspMessageTypeInfo is the LSP MessageType for an informational log message.
|
|
61
|
+
const lspMessageTypeInfo = 3
|
|
62
|
+
|
|
41
63
|
// formatDocumentCommand is the ttsc-owned workspace command that the lint
|
|
42
64
|
// sidecar advertises for whole-document formatting. The textDocument/formatting
|
|
43
65
|
// handler routes the cached editor buffer through this command so formatOnSave
|
|
@@ -102,8 +124,17 @@ type Proxy struct {
|
|
|
102
124
|
upstreamWriteMu sync.Mutex // serializes writes to upstreamIn
|
|
103
125
|
asyncErrCh chan error
|
|
104
126
|
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
// editorExit records that the editor sent the LSP `exit` notification.
|
|
128
|
+
// RunLSPServer reads it to decide whether a failed upstream process is a
|
|
129
|
+
// fault to report or the expected end of an editor-requested shutdown; see
|
|
130
|
+
// editorRequestedExit.
|
|
131
|
+
editorExit atomic.Bool
|
|
132
|
+
|
|
133
|
+
pendingMu sync.Mutex
|
|
134
|
+
pendingActions map[string]pendingCodeActionRequest
|
|
135
|
+
// pendingCompletions holds the plugin items computed for a forwarded
|
|
136
|
+
// completion request, keyed by request id, until upstream answers it.
|
|
137
|
+
pendingCompletions map[string]pendingCompletionRequest
|
|
107
138
|
pendingAugmentingActions map[string]struct{}
|
|
108
139
|
pendingLocalActions map[string]struct{}
|
|
109
140
|
pendingCommands map[string]struct{}
|
|
@@ -113,6 +144,15 @@ type Proxy struct {
|
|
|
113
144
|
upstreamCodeActionProvider bool
|
|
114
145
|
upstreamDocumentSymbolProvider bool
|
|
115
146
|
upstreamReferencesProvider bool
|
|
147
|
+
// initializeAnswered records that the editor has received the augmented
|
|
148
|
+
// capabilities, which is what makes a later trigger character "late".
|
|
149
|
+
initializeAnswered bool
|
|
150
|
+
// advertisedCompletionTriggers is the trigger set the editor was told about:
|
|
151
|
+
// tsgo's own characters plus whatever the corpus contributed at that moment.
|
|
152
|
+
// A refresh that produces a trigger outside this set cannot reach the editor
|
|
153
|
+
// without a restart, so the proxy says so once per character.
|
|
154
|
+
advertisedCompletionTriggers map[string]struct{}
|
|
155
|
+
reportedCompletionTriggers map[string]struct{}
|
|
116
156
|
|
|
117
157
|
diagnosticsMu sync.Mutex
|
|
118
158
|
upstreamDiagnostics map[string]cachedDiagnostics
|
|
@@ -161,7 +201,7 @@ func NewProxy(opts ProxyOptions) *Proxy {
|
|
|
161
201
|
if source == nil {
|
|
162
202
|
source = NullPluginSource{}
|
|
163
203
|
}
|
|
164
|
-
|
|
204
|
+
proxy := &Proxy{
|
|
165
205
|
editorIn: opts.EditorIn,
|
|
166
206
|
editorOut: opts.EditorOut,
|
|
167
207
|
upstreamIn: opts.UpstreamIn,
|
|
@@ -187,6 +227,16 @@ func NewProxy(opts ProxyOptions) *Proxy {
|
|
|
187
227
|
dirtyVersions: make(map[string]*int),
|
|
188
228
|
documentText: make(map[string]string),
|
|
189
229
|
}
|
|
230
|
+
// Optional-interface assertion for the same reason pluginCompletionHints uses
|
|
231
|
+
// one: a source that publishes no corpus, NullPluginSource included, simply
|
|
232
|
+
// never reports one changing.
|
|
233
|
+
type completionHintObserverSource interface {
|
|
234
|
+
SetCompletionHintsObserver(func())
|
|
235
|
+
}
|
|
236
|
+
if observed, ok := source.(completionHintObserverSource); ok {
|
|
237
|
+
observed.SetCompletionHintsObserver(proxy.completionHintsRefreshed)
|
|
238
|
+
}
|
|
239
|
+
return proxy
|
|
190
240
|
}
|
|
191
241
|
|
|
192
242
|
// Run drives both pump goroutines until they return. Pumps return when
|
|
@@ -255,7 +305,7 @@ func (p *Proxy) pumpEditorToUpstream(_ context.Context) error {
|
|
|
255
305
|
if handled {
|
|
256
306
|
continue
|
|
257
307
|
}
|
|
258
|
-
if err := p.writeUpstreamFrame(body); err != nil {
|
|
308
|
+
if err := p.writeUpstreamFrame(constrainInitializePositionEncoding(env, body)); err != nil {
|
|
259
309
|
return err
|
|
260
310
|
}
|
|
261
311
|
}
|
|
@@ -289,11 +339,27 @@ func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
|
|
|
289
339
|
case methodDidSave:
|
|
290
340
|
if env.IsNotification() {
|
|
291
341
|
p.invalidateSymbolProvider()
|
|
342
|
+
p.invalidateResidentPlugins(env)
|
|
343
|
+
p.refreshPluginCompletionHints()
|
|
292
344
|
p.publishPluginDiagnosticsForDocumentNotification(env)
|
|
293
345
|
}
|
|
346
|
+
case methodDidChangeConfiguration:
|
|
347
|
+
if env.IsNotification() {
|
|
348
|
+
// A rule can be enabled from editor settings, so the corpus may have gone
|
|
349
|
+
// stale. Watched-file changes schedule the same refresh, but from inside
|
|
350
|
+
// invalidateForWatchedFileChanges, which knows whether anything actually
|
|
351
|
+
// changed.
|
|
352
|
+
p.refreshPluginCompletionHints()
|
|
353
|
+
}
|
|
294
354
|
case methodDidChange:
|
|
295
355
|
if env.IsNotification() {
|
|
296
356
|
p.invalidateSymbolProvider()
|
|
357
|
+
// No resident invalidation on didChange: the buffer is dirty but disk is
|
|
358
|
+
// unchanged, so the warm Program (built from disk) stays valid, and plugin
|
|
359
|
+
// diagnostics and code actions are suppressed while dirty anyway. didSave
|
|
360
|
+
// updates the Program incrementally once the edit reaches disk. No corpus
|
|
361
|
+
// refresh either, for the same reason plus a sharper one: a refresh spawns
|
|
362
|
+
// a sidecar per plugin, and completion is on the live-buffer hot path.
|
|
297
363
|
p.cacheDidChangeText(env)
|
|
298
364
|
if err := p.markDocumentDirty(env); err != nil {
|
|
299
365
|
return false, err
|
|
@@ -304,6 +370,15 @@ func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
|
|
|
304
370
|
p.evictDocumentText(env)
|
|
305
371
|
p.clearDocumentDiagnostics(env)
|
|
306
372
|
}
|
|
373
|
+
case methodDidChangeWatchedFiles:
|
|
374
|
+
if env.IsNotification() {
|
|
375
|
+
// Refresh before the notification reaches tsgo, so no read that races
|
|
376
|
+
// this frame can still be answered from the pre-change snapshot. The
|
|
377
|
+
// notification itself keeps flowing upstream.
|
|
378
|
+
if err := p.invalidateForWatchedFileChanges(env); err != nil {
|
|
379
|
+
return false, err
|
|
380
|
+
}
|
|
381
|
+
}
|
|
307
382
|
case methodExecuteCommand:
|
|
308
383
|
if env.IsRequest() {
|
|
309
384
|
return p.tryExecuteCommand(env)
|
|
@@ -324,6 +399,14 @@ func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
|
|
|
324
399
|
if env.IsRequest() {
|
|
325
400
|
return p.handleReferencesRequest(env)
|
|
326
401
|
}
|
|
402
|
+
case methodCompletion:
|
|
403
|
+
return p.handleCompletionRequest(env)
|
|
404
|
+
case methodCompletionResolve:
|
|
405
|
+
return p.handleCompletionResolveRequest(env)
|
|
406
|
+
case methodExit:
|
|
407
|
+
if env.IsNotification() {
|
|
408
|
+
p.editorExit.Store(true)
|
|
409
|
+
}
|
|
327
410
|
case methodCancelRequest:
|
|
328
411
|
// $/cancelRequest names an in-flight id the editor has given up on.
|
|
329
412
|
// The proxy drops any pending codeAction entry for that id so the
|
|
@@ -334,6 +417,121 @@ func (p *Proxy) handleEditorEnvelope(env Envelope, body []byte) (bool, error) {
|
|
|
334
417
|
return false, nil
|
|
335
418
|
}
|
|
336
419
|
|
|
420
|
+
// positionEncodingUTF16 is the one LSP PositionEncodingKind ttscserver speaks.
|
|
421
|
+
//
|
|
422
|
+
// LSP 3.17 negotiates the encoding of `Position.character` per session: the
|
|
423
|
+
// client offers `general.positionEncodings`, the server picks one, and every
|
|
424
|
+
// position on that session is counted in the winner. tsgo selects `utf-8`
|
|
425
|
+
// whenever a client offers it, but ttsc computes positions of its own — the
|
|
426
|
+
// incremental buffer cache, plugin completion, the lint sidecar's diagnostic,
|
|
427
|
+
// fix, suggestion and formatting ranges, and the graph symbol provider — and
|
|
428
|
+
// every one of those counts UTF-16 code units. Because the sidecar protocol
|
|
429
|
+
// carries no encoding field, a session negotiated as `utf-8` would put tsgo's
|
|
430
|
+
// half of each response on byte columns and ttsc's half on UTF-16 columns.
|
|
431
|
+
//
|
|
432
|
+
// ttscserver therefore settles the negotiation instead of tracking it, and
|
|
433
|
+
// constrains the session to UTF-16 (see constrainInitializePositionEncoding).
|
|
434
|
+
// UTF-16 is the LSP default and the encoding every conforming client supports,
|
|
435
|
+
// so no client loses a capability; what is given up is tsgo's byte-oriented fast
|
|
436
|
+
// path, which costs one line scan per converted position inside tsgo.
|
|
437
|
+
const positionEncodingUTF16 = "utf-16"
|
|
438
|
+
|
|
439
|
+
// constrainInitializePositionEncoding narrows the client's position-encoding
|
|
440
|
+
// offer to UTF-16 before the initialize request is forwarded upstream, so the
|
|
441
|
+
// encoding tsgo selects — and therefore the one it advertises back through the
|
|
442
|
+
// proxy to the client — is the encoding every ttsc-owned conversion counts.
|
|
443
|
+
//
|
|
444
|
+
// It is deliberately a no-op for any envelope that is not an initialize request,
|
|
445
|
+
// and for any offer that names no encoding other than UTF-16: an absent, null,
|
|
446
|
+
// empty, or UTF-16-only list already means UTF-16, and rewriting it to say the
|
|
447
|
+
// same thing would change bytes on the wire for nothing. Every other offer has
|
|
448
|
+
// its list replaced rather than filtered, so no third encoding can be selected
|
|
449
|
+
// either.
|
|
450
|
+
func constrainInitializePositionEncoding(env Envelope, body []byte) []byte {
|
|
451
|
+
if env.Method != methodInitialize || !env.IsRequest() || len(env.Params) == 0 {
|
|
452
|
+
return body
|
|
453
|
+
}
|
|
454
|
+
params := map[string]json.RawMessage{}
|
|
455
|
+
if json.Unmarshal(env.Params, ¶ms) != nil {
|
|
456
|
+
return body
|
|
457
|
+
}
|
|
458
|
+
capabilities := map[string]json.RawMessage{}
|
|
459
|
+
if raw, ok := params["capabilities"]; !ok || json.Unmarshal(raw, &capabilities) != nil {
|
|
460
|
+
return body
|
|
461
|
+
}
|
|
462
|
+
general := map[string]json.RawMessage{}
|
|
463
|
+
if raw, ok := capabilities["general"]; !ok || json.Unmarshal(raw, &general) != nil {
|
|
464
|
+
return body
|
|
465
|
+
}
|
|
466
|
+
raw, ok := general["positionEncodings"]
|
|
467
|
+
if !ok {
|
|
468
|
+
return body
|
|
469
|
+
}
|
|
470
|
+
var offered []string
|
|
471
|
+
if json.Unmarshal(raw, &offered) != nil {
|
|
472
|
+
return body
|
|
473
|
+
}
|
|
474
|
+
if !offersEncodingOtherThanUTF16(offered) {
|
|
475
|
+
// Absent, null, empty, and UTF-16-only offers all already mean UTF-16.
|
|
476
|
+
return body
|
|
477
|
+
}
|
|
478
|
+
constrained, err := json.Marshal([]string{positionEncodingUTF16})
|
|
479
|
+
if err != nil {
|
|
480
|
+
return body
|
|
481
|
+
}
|
|
482
|
+
// Re-encode only the subtree that changed: every sibling value stays the
|
|
483
|
+
// client's original bytes, so a large or unusually shaped initialize payload
|
|
484
|
+
// reaches tsgo exactly as it was sent.
|
|
485
|
+
general["positionEncodings"] = constrained
|
|
486
|
+
encodedGeneral, err := json.Marshal(general)
|
|
487
|
+
if err != nil {
|
|
488
|
+
return body
|
|
489
|
+
}
|
|
490
|
+
capabilities["general"] = encodedGeneral
|
|
491
|
+
encodedCapabilities, err := json.Marshal(capabilities)
|
|
492
|
+
if err != nil {
|
|
493
|
+
return body
|
|
494
|
+
}
|
|
495
|
+
params["capabilities"] = encodedCapabilities
|
|
496
|
+
encodedParams, err := json.Marshal(params)
|
|
497
|
+
if err != nil {
|
|
498
|
+
return body
|
|
499
|
+
}
|
|
500
|
+
env.Params = encodedParams
|
|
501
|
+
encoded, err := json.Marshal(env)
|
|
502
|
+
if err != nil {
|
|
503
|
+
return body
|
|
504
|
+
}
|
|
505
|
+
return encoded
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// offersEncodingOtherThanUTF16 reports whether a client's position-encoding
|
|
509
|
+
// offer could select anything but UTF-16. An absent, null, or empty list already
|
|
510
|
+
// means the LSP default, so those offers are left exactly as the client wrote
|
|
511
|
+
// them rather than rewritten to say the same thing.
|
|
512
|
+
func offersEncodingOtherThanUTF16(offered []string) bool {
|
|
513
|
+
for _, encoding := range offered {
|
|
514
|
+
if encoding != positionEncodingUTF16 {
|
|
515
|
+
return true
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return false
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// editorRequestedExit reports whether the editor sent the LSP `exit`
|
|
522
|
+
// notification.
|
|
523
|
+
//
|
|
524
|
+
// After `exit` the upstream process is required to terminate, and the status it
|
|
525
|
+
// terminates with is not ttscserver's to report. tsgo makes that distinction
|
|
526
|
+
// load-bearing rather than theoretical: its `exit` handler returns io.EOF, which
|
|
527
|
+
// cancels its dispatch loop, and whether its process ends 0 or 1 depends on
|
|
528
|
+
// whether that cancellation or the closed stdin reaches its errgroup first. The
|
|
529
|
+
// editor sees the same clean quit either way, so ttscserver must not turn one
|
|
530
|
+
// side of that race into a failed server.
|
|
531
|
+
func (p *Proxy) editorRequestedExit() bool {
|
|
532
|
+
return p.editorExit.Load()
|
|
533
|
+
}
|
|
534
|
+
|
|
337
535
|
func (p *Proxy) rememberInitializeRequest(env Envelope) {
|
|
338
536
|
key := env.IDKey()
|
|
339
537
|
if key == "" {
|
|
@@ -369,6 +567,10 @@ func (p *Proxy) forgetCancelledRequest(env Envelope) {
|
|
|
369
567
|
delete(p.pendingLocalActions, key)
|
|
370
568
|
delete(p.pendingCommands, key)
|
|
371
569
|
delete(p.pendingInitialize, key)
|
|
570
|
+
// Completion is cancelled constantly — every keystroke supersedes the last
|
|
571
|
+
// request — so a pending entry that outlived its cancel would leak per
|
|
572
|
+
// character typed.
|
|
573
|
+
delete(p.pendingCompletions, key)
|
|
372
574
|
}
|
|
373
575
|
|
|
374
576
|
func (p *Proxy) handleCodeActionRequest(env Envelope) (bool, error) {
|
|
@@ -938,7 +1140,14 @@ func (p *Proxy) augmentUpstream(env Envelope, body []byte) []byte {
|
|
|
938
1140
|
if pendingInitialize {
|
|
939
1141
|
delete(p.pendingInitialize, key)
|
|
940
1142
|
}
|
|
1143
|
+
completions, hasCompletions := p.pendingCompletions[key]
|
|
1144
|
+
if hasCompletions {
|
|
1145
|
+
delete(p.pendingCompletions, key)
|
|
1146
|
+
}
|
|
941
1147
|
p.pendingMu.Unlock()
|
|
1148
|
+
if hasCompletions {
|
|
1149
|
+
return mergeCompletionResponseWithRequest(body, completions)
|
|
1150
|
+
}
|
|
942
1151
|
if pendingInitialize {
|
|
943
1152
|
if augmented, augOk := p.augmentInitializeResult(env); augOk {
|
|
944
1153
|
return augmented
|
|
@@ -1021,6 +1230,19 @@ func (p *Proxy) publishPluginDiagnosticsForDidOpen(env Envelope) error {
|
|
|
1021
1230
|
return nil
|
|
1022
1231
|
}
|
|
1023
1232
|
p.markDocumentClean(params.TextDocument.URI)
|
|
1233
|
+
// A clean open is a disk-snapshot boundary, not merely a diagnostics trigger.
|
|
1234
|
+
// The buffer equalling disk right now says nothing about when the warm Program
|
|
1235
|
+
// and the symbol graph last read that file: the document may have been closed
|
|
1236
|
+
// across a branch switch, a `git pull`, a generator run, or an edit in a second
|
|
1237
|
+
// editor. Refresh both compiler-backed caches for this document before the
|
|
1238
|
+
// asynchronous Diagnostics call below is scheduled, so the published findings
|
|
1239
|
+
// describe the text the editor just opened.
|
|
1240
|
+
//
|
|
1241
|
+
// The dirty branch above deliberately does not invalidate: it publishes an
|
|
1242
|
+
// empty set and reports nothing until the buffer reaches disk, so there is no
|
|
1243
|
+
// stale answer to prevent.
|
|
1244
|
+
p.invalidateSymbolProvider()
|
|
1245
|
+
p.invalidateResidentPluginsForURIs(params.TextDocument.URI)
|
|
1024
1246
|
generation, projectGeneration := p.nextDiagnosticsGenerations(params.TextDocument.URI)
|
|
1025
1247
|
go p.publishMergedPluginDiagnostics(params.TextDocument.URI, params.TextDocument.Version, false, generation, projectGeneration)
|
|
1026
1248
|
return nil
|
|
@@ -1285,11 +1507,13 @@ type lspRangeWire struct {
|
|
|
1285
1507
|
|
|
1286
1508
|
// lspPositionToByteOffset converts an LSP Position into a byte offset into text.
|
|
1287
1509
|
//
|
|
1288
|
-
// LSP Position.character is
|
|
1289
|
-
//
|
|
1290
|
-
//
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1510
|
+
// LSP Position.character is counted in the session's negotiated
|
|
1511
|
+
// PositionEncodingKind, which constrainInitializePositionEncoding pins to UTF-16
|
|
1512
|
+
// for every ttscserver session — so it is a UTF-16 code-unit offset here, not a
|
|
1513
|
+
// byte or rune offset: a rune in the astral planes (>= U+10000) counts as two
|
|
1514
|
+
// UTF-16 code units. The walk advances line by line over '\n' and '\r\n' endings
|
|
1515
|
+
// to the target line, then advances `character` UTF-16 code units within that
|
|
1516
|
+
// line and returns the corresponding byte index.
|
|
1293
1517
|
//
|
|
1294
1518
|
// Decision on out-of-range positions: when line/character point past the end of
|
|
1295
1519
|
// the text the function returns (len, false) rather than clamping — the caller
|
|
@@ -1633,6 +1857,27 @@ func (p *Proxy) augmentInitializeResult(env Envelope) ([]byte, bool) {
|
|
|
1633
1857
|
caps["executeCommandProvider"] = provider
|
|
1634
1858
|
changed = true
|
|
1635
1859
|
}
|
|
1860
|
+
// Merge the plugin's trigger characters into tsgo's completionProvider
|
|
1861
|
+
// rather than replacing it. tsgo already advertises `.`, `"`, `'`, backtick,
|
|
1862
|
+
// `/`, `@`, `<`, and `#`, and that list is what wakes its own completion —
|
|
1863
|
+
// substituting a plugin's would silence the compiler's suggestions to make
|
|
1864
|
+
// room for a rule's. When upstream advertises nothing, synthesize the
|
|
1865
|
+
// provider so the editor asks at all.
|
|
1866
|
+
triggers := p.pluginCompletionTriggerCharacters()
|
|
1867
|
+
// Record what the editor ends up being told before the merge rewrites it, so a
|
|
1868
|
+
// corpus that arrives after this response can tell whether its trigger already
|
|
1869
|
+
// reached the client. Recorded on every initialize, including the one where
|
|
1870
|
+
// the corpus is still empty.
|
|
1871
|
+
p.rememberAdvertisedCompletionTriggers(caps["completionProvider"], triggers)
|
|
1872
|
+
if len(triggers) > 0 {
|
|
1873
|
+
provider, _ := caps["completionProvider"].(map[string]any)
|
|
1874
|
+
if provider == nil {
|
|
1875
|
+
provider = map[string]any{}
|
|
1876
|
+
}
|
|
1877
|
+
provider["triggerCharacters"] = mergeCommandIDs(provider["triggerCharacters"], triggers)
|
|
1878
|
+
caps["completionProvider"] = provider
|
|
1879
|
+
changed = true
|
|
1880
|
+
}
|
|
1636
1881
|
// Advertise documentFormattingProvider when ttsc owns the document
|
|
1637
1882
|
// formatter so editors send textDocument/formatting (formatOnSave). The
|
|
1638
1883
|
// proxy intercepts that method and formats the live buffer, so it forces
|
|
@@ -1750,6 +1995,158 @@ func (p *Proxy) pluginCodeActionKinds() []string {
|
|
|
1750
1995
|
return nil
|
|
1751
1996
|
}
|
|
1752
1997
|
|
|
1998
|
+
// pluginCompletionHints returns the corpus a plugin published, or nil when the
|
|
1999
|
+
// source does not offer one.
|
|
2000
|
+
//
|
|
2001
|
+
// Optional-interface assertion for the same reason CodeActionKinds uses it: a
|
|
2002
|
+
// PluginSource that predates this — NullPluginSource included — must keep
|
|
2003
|
+
// compiling and must contribute nothing rather than break.
|
|
2004
|
+
func (p *Proxy) pluginCompletionHints() []LSPCompletionHint {
|
|
2005
|
+
type completionHintSource interface {
|
|
2006
|
+
CompletionHints() []LSPCompletionHint
|
|
2007
|
+
}
|
|
2008
|
+
if source, ok := p.source.(completionHintSource); ok {
|
|
2009
|
+
return source.CompletionHints()
|
|
2010
|
+
}
|
|
2011
|
+
return nil
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// pluginCompletionTriggerCharacters returns the characters that should wake the
|
|
2015
|
+
// editor for a plugin hint.
|
|
2016
|
+
//
|
|
2017
|
+
// The last character of each After is what the user types to reach the hint, so
|
|
2018
|
+
// that is what must be advertised. These are MERGED into whatever tsgo already
|
|
2019
|
+
// advertises, never substituted: tsgo's own list drives its completion, and
|
|
2020
|
+
// replacing it would silence the compiler's suggestions to make room for a
|
|
2021
|
+
// plugin's.
|
|
2022
|
+
func (p *Proxy) pluginCompletionTriggerCharacters() []string {
|
|
2023
|
+
hints := p.pluginCompletionHints()
|
|
2024
|
+
characters := make([]string, 0, len(hints))
|
|
2025
|
+
for _, hint := range hints {
|
|
2026
|
+
if hint.After == "" {
|
|
2027
|
+
continue
|
|
2028
|
+
}
|
|
2029
|
+
trigger, _ := utf8.DecodeLastRuneInString(hint.After)
|
|
2030
|
+
characters = append(characters, string(trigger))
|
|
2031
|
+
}
|
|
2032
|
+
return characters
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// refreshPluginCompletionHints asks the source to rediscover its corpus after an
|
|
2036
|
+
// editor event that can invalidate it. Optional-interface assertion for the same
|
|
2037
|
+
// reason pluginCompletionHints uses one: a source with no corpus to refresh —
|
|
2038
|
+
// NullPluginSource, or one built before this — is simply unaffected.
|
|
2039
|
+
//
|
|
2040
|
+
// The call returns immediately; the source owns the scheduling, coalescing, and
|
|
2041
|
+
// staleness rules, so the editor's notification is never held behind a plugin.
|
|
2042
|
+
func (p *Proxy) refreshPluginCompletionHints() {
|
|
2043
|
+
type completionHintRefresher interface {
|
|
2044
|
+
RefreshCompletionHints()
|
|
2045
|
+
}
|
|
2046
|
+
if source, ok := p.source.(completionHintRefresher); ok {
|
|
2047
|
+
source.RefreshCompletionHints()
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// completionHintsRefreshed runs after the source finishes a corpus refresh.
|
|
2052
|
+
//
|
|
2053
|
+
// Items from the new corpus need nothing here: completion reads the corpus per
|
|
2054
|
+
// request, so a refreshed corpus is live the moment it is stored. Trigger
|
|
2055
|
+
// characters are the exception. They were merged into the initialize response,
|
|
2056
|
+
// which the editor has already consumed, and LSP's only way to change them
|
|
2057
|
+
// afterwards is client/registerCapability — which in VS Code adds a SECOND
|
|
2058
|
+
// completion provider beside the static one instead of amending it, so every
|
|
2059
|
+
// item tsgo returns would be offered twice. Rather than corrupt the compiler's
|
|
2060
|
+
// own list to advertise a character, the proxy says once, per character, that
|
|
2061
|
+
// this one needs a restart. Everything else about the hint already works: the
|
|
2062
|
+
// items appear on Ctrl+Space and after any trigger the editor already knows.
|
|
2063
|
+
func (p *Proxy) completionHintsRefreshed() {
|
|
2064
|
+
for _, character := range p.takeUnadvertisedCompletionTriggers() {
|
|
2065
|
+
p.reportAsyncError(p.writeLateCompletionTriggerNotice(character))
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// takeUnadvertisedCompletionTriggers returns the corpus trigger characters the
|
|
2070
|
+
// editor was never told about, marking each as reported so a later refresh that
|
|
2071
|
+
// still carries it stays quiet. Nothing is reported before the editor has the
|
|
2072
|
+
// initialize response, because until then every trigger is still in time.
|
|
2073
|
+
func (p *Proxy) takeUnadvertisedCompletionTriggers() []string {
|
|
2074
|
+
triggers := p.pluginCompletionTriggerCharacters()
|
|
2075
|
+
if len(triggers) == 0 {
|
|
2076
|
+
return nil
|
|
2077
|
+
}
|
|
2078
|
+
p.capabilityMu.Lock()
|
|
2079
|
+
defer p.capabilityMu.Unlock()
|
|
2080
|
+
if !p.initializeAnswered {
|
|
2081
|
+
return nil
|
|
2082
|
+
}
|
|
2083
|
+
var late []string
|
|
2084
|
+
for _, character := range triggers {
|
|
2085
|
+
if _, advertised := p.advertisedCompletionTriggers[character]; advertised {
|
|
2086
|
+
continue
|
|
2087
|
+
}
|
|
2088
|
+
if _, reported := p.reportedCompletionTriggers[character]; reported {
|
|
2089
|
+
continue
|
|
2090
|
+
}
|
|
2091
|
+
if p.reportedCompletionTriggers == nil {
|
|
2092
|
+
p.reportedCompletionTriggers = map[string]struct{}{}
|
|
2093
|
+
}
|
|
2094
|
+
p.reportedCompletionTriggers[character] = struct{}{}
|
|
2095
|
+
late = append(late, character)
|
|
2096
|
+
}
|
|
2097
|
+
return late
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// rememberAdvertisedCompletionTriggers records the completion trigger set the
|
|
2101
|
+
// editor is about to receive: upstream tsgo's characters plus the ones the
|
|
2102
|
+
// corpus contributed while initialize was in flight. Recorded even when the
|
|
2103
|
+
// corpus added nothing, because that is exactly the session where a later
|
|
2104
|
+
// refresh has something new to say.
|
|
2105
|
+
func (p *Proxy) rememberAdvertisedCompletionTriggers(upstream any, added []string) {
|
|
2106
|
+
advertised := map[string]struct{}{}
|
|
2107
|
+
if provider, ok := upstream.(map[string]any); ok {
|
|
2108
|
+
if existing, ok := provider["triggerCharacters"].([]any); ok {
|
|
2109
|
+
for _, value := range existing {
|
|
2110
|
+
if character, ok := value.(string); ok && character != "" {
|
|
2111
|
+
advertised[character] = struct{}{}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
for _, character := range added {
|
|
2117
|
+
if character != "" {
|
|
2118
|
+
advertised[character] = struct{}{}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
p.capabilityMu.Lock()
|
|
2122
|
+
defer p.capabilityMu.Unlock()
|
|
2123
|
+
p.advertisedCompletionTriggers = advertised
|
|
2124
|
+
p.initializeAnswered = true
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// writeLateCompletionTriggerNotice tells the editor's output channel that one
|
|
2128
|
+
// trigger character arrived too late to be advertised. window/logMessage rather
|
|
2129
|
+
// than window/showMessage: this is a state the user may want to act on, not an
|
|
2130
|
+
// interruption of what they are doing.
|
|
2131
|
+
func (p *Proxy) writeLateCompletionTriggerNotice(character string) error {
|
|
2132
|
+
params, err := json.Marshal(map[string]any{
|
|
2133
|
+
"type": lspMessageTypeInfo,
|
|
2134
|
+
"message": fmt.Sprintf(
|
|
2135
|
+
"ttscserver: a plugin now publishes completion after %q, which was not advertised when this session started. "+
|
|
2136
|
+
"The items are available on explicit completion (Ctrl+Space); restart the language server to have the editor open them on that character.",
|
|
2137
|
+
character,
|
|
2138
|
+
),
|
|
2139
|
+
})
|
|
2140
|
+
if err != nil {
|
|
2141
|
+
return nil
|
|
2142
|
+
}
|
|
2143
|
+
body, err := json.Marshal(Envelope{JSONRPC: "2.0", Method: methodLogMessage, Params: params})
|
|
2144
|
+
if err != nil {
|
|
2145
|
+
return nil
|
|
2146
|
+
}
|
|
2147
|
+
return p.writeEditorFrame(body)
|
|
2148
|
+
}
|
|
2149
|
+
|
|
1753
2150
|
func (p *Proxy) setUpstreamCodeActionProvider(value any) {
|
|
1754
2151
|
provides := true
|
|
1755
2152
|
if boolValue, ok := value.(bool); ok {
|
|
@@ -1813,6 +2210,201 @@ func (p *Proxy) invalidateSymbolProvider() {
|
|
|
1813
2210
|
}
|
|
1814
2211
|
}
|
|
1815
2212
|
|
|
2213
|
+
// invalidateResidentPlugins tells a resident plugin source that a document
|
|
2214
|
+
// changed on disk, passing the saved document's URI so the daemon can update
|
|
2215
|
+
// that file incrementally; an unparseable notification falls back to a full
|
|
2216
|
+
// invalidation. Optional-interface assertion for the same reason
|
|
2217
|
+
// pluginCodeActionKinds uses one: a PluginSource without a resident daemon
|
|
2218
|
+
// (NullPluginSource, or one built before this) is simply unaffected.
|
|
2219
|
+
func (p *Proxy) invalidateResidentPlugins(env Envelope) {
|
|
2220
|
+
source, ok := p.residentInvalidator()
|
|
2221
|
+
if !ok {
|
|
2222
|
+
return
|
|
2223
|
+
}
|
|
2224
|
+
var params struct {
|
|
2225
|
+
TextDocument struct {
|
|
2226
|
+
URI string `json:"uri"`
|
|
2227
|
+
} `json:"textDocument"`
|
|
2228
|
+
}
|
|
2229
|
+
if len(env.Params) > 0 && json.Unmarshal(env.Params, ¶ms) == nil && params.TextDocument.URI != "" {
|
|
2230
|
+
source.InvalidateResidentPrograms(params.TextDocument.URI)
|
|
2231
|
+
return
|
|
2232
|
+
}
|
|
2233
|
+
source.InvalidateResidentPrograms()
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// residentInvalidator is the optional capability a PluginSource exposes to
|
|
2237
|
+
// refresh its warm compiler state. Optional-interface assertion for the same
|
|
2238
|
+
// reason pluginCodeActionKinds uses one: a PluginSource without a resident
|
|
2239
|
+
// daemon (NullPluginSource, or one built before this) is simply unaffected.
|
|
2240
|
+
type residentInvalidator interface {
|
|
2241
|
+
InvalidateResidentPrograms(...string)
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
func (p *Proxy) residentInvalidator() (residentInvalidator, bool) {
|
|
2245
|
+
source, ok := p.source.(residentInvalidator)
|
|
2246
|
+
return source, ok
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// invalidateResidentPluginsForURIs names the documents that changed on disk so
|
|
2250
|
+
// the daemon updates exactly those files incrementally. Calling it with no URI
|
|
2251
|
+
// is the deliberate full-reload request for a change the host cannot localize.
|
|
2252
|
+
func (p *Proxy) invalidateResidentPluginsForURIs(uris ...string) {
|
|
2253
|
+
if source, ok := p.residentInvalidator(); ok {
|
|
2254
|
+
source.InvalidateResidentPrograms(uris...)
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// LSP FileChangeType. Only `changed` names an edit to a file whose place in the
|
|
2259
|
+
// project is unchanged; created and deleted both reshape the root set, which a
|
|
2260
|
+
// per-file UpdateProgram cannot express.
|
|
2261
|
+
const (
|
|
2262
|
+
fileChangeTypeCreated = 1
|
|
2263
|
+
fileChangeTypeChanged = 2
|
|
2264
|
+
fileChangeTypeDeleted = 3
|
|
2265
|
+
)
|
|
2266
|
+
|
|
2267
|
+
// invalidateForWatchedFileChanges refreshes both compiler-backed caches for a
|
|
2268
|
+
// workspace/didChangeWatchedFiles batch, which is the only notification carrying
|
|
2269
|
+
// an on-disk change to a file the editor does not have open.
|
|
2270
|
+
//
|
|
2271
|
+
// A batch the proxy can localize — every entry a plain `changed` event on a file
|
|
2272
|
+
// that is not project configuration — travels as changed URIs so the daemon
|
|
2273
|
+
// re-parses just those files. Anything else drops the warm Program wholesale:
|
|
2274
|
+
// a created or deleted file changes the root set, and a tsconfig/jsconfig edit
|
|
2275
|
+
// changes the compiler options and the file selection, neither of which a
|
|
2276
|
+
// per-file update can express. A batch the proxy cannot decode is treated the
|
|
2277
|
+
// same conservative way, because an unread change is indistinguishable from an
|
|
2278
|
+
// unlocalizable one.
|
|
2279
|
+
//
|
|
2280
|
+
// A deleted document additionally withdraws whatever ttsc last published for it,
|
|
2281
|
+
// because refreshing the Program alone would leave the findings on screen: an
|
|
2282
|
+
// open document's content belongs to the client rather than to disk, so the
|
|
2283
|
+
// compiler has no reason to republish for that URI and trigger the merge that
|
|
2284
|
+
// would replace them.
|
|
2285
|
+
func (p *Proxy) invalidateForWatchedFileChanges(env Envelope) error {
|
|
2286
|
+
var params struct {
|
|
2287
|
+
Changes []struct {
|
|
2288
|
+
URI string `json:"uri"`
|
|
2289
|
+
Type *int `json:"type"`
|
|
2290
|
+
} `json:"changes"`
|
|
2291
|
+
}
|
|
2292
|
+
if len(env.Params) == 0 || json.Unmarshal(env.Params, ¶ms) != nil {
|
|
2293
|
+
p.invalidateSymbolProvider()
|
|
2294
|
+
p.invalidateResidentPluginsForURIs()
|
|
2295
|
+
p.refreshPluginCompletionHints()
|
|
2296
|
+
return nil
|
|
2297
|
+
}
|
|
2298
|
+
if len(params.Changes) == 0 {
|
|
2299
|
+
// The editor reported no change; dropping warm state here would throw away
|
|
2300
|
+
// the residency the daemon exists to provide for nothing. A corpus refresh
|
|
2301
|
+
// is skipped for the same reason and at higher cost: it spawns a sidecar
|
|
2302
|
+
// per plugin, each loading its own Program.
|
|
2303
|
+
return nil
|
|
2304
|
+
}
|
|
2305
|
+
uris := make([]string, 0, len(params.Changes))
|
|
2306
|
+
deleted := make([]string, 0, len(params.Changes))
|
|
2307
|
+
localizable := true
|
|
2308
|
+
for _, change := range params.Changes {
|
|
2309
|
+
if change.URI != "" && change.Type != nil && *change.Type == fileChangeTypeDeleted {
|
|
2310
|
+
deleted = append(deleted, change.URI)
|
|
2311
|
+
}
|
|
2312
|
+
if !watchedFileChangeIsLocalizable(change.URI, change.Type) {
|
|
2313
|
+
localizable = false
|
|
2314
|
+
continue
|
|
2315
|
+
}
|
|
2316
|
+
uris = append(uris, change.URI)
|
|
2317
|
+
}
|
|
2318
|
+
p.invalidateSymbolProvider()
|
|
2319
|
+
if !localizable || len(uris) == 0 {
|
|
2320
|
+
p.invalidateResidentPluginsForURIs()
|
|
2321
|
+
} else {
|
|
2322
|
+
p.invalidateResidentPluginsForURIs(uris...)
|
|
2323
|
+
}
|
|
2324
|
+
// A completion corpus projects the whole Program, so it cannot be invalidated
|
|
2325
|
+
// per URI the way the resident daemon's is: any reported change can change it.
|
|
2326
|
+
p.refreshPluginCompletionHints()
|
|
2327
|
+
for _, uri := range deleted {
|
|
2328
|
+
if err := p.withdrawPluginDiagnosticsForDeletedDocument(uri); err != nil {
|
|
2329
|
+
return err
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
return nil
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// withdrawPluginDiagnosticsForDeletedDocument removes ttsc's findings for a
|
|
2336
|
+
// document that no longer exists on disk and republishes what remains, so a
|
|
2337
|
+
// deleted file cannot keep a plugin squiggle from the last time it was linted.
|
|
2338
|
+
//
|
|
2339
|
+
// Upstream diagnostics are left in place: the compiler owns its own answer for
|
|
2340
|
+
// the same document, and the editor may legitimately keep showing it while the
|
|
2341
|
+
// buffer is still open. Bumping the diagnostic generation discards any in-flight
|
|
2342
|
+
// computation started against the pre-deletion Program. A document ttsc never
|
|
2343
|
+
// published for is a no-op, which is the ordinary case.
|
|
2344
|
+
func (p *Proxy) withdrawPluginDiagnosticsForDeletedDocument(uri string) error {
|
|
2345
|
+
p.writeMu.Lock()
|
|
2346
|
+
defer p.writeMu.Unlock()
|
|
2347
|
+
p.diagnosticsMu.Lock()
|
|
2348
|
+
defer p.diagnosticsMu.Unlock()
|
|
2349
|
+
if len(p.pluginDiagnostics[uri].diagnostics) == 0 {
|
|
2350
|
+
return nil
|
|
2351
|
+
}
|
|
2352
|
+
delete(p.pluginDiagnostics, uri)
|
|
2353
|
+
p.diagnosticGeneration[uri]++
|
|
2354
|
+
return WriteFrame(p.editorOut, p.publishDiagnosticsBody(uri, nil, p.mergedDiagnosticsLocked(uri)))
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// watchedFileChangeIsLocalizable reports whether one watched-file entry can be
|
|
2358
|
+
// delivered to the resident daemon as a changed URI, rather than forcing it to
|
|
2359
|
+
// drop the warm Program and reload.
|
|
2360
|
+
func watchedFileChangeIsLocalizable(uri string, changeType *int) bool {
|
|
2361
|
+
if uri == "" || changeType == nil {
|
|
2362
|
+
return false
|
|
2363
|
+
}
|
|
2364
|
+
switch *changeType {
|
|
2365
|
+
case fileChangeTypeChanged:
|
|
2366
|
+
// A configuration edit can change the compiler options and the selected root
|
|
2367
|
+
// files at once, which is not a single-file update however it is spelled.
|
|
2368
|
+
return !isProjectConfigURI(uri)
|
|
2369
|
+
case fileChangeTypeCreated, fileChangeTypeDeleted:
|
|
2370
|
+
// The root set moved, which tsgo's per-file UpdateProgram cannot express.
|
|
2371
|
+
return false
|
|
2372
|
+
default:
|
|
2373
|
+
// A FileChangeType outside the enum LSP defines.
|
|
2374
|
+
return false
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// isProjectConfigURI reports whether uri names a TypeScript project
|
|
2379
|
+
// configuration file — `tsconfig.json`, `jsconfig.json`, and the
|
|
2380
|
+
// `tsconfig.<name>.json` spellings the VS Code client watches. Such an edit can
|
|
2381
|
+
// change the compiler options and the root file set at once, so it is never
|
|
2382
|
+
// localizable to a single source file.
|
|
2383
|
+
func isProjectConfigURI(uri string) bool {
|
|
2384
|
+
path := uri
|
|
2385
|
+
if parsed, err := url.Parse(uri); err == nil && parsed.Path != "" {
|
|
2386
|
+
path = parsed.Path
|
|
2387
|
+
}
|
|
2388
|
+
name := strings.ToLower(path)
|
|
2389
|
+
if index := strings.LastIndexAny(name, "/\\"); index >= 0 {
|
|
2390
|
+
name = name[index+1:]
|
|
2391
|
+
}
|
|
2392
|
+
if !strings.HasSuffix(name, ".json") {
|
|
2393
|
+
return false
|
|
2394
|
+
}
|
|
2395
|
+
return strings.HasPrefix(name, "tsconfig") || strings.HasPrefix(name, "jsconfig")
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// shutdownResidentPlugins kills any resident plugin daemons on server teardown.
|
|
2399
|
+
// The children also exit on their own when the parent closes their stdin at
|
|
2400
|
+
// process exit, so this is the graceful path, not the only one.
|
|
2401
|
+
func (p *Proxy) shutdownResidentPlugins() {
|
|
2402
|
+
type residentShutdown interface{ shutdownResidents() }
|
|
2403
|
+
if source, ok := p.source.(residentShutdown); ok {
|
|
2404
|
+
source.shutdownResidents()
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
1816
2408
|
// capabilityAdvertised reports whether an LSP server-capability value means the
|
|
1817
2409
|
// server implements the capability: boolean true, or an options object. Absent
|
|
1818
2410
|
// (nil), false, or any other shape counts as unsupported.
|