bonecode 1.0.0 → 1.1.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/LICENSE +21 -0
- package/README.md +64 -50
- package/bone/output/agent/.dockerignore +7 -7
- package/bone/output/agent/.env.example +36 -36
- package/bone/output/agent/.github/workflows/ci.yaml +58 -58
- package/bone/output/agent/AgentDomain.bone.map +349 -349
- package/bone/output/agent/AgentDomain.postman_collection.json +957 -957
- package/bone/output/agent/Dockerfile +22 -22
- package/bone/output/agent/README.md +47 -47
- package/bone/output/agent/admin/index.html +739 -739
- package/bone/output/agent/docker-compose.yaml +22 -22
- package/bone/output/agent/k8s/deployment.yaml +75 -75
- package/bone/output/agent/migrations/agent.sql +36 -36
- package/bone/output/agent/migrations/agent_instance.sql +36 -36
- package/bone/output/agent/migrations/audit_log.sql +18 -18
- package/bone/output/agent/migrations/build_step.sql +34 -34
- package/bone/output/agent/migrations/event_outbox.sql +31 -31
- package/bone/output/agent/migrations/plan.sql +30 -30
- package/bone/output/agent/migrations/task.sql +30 -30
- package/bone/output/agent/migrations/tool_call.sql +33 -33
- package/bone/output/agent/openapi.yaml +1116 -1116
- package/bone/output/agent/package.json +35 -35
- package/bone/output/agent/schema.graphql +233 -233
- package/bone/output/agent/sdk/client.ts +231 -231
- package/bone/output/agent/src/algorithms.ts +2 -2
- package/bone/output/agent/src/audit.ts +44 -44
- package/bone/output/agent/src/auth.ts +57 -57
- package/bone/output/agent/src/cron.ts +12 -12
- package/bone/output/agent/src/db.ts +31 -31
- package/bone/output/agent/src/debug.ts +66 -66
- package/bone/output/agent/src/events.ts +243 -243
- package/bone/output/agent/src/extensions.ts +54 -54
- package/bone/output/agent/src/failure_rules.ts +322 -322
- package/bone/output/agent/src/flows.ts +168 -168
- package/bone/output/agent/src/health.ts +43 -43
- package/bone/output/agent/src/index.ts +99 -99
- package/bone/output/agent/src/logger.ts +69 -66
- package/bone/output/agent/src/metrics.ts +75 -75
- package/bone/output/agent/src/migrate.ts +351 -351
- package/bone/output/agent/src/migration_diff.ts +108 -108
- package/bone/output/agent/src/notify.ts +125 -125
- package/bone/output/agent/src/routes/plan.ts +91 -91
- package/bone/output/agent/src/routes/task.ts +105 -105
- package/bone/output/agent/src/routes/tool_call.ts +166 -166
- package/bone/output/agent/src/schemas.ts +384 -384
- package/bone/output/agent/src/state_machines/agent_instance.ts +24 -24
- package/bone/output/agent/src/state_machines/build_step.ts +22 -22
- package/bone/output/agent/src/state_machines/plan.ts +22 -22
- package/bone/output/agent/src/state_machines/task.ts +22 -22
- package/bone/output/agent/src/state_machines/tool_call.ts +22 -22
- package/bone/output/agent/src/tests.ts +361 -361
- package/bone/output/agent/src/websocket.ts +200 -200
- package/bone/output/agent/tsconfig.json +24 -24
- package/bone/output/rag/.dockerignore +7 -7
- package/bone/output/rag/.env.example +36 -36
- package/bone/output/rag/.github/workflows/ci.yaml +58 -58
- package/bone/output/rag/Dockerfile +22 -22
- package/bone/output/rag/RAGDomain.bone.map +286 -286
- package/bone/output/rag/RAGDomain.postman_collection.json +922 -922
- package/bone/output/rag/README.md +47 -47
- package/bone/output/rag/admin/index.html +817 -817
- package/bone/output/rag/docker-compose.yaml +22 -22
- package/bone/output/rag/k8s/deployment.yaml +75 -75
- package/bone/output/rag/migrations/audit_log.sql +18 -18
- package/bone/output/rag/migrations/code_chunk.sql +34 -34
- package/bone/output/rag/migrations/code_file.sql +33 -33
- package/bone/output/rag/migrations/event_outbox.sql +31 -31
- package/bone/output/rag/migrations/indexing_job.sql +33 -33
- package/bone/output/rag/migrations/knowledge_base.sql +35 -35
- package/bone/output/rag/migrations/memory_entry.sql +34 -34
- package/bone/output/rag/openapi.yaml +1097 -1097
- package/bone/output/rag/package.json +35 -35
- package/bone/output/rag/schema.graphql +245 -245
- package/bone/output/rag/sdk/client.ts +234 -234
- package/bone/output/rag/src/algorithms.ts +2 -2
- package/bone/output/rag/src/audit.ts +37 -37
- package/bone/output/rag/src/auth.ts +57 -57
- package/bone/output/rag/src/cron.ts +12 -12
- package/bone/output/rag/src/db.ts +31 -31
- package/bone/output/rag/src/debug.ts +66 -66
- package/bone/output/rag/src/events.ts +243 -243
- package/bone/output/rag/src/extensions.ts +350 -350
- package/bone/output/rag/src/failure_rules.ts +314 -314
- package/bone/output/rag/src/flows.ts +239 -239
- package/bone/output/rag/src/health.ts +43 -43
- package/bone/output/rag/src/index.ts +94 -94
- package/bone/output/rag/src/logger.ts +69 -66
- package/bone/output/rag/src/metrics.ts +75 -75
- package/bone/output/rag/src/migrate.ts +363 -363
- package/bone/output/rag/src/migration_diff.ts +108 -108
- package/bone/output/rag/src/notify.ts +99 -99
- package/bone/output/rag/src/routes/code_chunk.ts +75 -75
- package/bone/output/rag/src/routes/code_file.ts +101 -101
- package/bone/output/rag/src/routes/indexing_job.ts +87 -87
- package/bone/output/rag/src/routes/knowledge_base.ts +230 -230
- package/bone/output/rag/src/routes/memory_entry.ts +87 -87
- package/bone/output/rag/src/schemas.ts +394 -394
- package/bone/output/rag/src/state_machines/code_file.ts +23 -23
- package/bone/output/rag/src/state_machines/indexing_job.ts +22 -22
- package/bone/output/rag/src/state_machines/knowledge_base.ts +23 -23
- package/bone/output/rag/src/state_machines/memory_entry.ts +20 -20
- package/bone/output/rag/src/tests.ts +339 -339
- package/bone/output/rag/tsconfig.json +24 -24
- package/bone/output/session/.dockerignore +7 -7
- package/bone/output/session/.env.example +36 -36
- package/bone/output/session/.github/workflows/ci.yaml +58 -58
- package/bone/output/session/Dockerfile +22 -22
- package/bone/output/session/README.md +47 -47
- package/bone/output/session/SessionDomain.bone.map +349 -349
- package/bone/output/session/SessionDomain.postman_collection.json +957 -957
- package/bone/output/session/admin/index.html +666 -666
- package/bone/output/session/docker-compose.yaml +22 -22
- package/bone/output/session/k8s/deployment.yaml +75 -75
- package/bone/output/session/migrations/audit_log.sql +18 -18
- package/bone/output/session/migrations/event_outbox.sql +31 -31
- package/bone/output/session/migrations/message.sql +31 -31
- package/bone/output/session/migrations/part.sql +28 -28
- package/bone/output/session/migrations/permission.sql +28 -28
- package/bone/output/session/migrations/project.sql +28 -28
- package/bone/output/session/migrations/session.sql +38 -38
- package/bone/output/session/openapi.yaml +1101 -1101
- package/bone/output/session/package.json +35 -35
- package/bone/output/session/schema.graphql +222 -222
- package/bone/output/session/sdk/client.ts +225 -225
- package/bone/output/session/src/algorithms.ts +2 -2
- package/bone/output/session/src/audit.ts +44 -44
- package/bone/output/session/src/auth.ts +57 -57
- package/bone/output/session/src/cron.ts +12 -12
- package/bone/output/session/src/db.ts +31 -31
- package/bone/output/session/src/debug.ts +66 -66
- package/bone/output/session/src/events.ts +270 -270
- package/bone/output/session/src/extensions.ts +215 -215
- package/bone/output/session/src/failure_rules.ts +283 -283
- package/bone/output/session/src/flows.ts +168 -168
- package/bone/output/session/src/health.ts +43 -43
- package/bone/output/session/src/index.ts +99 -99
- package/bone/output/session/src/logger.ts +67 -66
- package/bone/output/session/src/metrics.ts +75 -75
- package/bone/output/session/src/migrate.ts +331 -331
- package/bone/output/session/src/migration_diff.ts +108 -108
- package/bone/output/session/src/notify.ts +112 -112
- package/bone/output/session/src/routes/message.ts +93 -93
- package/bone/output/session/src/routes/part.ts +79 -79
- package/bone/output/session/src/routes/permission.ts +79 -79
- package/bone/output/session/src/routes/project.ts +79 -79
- package/bone/output/session/src/routes/session.ts +294 -294
- package/bone/output/session/src/schemas.ts +357 -357
- package/bone/output/session/src/state_machines/session.ts +23 -23
- package/bone/output/session/src/tests.ts +325 -325
- package/bone/output/session/src/websocket.ts +223 -200
- package/bone/output/session/tsconfig.json +24 -24
- package/bone/output/workspace/.dockerignore +7 -7
- package/bone/output/workspace/.env.example +36 -36
- package/bone/output/workspace/.github/workflows/ci.yaml +58 -58
- package/bone/output/workspace/Dockerfile +22 -22
- package/bone/output/workspace/README.md +45 -45
- package/bone/output/workspace/WorkspaceDomain.bone.map +188 -188
- package/bone/output/workspace/WorkspaceDomain.postman_collection.json +620 -620
- package/bone/output/workspace/admin/index.html +484 -484
- package/bone/output/workspace/docker-compose.yaml +22 -22
- package/bone/output/workspace/k8s/deployment.yaml +75 -75
- package/bone/output/workspace/migrations/audit_log.sql +18 -18
- package/bone/output/workspace/migrations/codebase.sql +34 -34
- package/bone/output/workspace/migrations/event_outbox.sql +31 -31
- package/bone/output/workspace/migrations/snapshot.sql +32 -32
- package/bone/output/workspace/migrations/workspace.sql +33 -33
- package/bone/output/workspace/openapi.yaml +721 -721
- package/bone/output/workspace/package.json +35 -35
- package/bone/output/workspace/schema.graphql +153 -153
- package/bone/output/workspace/sdk/client.ts +155 -155
- package/bone/output/workspace/src/algorithms.ts +2 -2
- package/bone/output/workspace/src/audit.ts +37 -37
- package/bone/output/workspace/src/auth.ts +57 -57
- package/bone/output/workspace/src/cron.ts +12 -12
- package/bone/output/workspace/src/db.ts +31 -31
- package/bone/output/workspace/src/debug.ts +66 -66
- package/bone/output/workspace/src/events.ts +243 -243
- package/bone/output/workspace/src/extensions.ts +44 -44
- package/bone/output/workspace/src/failure_rules.ts +152 -152
- package/bone/output/workspace/src/health.ts +43 -43
- package/bone/output/workspace/src/index.ts +88 -88
- package/bone/output/workspace/src/logger.ts +69 -66
- package/bone/output/workspace/src/metrics.ts +75 -75
- package/bone/output/workspace/src/migrate.ts +219 -219
- package/bone/output/workspace/src/migration_diff.ts +108 -108
- package/bone/output/workspace/src/notify.ts +73 -73
- package/bone/output/workspace/src/routes/codebase.ts +87 -87
- package/bone/output/workspace/src/routes/snapshot.ts +127 -127
- package/bone/output/workspace/src/routes/workspace.ts +190 -190
- package/bone/output/workspace/src/schemas.ts +231 -231
- package/bone/output/workspace/src/state_machines/codebase.ts +21 -21
- package/bone/output/workspace/src/state_machines/snapshot.ts +20 -20
- package/bone/output/workspace/src/state_machines/workspace.ts +21 -21
- package/bone/output/workspace/src/tests.ts +248 -248
- package/bone/output/workspace/tsconfig.json +24 -24
- package/compat/opencode_adapter.ts +94 -17
- package/package.json +15 -2
- package/src/cli.ts +66 -107
- package/src/db_adapter.ts +354 -0
- package/src/engine/account/account.sql.ts +39 -39
- package/src/engine/account/account.ts +456 -456
- package/src/engine/account/repo.ts +166 -166
- package/src/engine/account/schema.ts +99 -99
- package/src/engine/account/url.ts +8 -8
- package/src/engine/acp/README.md +174 -174
- package/src/engine/acp/agent.ts +1968 -1968
- package/src/engine/acp/runtime.ts +22 -22
- package/src/engine/acp/session.ts +122 -122
- package/src/engine/acp/types.ts +24 -24
- package/src/engine/agent/agent.ts +463 -463
- package/src/engine/agent/generate.txt +75 -75
- package/src/engine/agent/prompt/compaction.txt +9 -9
- package/src/engine/agent/prompt/explore.txt +18 -18
- package/src/engine/agent/prompt/scout.txt +36 -36
- package/src/engine/agent/prompt/summary.txt +11 -11
- package/src/engine/agent/prompt/title.txt +44 -44
- package/src/engine/agent/subagent-permissions.ts +34 -34
- package/src/engine/auth/index.ts +96 -96
- package/src/engine/background/background/job.ts +200 -200
- package/src/engine/background/job.ts +200 -200
- package/src/engine/bus/bus-event.ts +45 -45
- package/src/engine/bus/global.ts +22 -22
- package/src/engine/bus/index.ts +203 -203
- package/src/engine/command/command/index.ts +181 -181
- package/src/engine/command/command/template/initialize.txt +66 -66
- package/src/engine/command/command/template/review.txt +101 -101
- package/src/engine/command/index.ts +181 -181
- package/src/engine/command/template/initialize.txt +66 -66
- package/src/engine/command/template/review.txt +101 -101
- package/src/engine/config/agent.ts +172 -172
- package/src/engine/config/attachment.ts +25 -25
- package/src/engine/config/command.ts +62 -62
- package/src/engine/config/config.ts +833 -833
- package/src/engine/config/console-state.ts +14 -14
- package/src/engine/config/entry-name.ts +16 -16
- package/src/engine/config/error.ts +23 -23
- package/src/engine/config/formatter.ts +13 -13
- package/src/engine/config/layout.ts +6 -6
- package/src/engine/config/lsp.ts +43 -43
- package/src/engine/config/managed.ts +71 -71
- package/src/engine/config/markdown.ts +96 -96
- package/src/engine/config/mcp.ts +56 -56
- package/src/engine/config/model-id.ts +5 -5
- package/src/engine/config/parse.ts +79 -79
- package/src/engine/config/paths.ts +45 -45
- package/src/engine/config/permission.ts +58 -58
- package/src/engine/config/plugin.ts +84 -84
- package/src/engine/config/provider.ts +111 -111
- package/src/engine/config/reference.ts +23 -23
- package/src/engine/config/server.ts +19 -19
- package/src/engine/config/skills.ts +14 -14
- package/src/engine/config/variable.ts +90 -90
- package/src/engine/control-plane/adapters/index.ts +41 -41
- package/src/engine/control-plane/adapters/worktree.ts +96 -96
- package/src/engine/control-plane/dev/README.md +19 -19
- package/src/engine/control-plane/dev/debug-workspace-plugin.ts +73 -73
- package/src/engine/control-plane/schema.ts +14 -14
- package/src/engine/control-plane/types.ts +59 -59
- package/src/engine/control-plane/util.ts +39 -39
- package/src/engine/control-plane/workspace-adapter-runtime.ts +51 -51
- package/src/engine/control-plane/workspace-context.ts +26 -26
- package/src/engine/control-plane/workspace.sql.ts +20 -20
- package/src/engine/control-plane/workspace.ts +1072 -1072
- package/src/engine/data-migration.ts +161 -161
- package/src/engine/effect/app-runtime.ts +143 -143
- package/src/engine/effect/bootstrap-runtime.ts +29 -29
- package/src/engine/effect/bridge.ts +84 -84
- package/src/engine/effect/config-service.ts +67 -67
- package/src/engine/effect/instance-ref.ts +11 -11
- package/src/engine/effect/instance-registry.ts +12 -12
- package/src/engine/effect/instance-state.ts +72 -72
- package/src/engine/effect/promise.ts +17 -17
- package/src/engine/effect/run-service.ts +47 -47
- package/src/engine/effect/runner.ts +217 -217
- package/src/engine/effect/runtime-flags.ts +74 -74
- package/src/engine/effect/service-use.ts +38 -38
- package/src/engine/env/index.ts +37 -37
- package/src/engine/event-v2-bridge.ts +89 -89
- package/src/engine/file/file/ignore.ts +81 -81
- package/src/engine/file/file/index.ts +651 -651
- package/src/engine/file/file/protected.ts +59 -59
- package/src/engine/file/file/ripgrep.ts +481 -481
- package/src/engine/file/file/watcher.ts +167 -167
- package/src/engine/file/ignore.ts +81 -81
- package/src/engine/file/index.ts +651 -651
- package/src/engine/file/protected.ts +59 -59
- package/src/engine/file/ripgrep.ts +481 -481
- package/src/engine/file/watcher.ts +167 -167
- package/src/engine/format/format/formatter.ts +404 -404
- package/src/engine/format/format/index.ts +209 -209
- package/src/engine/format/formatter.ts +404 -404
- package/src/engine/format/index.ts +209 -209
- package/src/engine/git/git/index.ts +347 -347
- package/src/engine/git/index.ts +347 -347
- package/src/engine/id/id.ts +80 -80
- package/src/engine/ide/index.ts +70 -70
- package/src/engine/image/image/image.ts +176 -176
- package/src/engine/image/image.ts +176 -176
- package/src/engine/index.ts +251 -251
- package/src/engine/installation/index.ts +327 -327
- package/src/engine/lsp/client.ts +707 -707
- package/src/engine/lsp/diagnostic.ts +29 -29
- package/src/engine/lsp/language.ts +121 -121
- package/src/engine/lsp/launch.ts +21 -21
- package/src/engine/lsp/lsp/client.ts +707 -707
- package/src/engine/lsp/lsp/diagnostic.ts +29 -29
- package/src/engine/lsp/lsp/language.ts +121 -121
- package/src/engine/lsp/lsp/launch.ts +21 -21
- package/src/engine/lsp/lsp/lsp.ts +507 -507
- package/src/engine/lsp/lsp/server.ts +2064 -2064
- package/src/engine/lsp/lsp.ts +507 -507
- package/src/engine/lsp/server.ts +2064 -2064
- package/src/engine/mcp/auth.ts +146 -146
- package/src/engine/mcp/index.ts +958 -958
- package/src/engine/mcp/mcp/auth.ts +146 -146
- package/src/engine/mcp/mcp/index.ts +958 -958
- package/src/engine/mcp/mcp/oauth-callback.ts +232 -232
- package/src/engine/mcp/mcp/oauth-provider.ts +214 -214
- package/src/engine/mcp/oauth-callback.ts +232 -232
- package/src/engine/mcp/oauth-provider.ts +214 -214
- package/src/engine/node.ts +6 -6
- package/src/engine/patch/index.ts +689 -689
- package/src/engine/patch/patch/index.ts +689 -689
- package/src/engine/permission/arity.ts +163 -163
- package/src/engine/permission/evaluate.ts +15 -15
- package/src/engine/permission/index.ts +306 -306
- package/src/engine/permission/permission/arity.ts +163 -163
- package/src/engine/permission/permission/evaluate.ts +15 -15
- package/src/engine/permission/permission/index.ts +306 -306
- package/src/engine/permission/permission/schema.ts +13 -13
- package/src/engine/permission/schema.ts +13 -13
- package/src/engine/plugin/azure.ts +26 -26
- package/src/engine/plugin/cloudflare.ts +76 -76
- package/src/engine/plugin/codex.ts +622 -622
- package/src/engine/plugin/digitalocean.ts +411 -411
- package/src/engine/plugin/github-copilot/copilot.ts +394 -394
- package/src/engine/plugin/github-copilot/models.ts +196 -196
- package/src/engine/plugin/index.ts +295 -295
- package/src/engine/plugin/install.ts +439 -439
- package/src/engine/plugin/loader.ts +216 -216
- package/src/engine/plugin/meta.ts +188 -188
- package/src/engine/plugin/shared.ts +323 -323
- package/src/engine/project/bootstrap-service.ts +9 -9
- package/src/engine/project/bootstrap.ts +75 -75
- package/src/engine/project/instance-context.ts +24 -24
- package/src/engine/project/instance-layer.ts +11 -11
- package/src/engine/project/instance-runtime.ts +16 -16
- package/src/engine/project/instance-store.ts +193 -193
- package/src/engine/project/project.sql.ts +17 -17
- package/src/engine/project/project.ts +537 -537
- package/src/engine/project/schema.ts +13 -13
- package/src/engine/project/vcs.ts +405 -405
- package/src/engine/provider/auth.ts +225 -225
- package/src/engine/provider/error.ts +204 -204
- package/src/engine/provider/model-status.ts +8 -8
- package/src/engine/provider/provider.ts +1843 -1843
- package/src/engine/provider/schema.ts +30 -30
- package/src/engine/provider/transform.ts +1376 -1376
- package/src/engine/pty/index.ts +365 -365
- package/src/engine/pty/input.ts +24 -24
- package/src/engine/pty/pty/index.ts +365 -365
- package/src/engine/pty/pty/input.ts +24 -24
- package/src/engine/pty/pty/pty.bun.ts +26 -26
- package/src/engine/pty/pty/pty.node.ts +27 -27
- package/src/engine/pty/pty/pty.ts +25 -25
- package/src/engine/pty/pty/schema.ts +14 -14
- package/src/engine/pty/pty/ticket.ts +68 -68
- package/src/engine/pty/pty.bun.ts +26 -26
- package/src/engine/pty/pty.node.ts +27 -27
- package/src/engine/pty/pty.ts +25 -25
- package/src/engine/pty/schema.ts +14 -14
- package/src/engine/pty/ticket.ts +68 -68
- package/src/engine/question/index.ts +213 -213
- package/src/engine/question/question/index.ts +213 -213
- package/src/engine/question/question/schema.ts +10 -10
- package/src/engine/question/schema.ts +10 -10
- package/src/engine/reference/reference/reference.ts +241 -241
- package/src/engine/reference/reference/repository-cache.ts +147 -147
- package/src/engine/reference/reference.ts +241 -241
- package/src/engine/reference/repository-cache.ts +147 -147
- package/src/engine/session/compaction.ts +651 -651
- package/src/engine/session/instruction.ts +238 -238
- package/src/engine/session/llm.ts +459 -459
- package/src/engine/session/message-error.ts +14 -14
- package/src/engine/session/message-v2.ts +1202 -1202
- package/src/engine/session/message.ts +146 -146
- package/src/engine/session/overflow.ts +32 -32
- package/src/engine/session/processor.ts +823 -823
- package/src/engine/session/prompt/anthropic.txt +105 -105
- package/src/engine/session/prompt/beast.txt +147 -147
- package/src/engine/session/prompt/build-switch.txt +5 -5
- package/src/engine/session/prompt/codex.txt +79 -79
- package/src/engine/session/prompt/copilot-gpt-5.txt +143 -143
- package/src/engine/session/prompt/default.txt +105 -105
- package/src/engine/session/prompt/gemini.txt +155 -155
- package/src/engine/session/prompt/gpt.txt +107 -107
- package/src/engine/session/prompt/kimi.txt +95 -95
- package/src/engine/session/prompt/max-steps.txt +15 -15
- package/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -67
- package/src/engine/session/prompt/plan.txt +26 -26
- package/src/engine/session/prompt/trinity.txt +97 -97
- package/src/engine/session/prompt.ts +66 -9
- package/src/engine/session/retry.ts +200 -200
- package/src/engine/session/revert.ts +162 -162
- package/src/engine/session/run-state.ts +153 -153
- package/src/engine/session/schema.ts +26 -26
- package/src/engine/session/session.sql.ts +137 -137
- package/src/engine/session/session.ts +1011 -1011
- package/src/engine/session/status.ts +94 -94
- package/src/engine/session/summary.ts +164 -164
- package/src/engine/session/system.ts +84 -84
- package/src/engine/session/todo.ts +81 -81
- package/src/engine/share/session.ts +61 -61
- package/src/engine/share/share-next.ts +376 -376
- package/src/engine/share/share.sql.ts +13 -13
- package/src/engine/shell/shell/shell.ts +215 -215
- package/src/engine/shell/shell.ts +215 -215
- package/src/engine/skill/discovery.ts +116 -116
- package/src/engine/skill/index.ts +336 -336
- package/src/engine/skill/prompt/customize-opencode.md +377 -377
- package/src/engine/skill/skill/discovery.ts +116 -116
- package/src/engine/skill/skill/index.ts +336 -336
- package/src/engine/skill/skill/prompt/customize-opencode.md +377 -377
- package/src/engine/snapshot/index.ts +762 -762
- package/src/engine/snapshot/snapshot/index.ts +762 -762
- package/src/engine/sync/README.md +179 -179
- package/src/engine/sync/event.sql.ts +17 -17
- package/src/engine/sync/index.ts +410 -410
- package/src/engine/sync/schema.ts +11 -11
- package/src/engine/temporary.ts +33 -33
- package/src/engine/tool/apply_patch.ts +313 -313
- package/src/engine/tool/apply_patch.txt +33 -33
- package/src/engine/tool/edit.ts +711 -711
- package/src/engine/tool/edit.txt +10 -10
- package/src/engine/tool/external-directory.ts +49 -49
- package/src/engine/tool/glob.ts +103 -103
- package/src/engine/tool/glob.txt +6 -6
- package/src/engine/tool/grep.ts +156 -156
- package/src/engine/tool/grep.txt +8 -8
- package/src/engine/tool/invalid.ts +21 -21
- package/src/engine/tool/json-schema.ts +164 -164
- package/src/engine/tool/lsp.ts +113 -113
- package/src/engine/tool/lsp.txt +24 -24
- package/src/engine/tool/mcp-websearch.ts +96 -96
- package/src/engine/tool/plan-enter.txt +14 -14
- package/src/engine/tool/plan-exit.txt +13 -13
- package/src/engine/tool/plan.ts +78 -78
- package/src/engine/tool/question.ts +44 -44
- package/src/engine/tool/question.txt +10 -10
- package/src/engine/tool/read.ts +337 -337
- package/src/engine/tool/read.txt +14 -14
- package/src/engine/tool/registry.ts +472 -472
- package/src/engine/tool/repo_clone.ts +80 -80
- package/src/engine/tool/repo_clone.txt +5 -5
- package/src/engine/tool/repo_overview.ts +279 -279
- package/src/engine/tool/repo_overview.txt +4 -4
- package/src/engine/tool/schema.ts +14 -14
- package/src/engine/tool/shell/id.ts +19 -19
- package/src/engine/tool/shell/prompt.ts +295 -295
- package/src/engine/tool/shell/shell.txt +77 -77
- package/src/engine/tool/shell.ts +647 -647
- package/src/engine/tool/skill.ts +75 -75
- package/src/engine/tool/skill.txt +5 -5
- package/src/engine/tool/task.ts +337 -337
- package/src/engine/tool/task.txt +58 -58
- package/src/engine/tool/task_status.ts +179 -179
- package/src/engine/tool/task_status.txt +13 -13
- package/src/engine/tool/todo.ts +57 -57
- package/src/engine/tool/todowrite.txt +167 -167
- package/src/engine/tool/tool/apply_patch.ts +313 -313
- package/src/engine/tool/tool/apply_patch.txt +33 -33
- package/src/engine/tool/tool/edit.ts +711 -711
- package/src/engine/tool/tool/edit.txt +10 -10
- package/src/engine/tool/tool/external-directory.ts +49 -49
- package/src/engine/tool/tool/glob.ts +103 -103
- package/src/engine/tool/tool/glob.txt +6 -6
- package/src/engine/tool/tool/grep.ts +156 -156
- package/src/engine/tool/tool/grep.txt +8 -8
- package/src/engine/tool/tool/invalid.ts +21 -21
- package/src/engine/tool/tool/json-schema.ts +164 -164
- package/src/engine/tool/tool/lsp.ts +113 -113
- package/src/engine/tool/tool/lsp.txt +24 -24
- package/src/engine/tool/tool/mcp-websearch.ts +96 -96
- package/src/engine/tool/tool/plan-enter.txt +14 -14
- package/src/engine/tool/tool/plan-exit.txt +13 -13
- package/src/engine/tool/tool/plan.ts +78 -78
- package/src/engine/tool/tool/question.ts +44 -44
- package/src/engine/tool/tool/question.txt +10 -10
- package/src/engine/tool/tool/read.ts +337 -337
- package/src/engine/tool/tool/read.txt +14 -14
- package/src/engine/tool/tool/registry.ts +472 -472
- package/src/engine/tool/tool/repo_clone.ts +80 -80
- package/src/engine/tool/tool/repo_clone.txt +5 -5
- package/src/engine/tool/tool/repo_overview.ts +279 -279
- package/src/engine/tool/tool/repo_overview.txt +4 -4
- package/src/engine/tool/tool/schema.ts +14 -14
- package/src/engine/tool/tool/shell/id.ts +19 -19
- package/src/engine/tool/tool/shell/prompt.ts +295 -295
- package/src/engine/tool/tool/shell/shell.txt +77 -77
- package/src/engine/tool/tool/shell.ts +647 -647
- package/src/engine/tool/tool/skill.ts +75 -75
- package/src/engine/tool/tool/skill.txt +5 -5
- package/src/engine/tool/tool/task.ts +337 -337
- package/src/engine/tool/tool/task.txt +58 -58
- package/src/engine/tool/tool/task_status.ts +179 -179
- package/src/engine/tool/tool/task_status.txt +13 -13
- package/src/engine/tool/tool/todo.ts +57 -57
- package/src/engine/tool/tool/todowrite.txt +167 -167
- package/src/engine/tool/tool/tool.ts +164 -164
- package/src/engine/tool/tool/truncate.ts +160 -160
- package/src/engine/tool/tool/truncation-dir.ts +4 -4
- package/src/engine/tool/tool/webfetch.ts +192 -192
- package/src/engine/tool/tool/webfetch.txt +13 -13
- package/src/engine/tool/tool/websearch.ts +143 -143
- package/src/engine/tool/tool/websearch.txt +14 -14
- package/src/engine/tool/tool/write.ts +104 -104
- package/src/engine/tool/tool/write.txt +8 -8
- package/src/engine/tool/tool.ts +164 -164
- package/src/engine/tool/truncate.ts +160 -160
- package/src/engine/tool/truncation-dir.ts +4 -4
- package/src/engine/tool/webfetch.ts +192 -192
- package/src/engine/tool/webfetch.txt +13 -13
- package/src/engine/tool/websearch.ts +143 -143
- package/src/engine/tool/websearch.txt +14 -14
- package/src/engine/tool/write.ts +104 -104
- package/src/engine/tool/write.txt +8 -8
- package/src/engine/util/archive.ts +17 -17
- package/src/engine/util/bom.ts +31 -31
- package/src/engine/util/data-url.ts +9 -9
- package/src/engine/util/defer.ts +10 -10
- package/src/engine/util/effect-http-client.ts +11 -11
- package/src/engine/util/error.ts +88 -88
- package/src/engine/util/filesystem.ts +252 -252
- package/src/engine/util/format.ts +20 -20
- package/src/engine/util/iife.ts +3 -3
- package/src/engine/util/lazy.ts +20 -20
- package/src/engine/util/local-context.ts +25 -25
- package/src/engine/util/locale.ts +86 -86
- package/src/engine/util/media.ts +26 -26
- package/src/engine/util/process.ts +176 -176
- package/src/engine/util/queue.ts +32 -32
- package/src/engine/util/record.ts +3 -3
- package/src/engine/util/repository.ts +158 -158
- package/src/engine/util/rpc.ts +66 -66
- package/src/engine/util/signal.ts +12 -12
- package/src/engine/util/timeout.ts +13 -13
- package/src/engine/util/token.ts +7 -7
- package/src/engine/util/util/archive.ts +17 -17
- package/src/engine/util/util/bom.ts +31 -31
- package/src/engine/util/util/data-url.ts +9 -9
- package/src/engine/util/util/defer.ts +10 -10
- package/src/engine/util/util/effect-http-client.ts +11 -11
- package/src/engine/util/util/error.ts +88 -88
- package/src/engine/util/util/filesystem.ts +252 -252
- package/src/engine/util/util/format.ts +20 -20
- package/src/engine/util/util/iife.ts +3 -3
- package/src/engine/util/util/lazy.ts +20 -20
- package/src/engine/util/util/local-context.ts +25 -25
- package/src/engine/util/util/locale.ts +86 -86
- package/src/engine/util/util/media.ts +26 -26
- package/src/engine/util/util/process.ts +176 -176
- package/src/engine/util/util/queue.ts +32 -32
- package/src/engine/util/util/record.ts +3 -3
- package/src/engine/util/util/repository.ts +158 -158
- package/src/engine/util/util/rpc.ts +66 -66
- package/src/engine/util/util/signal.ts +12 -12
- package/src/engine/util/util/timeout.ts +13 -13
- package/src/engine/util/util/token.ts +7 -7
- package/src/engine/util/util/which.ts +14 -14
- package/src/engine/util/util/wildcard.ts +59 -59
- package/src/engine/util/which.ts +14 -14
- package/src/engine/util/wildcard.ts +59 -59
- package/src/engine/worktree/index.ts +621 -621
- package/src/server.ts +121 -158
- package/src/tui.ts +485 -502
package/src/engine/lsp/client.ts
CHANGED
|
@@ -1,707 +1,707 @@
|
|
|
1
|
-
import { BusEvent } from "@/bus/bus-event"
|
|
2
|
-
import { Bus } from "@/bus"
|
|
3
|
-
import path from "path"
|
|
4
|
-
import { pathToFileURL, fileURLToPath } from "url"
|
|
5
|
-
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
|
|
6
|
-
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
|
|
7
|
-
import * as Log from "@opencode-ai/core/util/log"
|
|
8
|
-
import { Process } from "@/util/process"
|
|
9
|
-
import { LANGUAGE_EXTENSIONS } from "./language"
|
|
10
|
-
import { Effect, Schema } from "effect"
|
|
11
|
-
import type * as LSPServer from "./server"
|
|
12
|
-
import { withTimeout } from "../util/timeout"
|
|
13
|
-
import { Filesystem } from "@/util/filesystem"
|
|
14
|
-
import { InstanceRef } from "@/effect/instance-ref"
|
|
15
|
-
import { makeRuntime } from "@/effect/run-service"
|
|
16
|
-
import type { InstanceContext } from "@/project/instance-context"
|
|
17
|
-
|
|
18
|
-
const DIAGNOSTICS_DEBOUNCE_MS = 150
|
|
19
|
-
const DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS = 5_000
|
|
20
|
-
const DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS = 10_000
|
|
21
|
-
const DIAGNOSTICS_REQUEST_TIMEOUT_MS = 3_000
|
|
22
|
-
|
|
23
|
-
const INITIALIZE_TIMEOUT_MS = 45_000
|
|
24
|
-
|
|
25
|
-
// LSP spec constants
|
|
26
|
-
const FILE_CHANGE_CREATED = 1
|
|
27
|
-
const FILE_CHANGE_CHANGED = 2
|
|
28
|
-
const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2
|
|
29
|
-
|
|
30
|
-
const log = Log.create({ service: "lsp.client" })
|
|
31
|
-
const busRuntime = makeRuntime(Bus.Service, Bus.layer)
|
|
32
|
-
|
|
33
|
-
export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
|
|
34
|
-
|
|
35
|
-
export type Diagnostic = VSCodeDiagnostic
|
|
36
|
-
|
|
37
|
-
export class InitializeError extends Schema.TaggedErrorClass<InitializeError>()("LSPInitializeError", {
|
|
38
|
-
serverID: Schema.String,
|
|
39
|
-
cause: Schema.optional(Schema.Defect),
|
|
40
|
-
}) {}
|
|
41
|
-
|
|
42
|
-
export const Event = {
|
|
43
|
-
Diagnostics: BusEvent.define(
|
|
44
|
-
"lsp.client.diagnostics",
|
|
45
|
-
Schema.Struct({
|
|
46
|
-
serverID: Schema.String,
|
|
47
|
-
path: Schema.String,
|
|
48
|
-
}),
|
|
49
|
-
),
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type DocumentDiagnosticReport = {
|
|
53
|
-
items?: Diagnostic[]
|
|
54
|
-
relatedDocuments?: Record<string, DocumentDiagnosticReport>
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type WorkspaceDiagnosticReport = {
|
|
58
|
-
items?: {
|
|
59
|
-
uri?: string
|
|
60
|
-
items?: Diagnostic[]
|
|
61
|
-
}[]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
type DiagnosticRequestResult = {
|
|
65
|
-
handled: boolean
|
|
66
|
-
matched: boolean
|
|
67
|
-
byFile: Map<string, Diagnostic[]>
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
type CapabilityRegistration = {
|
|
71
|
-
id: string
|
|
72
|
-
method: string
|
|
73
|
-
registerOptions?: {
|
|
74
|
-
identifier?: string
|
|
75
|
-
workspaceDiagnostics?: boolean
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
type ServerCapabilities = {
|
|
80
|
-
textDocumentSync?:
|
|
81
|
-
| number
|
|
82
|
-
| {
|
|
83
|
-
change?: number
|
|
84
|
-
}
|
|
85
|
-
diagnosticProvider?: unknown
|
|
86
|
-
[key: string]: unknown
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function getFilePath(uri: string) {
|
|
90
|
-
if (!uri.startsWith("file://")) return
|
|
91
|
-
return Filesystem.normalizePath(fileURLToPath(uri))
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getSyncKind(capabilities?: ServerCapabilities) {
|
|
95
|
-
if (!capabilities) return
|
|
96
|
-
const sync = capabilities.textDocumentSync
|
|
97
|
-
if (typeof sync === "number") return sync
|
|
98
|
-
return sync?.change
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function endPosition(text: string) {
|
|
102
|
-
const lines = text.split(/\r\n|\r|\n/)
|
|
103
|
-
return {
|
|
104
|
-
line: lines.length - 1,
|
|
105
|
-
character: lines.at(-1)?.length ?? 0,
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function dedupeDiagnostics(items: Diagnostic[]) {
|
|
110
|
-
const seen = new Set<string>()
|
|
111
|
-
return items.filter((item) => {
|
|
112
|
-
const key = JSON.stringify({
|
|
113
|
-
code: item.code,
|
|
114
|
-
severity: item.severity,
|
|
115
|
-
message: item.message,
|
|
116
|
-
source: item.source,
|
|
117
|
-
range: item.range,
|
|
118
|
-
})
|
|
119
|
-
if (seen.has(key)) return false
|
|
120
|
-
seen.add(key)
|
|
121
|
-
return true
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function configurationValue(settings: unknown, section?: string) {
|
|
126
|
-
if (!section) return settings ?? null
|
|
127
|
-
const result = section.split(".").reduce<unknown>((acc, key) => {
|
|
128
|
-
if (!acc || typeof acc !== "object" || !(key in acc)) return undefined
|
|
129
|
-
return (acc as Record<string, unknown>)[key]
|
|
130
|
-
}, settings)
|
|
131
|
-
return result ?? null
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// TypeScript's built-in LSP pushes diagnostics aggressively on first open.
|
|
135
|
-
// We seed the push cache on the very first publish so waitForFreshPush can
|
|
136
|
-
// resolve immediately instead of waiting for a second debounced push.
|
|
137
|
-
function shouldSeedDiagnosticsOnFirstPush(serverID: string) {
|
|
138
|
-
return serverID === "typescript"
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function create(input: {
|
|
142
|
-
serverID: string
|
|
143
|
-
server: LSPServer.Handle
|
|
144
|
-
root: string
|
|
145
|
-
directory: string
|
|
146
|
-
instance: InstanceContext
|
|
147
|
-
}) {
|
|
148
|
-
const logger = log.clone().tag("serverID", input.serverID)
|
|
149
|
-
logger.info("starting client")
|
|
150
|
-
const instance = input.instance
|
|
151
|
-
|
|
152
|
-
const connection = createMessageConnection(
|
|
153
|
-
new StreamMessageReader(input.server.process.stdout as any),
|
|
154
|
-
new StreamMessageWriter(input.server.process.stdin as any),
|
|
155
|
-
)
|
|
156
|
-
// Server stderr can contain both real errors and routine informational logs,
|
|
157
|
-
// which is normal stderr practice for some tools. Keep the raw stream at
|
|
158
|
-
// debug so users can opt in with --print-logs --log-level DEBUG without
|
|
159
|
-
// polluting normal logs.
|
|
160
|
-
input.server.process.stderr?.on("data", (data: Buffer) => {
|
|
161
|
-
const text = data.toString().trim()
|
|
162
|
-
if (text) logger.debug("server stderr", { text: text.slice(0, 1000) })
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// --- Connection state ---
|
|
166
|
-
|
|
167
|
-
const pushDiagnostics = new Map<string, Diagnostic[]>()
|
|
168
|
-
const pullDiagnostics = new Map<string, Diagnostic[]>()
|
|
169
|
-
const published = new Map<string, { at: number; version?: number }>()
|
|
170
|
-
const diagnosticRegistrations = new Map<string, CapabilityRegistration>()
|
|
171
|
-
const registrationListeners = new Set<() => void>()
|
|
172
|
-
const mergedDiagnostics = (filePath: string) =>
|
|
173
|
-
dedupeDiagnostics([...(pushDiagnostics.get(filePath) ?? []), ...(pullDiagnostics.get(filePath) ?? [])])
|
|
174
|
-
const updatePushDiagnostics = (filePath: string, next: Diagnostic[]) => {
|
|
175
|
-
pushDiagnostics.set(filePath, next)
|
|
176
|
-
void busRuntime.runPromise((svc) =>
|
|
177
|
-
svc
|
|
178
|
-
.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
|
|
179
|
-
.pipe(Effect.provideService(InstanceRef, instance)),
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
const updatePullDiagnostics = (filePath: string, next: Diagnostic[]) => {
|
|
183
|
-
pullDiagnostics.set(filePath, next)
|
|
184
|
-
}
|
|
185
|
-
const emitRegistrationChange = () => {
|
|
186
|
-
for (const listener of [...registrationListeners]) listener()
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// --- LSP connection handlers ---
|
|
190
|
-
|
|
191
|
-
connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
192
|
-
const filePath = getFilePath(params.uri)
|
|
193
|
-
if (!filePath) return
|
|
194
|
-
logger.info("textDocument/publishDiagnostics", {
|
|
195
|
-
path: filePath,
|
|
196
|
-
count: params.diagnostics.length,
|
|
197
|
-
version: params.version,
|
|
198
|
-
})
|
|
199
|
-
published.set(filePath, {
|
|
200
|
-
at: Date.now(),
|
|
201
|
-
version: typeof params.version === "number" ? params.version : undefined,
|
|
202
|
-
})
|
|
203
|
-
if (shouldSeedDiagnosticsOnFirstPush(input.serverID) && !pushDiagnostics.has(filePath)) {
|
|
204
|
-
pushDiagnostics.set(filePath, params.diagnostics)
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
updatePushDiagnostics(filePath, params.diagnostics)
|
|
208
|
-
})
|
|
209
|
-
connection.onRequest("window/workDoneProgress/create", (params) => {
|
|
210
|
-
logger.info("window/workDoneProgress/create", params)
|
|
211
|
-
return null
|
|
212
|
-
})
|
|
213
|
-
connection.onRequest("workspace/configuration", async (params) => {
|
|
214
|
-
const items = (params as { items?: { section?: string }[] }).items ?? []
|
|
215
|
-
return items.map((item) => configurationValue(input.server.initialization, item.section))
|
|
216
|
-
})
|
|
217
|
-
connection.onRequest("client/registerCapability", async (params) => {
|
|
218
|
-
const registrations = (params as { registrations?: CapabilityRegistration[] }).registrations ?? []
|
|
219
|
-
let changed = false
|
|
220
|
-
for (const registration of registrations) {
|
|
221
|
-
if (registration.method !== "textDocument/diagnostic") continue
|
|
222
|
-
diagnosticRegistrations.set(registration.id, registration)
|
|
223
|
-
changed = true
|
|
224
|
-
}
|
|
225
|
-
if (changed) emitRegistrationChange()
|
|
226
|
-
})
|
|
227
|
-
connection.onRequest("client/unregisterCapability", async (params) => {
|
|
228
|
-
const registrations = (params as { unregisterations?: { id: string; method: string }[] }).unregisterations ?? []
|
|
229
|
-
let changed = false
|
|
230
|
-
for (const registration of registrations) {
|
|
231
|
-
if (registration.method !== "textDocument/diagnostic") continue
|
|
232
|
-
diagnosticRegistrations.delete(registration.id)
|
|
233
|
-
changed = true
|
|
234
|
-
}
|
|
235
|
-
if (changed) emitRegistrationChange()
|
|
236
|
-
})
|
|
237
|
-
connection.onRequest("workspace/workspaceFolders", async () => [
|
|
238
|
-
{
|
|
239
|
-
name: "workspace",
|
|
240
|
-
uri: pathToFileURL(input.root).href,
|
|
241
|
-
},
|
|
242
|
-
])
|
|
243
|
-
connection.onRequest("workspace/diagnostic/refresh", async () => null)
|
|
244
|
-
connection.listen()
|
|
245
|
-
|
|
246
|
-
// --- Initialize handshake ---
|
|
247
|
-
|
|
248
|
-
logger.info("sending initialize")
|
|
249
|
-
const initialized = await withTimeout(
|
|
250
|
-
connection.sendRequest<{ capabilities?: ServerCapabilities }>("initialize", {
|
|
251
|
-
rootUri: pathToFileURL(input.root).href,
|
|
252
|
-
processId: input.server.process.pid,
|
|
253
|
-
workspaceFolders: [
|
|
254
|
-
{
|
|
255
|
-
name: "workspace",
|
|
256
|
-
uri: pathToFileURL(input.root).href,
|
|
257
|
-
},
|
|
258
|
-
],
|
|
259
|
-
initializationOptions: {
|
|
260
|
-
...input.server.initialization,
|
|
261
|
-
},
|
|
262
|
-
capabilities: {
|
|
263
|
-
window: {
|
|
264
|
-
workDoneProgress: true,
|
|
265
|
-
},
|
|
266
|
-
workspace: {
|
|
267
|
-
configuration: true,
|
|
268
|
-
didChangeWatchedFiles: {
|
|
269
|
-
dynamicRegistration: true,
|
|
270
|
-
},
|
|
271
|
-
diagnostics: {
|
|
272
|
-
refreshSupport: false,
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
textDocument: {
|
|
276
|
-
synchronization: {
|
|
277
|
-
didOpen: true,
|
|
278
|
-
didChange: true,
|
|
279
|
-
},
|
|
280
|
-
diagnostic: {
|
|
281
|
-
dynamicRegistration: true,
|
|
282
|
-
relatedDocumentSupport: true,
|
|
283
|
-
},
|
|
284
|
-
publishDiagnostics: {
|
|
285
|
-
versionSupport: false,
|
|
286
|
-
},
|
|
287
|
-
},
|
|
288
|
-
},
|
|
289
|
-
}),
|
|
290
|
-
INITIALIZE_TIMEOUT_MS,
|
|
291
|
-
).catch((err) => {
|
|
292
|
-
logger.error("initialize error", { error: err })
|
|
293
|
-
throw new InitializeError({ serverID: input.serverID, cause: err })
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
const syncKind = getSyncKind(initialized.capabilities)
|
|
297
|
-
const hasStaticPullDiagnostics = Boolean(initialized.capabilities?.diagnosticProvider)
|
|
298
|
-
|
|
299
|
-
await connection.sendNotification("initialized", {})
|
|
300
|
-
|
|
301
|
-
if (input.server.initialization) {
|
|
302
|
-
await connection.sendNotification("workspace/didChangeConfiguration", {
|
|
303
|
-
settings: input.server.initialization,
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const files: Record<string, { version: number; text: string }> = {}
|
|
308
|
-
|
|
309
|
-
// --- Diagnostic helpers ---
|
|
310
|
-
|
|
311
|
-
const mergeResults = (filePath: string, results: DiagnosticRequestResult[]) => {
|
|
312
|
-
const handled = results.some((result) => result.handled)
|
|
313
|
-
const matched = results.some((result) => result.matched)
|
|
314
|
-
if (!handled) return { handled: false, matched: false }
|
|
315
|
-
|
|
316
|
-
const merged = new Map<string, Diagnostic[]>()
|
|
317
|
-
for (const result of results) {
|
|
318
|
-
for (const [target, items] of result.byFile.entries()) {
|
|
319
|
-
const existing = merged.get(target) ?? []
|
|
320
|
-
merged.set(target, existing.concat(items))
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (matched && !merged.has(filePath)) merged.set(filePath, [])
|
|
325
|
-
for (const [target, items] of merged.entries()) {
|
|
326
|
-
updatePullDiagnostics(target, dedupeDiagnostics(items))
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return { handled, matched }
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function requestDiagnosticReport(filePath: string, identifier?: string): Promise<DiagnosticRequestResult> {
|
|
333
|
-
const report = await withTimeout(
|
|
334
|
-
connection.sendRequest<DocumentDiagnosticReport | null>("textDocument/diagnostic", {
|
|
335
|
-
...(identifier ? { identifier } : {}),
|
|
336
|
-
textDocument: {
|
|
337
|
-
uri: pathToFileURL(filePath).href,
|
|
338
|
-
},
|
|
339
|
-
}),
|
|
340
|
-
DIAGNOSTICS_REQUEST_TIMEOUT_MS,
|
|
341
|
-
).catch(() => null)
|
|
342
|
-
if (!report) return { handled: false, matched: false, byFile: new Map<string, Diagnostic[]>() }
|
|
343
|
-
|
|
344
|
-
const byFile = new Map<string, Diagnostic[]>()
|
|
345
|
-
const push = (target: string, items: Diagnostic[]) => {
|
|
346
|
-
const existing = byFile.get(target) ?? []
|
|
347
|
-
byFile.set(target, existing.concat(items))
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
let handled = false
|
|
351
|
-
let matched = false
|
|
352
|
-
if (Array.isArray(report.items)) {
|
|
353
|
-
push(filePath, report.items)
|
|
354
|
-
handled = true
|
|
355
|
-
matched = true
|
|
356
|
-
}
|
|
357
|
-
for (const [uri, related] of Object.entries(report.relatedDocuments ?? {})) {
|
|
358
|
-
const relatedPath = getFilePath(uri)
|
|
359
|
-
if (!relatedPath || !Array.isArray(related.items)) continue
|
|
360
|
-
push(relatedPath, related.items)
|
|
361
|
-
handled = true
|
|
362
|
-
matched = matched || relatedPath === filePath
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return { handled, matched, byFile }
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async function requestWorkspaceDiagnosticReport(
|
|
369
|
-
filePath: string,
|
|
370
|
-
identifier?: string,
|
|
371
|
-
): Promise<DiagnosticRequestResult> {
|
|
372
|
-
const report = await withTimeout(
|
|
373
|
-
connection.sendRequest<WorkspaceDiagnosticReport | null>("workspace/diagnostic", {
|
|
374
|
-
...(identifier ? { identifier } : {}),
|
|
375
|
-
previousResultIds: [],
|
|
376
|
-
}),
|
|
377
|
-
DIAGNOSTICS_REQUEST_TIMEOUT_MS,
|
|
378
|
-
).catch(() => null)
|
|
379
|
-
if (!report) return { handled: false, matched: false, byFile: new Map<string, Diagnostic[]>() }
|
|
380
|
-
|
|
381
|
-
const byFile = new Map<string, Diagnostic[]>()
|
|
382
|
-
let matched = false
|
|
383
|
-
for (const item of report.items ?? []) {
|
|
384
|
-
const relatedPath = item.uri ? getFilePath(item.uri) : undefined
|
|
385
|
-
if (!relatedPath || !Array.isArray(item.items)) continue
|
|
386
|
-
const existing = byFile.get(relatedPath) ?? []
|
|
387
|
-
byFile.set(relatedPath, existing.concat(item.items))
|
|
388
|
-
matched = matched || relatedPath === filePath
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return { handled: true, matched, byFile }
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function documentPullState() {
|
|
395
|
-
const documentRegistrations = [...diagnosticRegistrations.values()].filter(
|
|
396
|
-
(registration) => registration.registerOptions?.workspaceDiagnostics !== true,
|
|
397
|
-
)
|
|
398
|
-
return {
|
|
399
|
-
documentIdentifiers: [
|
|
400
|
-
...new Set(documentRegistrations.flatMap((registration) => registration.registerOptions?.identifier ?? [])),
|
|
401
|
-
],
|
|
402
|
-
supported: hasStaticPullDiagnostics || documentRegistrations.length > 0,
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function workspacePullState() {
|
|
407
|
-
const workspaceRegistrations = [...diagnosticRegistrations.values()].filter(
|
|
408
|
-
(registration) => registration.registerOptions?.workspaceDiagnostics === true,
|
|
409
|
-
)
|
|
410
|
-
return {
|
|
411
|
-
workspaceIdentifiers: [
|
|
412
|
-
...new Set(workspaceRegistrations.flatMap((registration) => registration.registerOptions?.identifier ?? [])),
|
|
413
|
-
],
|
|
414
|
-
supported: workspaceRegistrations.length > 0,
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const hasCurrentFileDiagnostics = (filePath: string, results: DiagnosticRequestResult[]) =>
|
|
419
|
-
results.some((result) => (result.byFile.get(filePath)?.length ?? 0) > 0)
|
|
420
|
-
|
|
421
|
-
async function requestDiagnostics(
|
|
422
|
-
filePath: string,
|
|
423
|
-
requests: Promise<DiagnosticRequestResult>[],
|
|
424
|
-
done: (results: DiagnosticRequestResult[]) => boolean,
|
|
425
|
-
) {
|
|
426
|
-
if (!requests.length) return { handled: false, matched: false }
|
|
427
|
-
|
|
428
|
-
const results: DiagnosticRequestResult[] = []
|
|
429
|
-
return new Promise<{ handled: boolean; matched: boolean }>((resolve) => {
|
|
430
|
-
let pending = requests.length
|
|
431
|
-
let resolved = false
|
|
432
|
-
const finish = (merged: { handled: boolean; matched: boolean }, force = false) => {
|
|
433
|
-
if (resolved) return
|
|
434
|
-
if (!force && !done(results)) return
|
|
435
|
-
resolved = true
|
|
436
|
-
resolve(merged)
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
for (const request of requests) {
|
|
440
|
-
request.then((result) => {
|
|
441
|
-
results.push(result)
|
|
442
|
-
pending -= 1
|
|
443
|
-
const merged = mergeResults(filePath, results)
|
|
444
|
-
finish(merged)
|
|
445
|
-
if (pending === 0) finish(merged, true)
|
|
446
|
-
})
|
|
447
|
-
}
|
|
448
|
-
})
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// LATENCY-CRITICAL: dispatch identifier pulls in parallel and unblock once one
|
|
452
|
-
// batch already produced diagnostics for the current file. Let slower pulls keep
|
|
453
|
-
// merging in the background; do not sequence identifier-by-identifier, and do
|
|
454
|
-
// not add a post-match settle/debounce delay. See PR #23771.
|
|
455
|
-
async function requestDocumentDiagnostics(filePath: string) {
|
|
456
|
-
const state = documentPullState()
|
|
457
|
-
if (!state.supported) return { handled: false, matched: false }
|
|
458
|
-
return requestDiagnostics(
|
|
459
|
-
filePath,
|
|
460
|
-
[
|
|
461
|
-
requestDiagnosticReport(filePath),
|
|
462
|
-
...state.documentIdentifiers.map((identifier) => requestDiagnosticReport(filePath, identifier)),
|
|
463
|
-
],
|
|
464
|
-
(results) => hasCurrentFileDiagnostics(filePath, results),
|
|
465
|
-
)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async function requestFullDiagnostics(filePath: string) {
|
|
469
|
-
const documentState = documentPullState()
|
|
470
|
-
const workspaceState = workspacePullState()
|
|
471
|
-
if (!documentState.supported && !workspaceState.supported) return { handled: false, matched: false }
|
|
472
|
-
return mergeResults(
|
|
473
|
-
filePath,
|
|
474
|
-
await Promise.all([
|
|
475
|
-
...(documentState.supported ? [requestDiagnosticReport(filePath)] : []),
|
|
476
|
-
...documentState.documentIdentifiers.map((identifier) => requestDiagnosticReport(filePath, identifier)),
|
|
477
|
-
...(workspaceState.supported ? [requestWorkspaceDiagnosticReport(filePath)] : []),
|
|
478
|
-
...workspaceState.workspaceIdentifiers.map((identifier) =>
|
|
479
|
-
requestWorkspaceDiagnosticReport(filePath, identifier),
|
|
480
|
-
),
|
|
481
|
-
]),
|
|
482
|
-
)
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function waitForRegistrationChange(timeout: number) {
|
|
486
|
-
if (timeout <= 0) return Promise.resolve(false)
|
|
487
|
-
return new Promise<boolean>((resolve) => {
|
|
488
|
-
let finished = false
|
|
489
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
490
|
-
const finish = (result: boolean) => {
|
|
491
|
-
if (finished) return
|
|
492
|
-
finished = true
|
|
493
|
-
if (timer) clearTimeout(timer)
|
|
494
|
-
registrationListeners.delete(listener)
|
|
495
|
-
resolve(result)
|
|
496
|
-
}
|
|
497
|
-
const listener = () => finish(true)
|
|
498
|
-
registrationListeners.add(listener)
|
|
499
|
-
timer = setTimeout(() => finish(false), timeout)
|
|
500
|
-
})
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function waitForFreshPush(request: { path: string; version: number; after: number; timeout: number }) {
|
|
504
|
-
if (request.timeout <= 0) return Promise.resolve(false)
|
|
505
|
-
return new Promise<boolean>((resolve) => {
|
|
506
|
-
let finished = false
|
|
507
|
-
let debounceTimer: ReturnType<typeof setTimeout> | undefined
|
|
508
|
-
let timeoutTimer: ReturnType<typeof setTimeout> | undefined
|
|
509
|
-
let unsub: (() => void) | undefined
|
|
510
|
-
const finish = (result: boolean) => {
|
|
511
|
-
if (finished) return
|
|
512
|
-
finished = true
|
|
513
|
-
if (debounceTimer) clearTimeout(debounceTimer)
|
|
514
|
-
if (timeoutTimer) clearTimeout(timeoutTimer)
|
|
515
|
-
unsub?.()
|
|
516
|
-
resolve(result)
|
|
517
|
-
}
|
|
518
|
-
const schedule = () => {
|
|
519
|
-
const hit = published.get(request.path)
|
|
520
|
-
if (!hit) return
|
|
521
|
-
if (typeof hit.version === "number" && hit.version !== request.version) return
|
|
522
|
-
if (hit.at < request.after && hit.version !== request.version) return
|
|
523
|
-
if (debounceTimer) clearTimeout(debounceTimer)
|
|
524
|
-
debounceTimer = setTimeout(() => finish(true), Math.max(0, DIAGNOSTICS_DEBOUNCE_MS - (Date.now() - hit.at)))
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
timeoutTimer = setTimeout(() => finish(false), request.timeout)
|
|
528
|
-
unsub = busRuntime.runSync((svc) =>
|
|
529
|
-
svc
|
|
530
|
-
.subscribeCallback(Event.Diagnostics, (event) => {
|
|
531
|
-
if (event.properties.path !== request.path || event.properties.serverID !== input.serverID) return
|
|
532
|
-
schedule()
|
|
533
|
-
})
|
|
534
|
-
.pipe(Effect.provideService(InstanceRef, instance)),
|
|
535
|
-
)
|
|
536
|
-
schedule()
|
|
537
|
-
})
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
async function waitForDocumentDiagnostics(request: { path: string; version: number; after?: number }) {
|
|
541
|
-
const startedAt = request.after ?? Date.now()
|
|
542
|
-
const pushWait = waitForFreshPush({
|
|
543
|
-
path: request.path,
|
|
544
|
-
version: request.version,
|
|
545
|
-
after: startedAt,
|
|
546
|
-
timeout: DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS,
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
while (Date.now() - startedAt < DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS) {
|
|
550
|
-
const result = await requestDocumentDiagnostics(request.path)
|
|
551
|
-
if (result.matched) return
|
|
552
|
-
const remaining = DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS - (Date.now() - startedAt)
|
|
553
|
-
if (remaining <= 0) return
|
|
554
|
-
const next = await Promise.race([
|
|
555
|
-
pushWait.then((ready) => (ready ? "push" : ("timeout" as const))),
|
|
556
|
-
waitForRegistrationChange(remaining).then((changed) => (changed ? "registration" : ("timeout" as const))),
|
|
557
|
-
])
|
|
558
|
-
if (next !== "registration") return
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
async function waitForFullDiagnostics(request: { path: string; version: number; after?: number }) {
|
|
563
|
-
const startedAt = request.after ?? Date.now()
|
|
564
|
-
const pushWait = waitForFreshPush({
|
|
565
|
-
path: request.path,
|
|
566
|
-
version: request.version,
|
|
567
|
-
after: startedAt,
|
|
568
|
-
timeout: DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS,
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
while (Date.now() - startedAt < DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS) {
|
|
572
|
-
const result = await requestFullDiagnostics(request.path)
|
|
573
|
-
if (result.handled || result.matched) return
|
|
574
|
-
const remaining = DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS - (Date.now() - startedAt)
|
|
575
|
-
if (remaining <= 0) return
|
|
576
|
-
const next = await Promise.race([
|
|
577
|
-
pushWait.then((ready) => (ready ? "push" : ("timeout" as const))),
|
|
578
|
-
waitForRegistrationChange(remaining).then((changed) => (changed ? "registration" : ("timeout" as const))),
|
|
579
|
-
])
|
|
580
|
-
if (next !== "registration") return
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// --- Public API ---
|
|
585
|
-
|
|
586
|
-
const result = {
|
|
587
|
-
root: input.root,
|
|
588
|
-
get serverID() {
|
|
589
|
-
return input.serverID
|
|
590
|
-
},
|
|
591
|
-
get connection() {
|
|
592
|
-
return connection
|
|
593
|
-
},
|
|
594
|
-
notify: {
|
|
595
|
-
async open(request: { path: string }) {
|
|
596
|
-
request.path = Filesystem.normalizePath(
|
|
597
|
-
path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path),
|
|
598
|
-
)
|
|
599
|
-
const text = await Filesystem.readText(request.path)
|
|
600
|
-
const extension = path.extname(request.path)
|
|
601
|
-
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
|
602
|
-
|
|
603
|
-
const document = files[request.path]
|
|
604
|
-
if (document !== undefined) {
|
|
605
|
-
// Do not wipe diagnostics on didChange. Some servers (e.g. clangd) only
|
|
606
|
-
// re-emit diagnostics when the content actually changes, so clearing
|
|
607
|
-
// here would lose errors for no-op touchFile calls. Let the server's
|
|
608
|
-
// next push/pull overwrite naturally.
|
|
609
|
-
logger.info("workspace/didChangeWatchedFiles", request)
|
|
610
|
-
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
|
611
|
-
changes: [
|
|
612
|
-
{
|
|
613
|
-
uri: pathToFileURL(request.path).href,
|
|
614
|
-
type: FILE_CHANGE_CHANGED,
|
|
615
|
-
},
|
|
616
|
-
],
|
|
617
|
-
})
|
|
618
|
-
|
|
619
|
-
const next = document.version + 1
|
|
620
|
-
files[request.path] = { version: next, text }
|
|
621
|
-
logger.info("textDocument/didChange", {
|
|
622
|
-
path: request.path,
|
|
623
|
-
version: next,
|
|
624
|
-
})
|
|
625
|
-
await connection.sendNotification("textDocument/didChange", {
|
|
626
|
-
textDocument: {
|
|
627
|
-
uri: pathToFileURL(request.path).href,
|
|
628
|
-
version: next,
|
|
629
|
-
},
|
|
630
|
-
contentChanges:
|
|
631
|
-
syncKind === TEXT_DOCUMENT_SYNC_INCREMENTAL
|
|
632
|
-
? [
|
|
633
|
-
{
|
|
634
|
-
range: {
|
|
635
|
-
start: { line: 0, character: 0 },
|
|
636
|
-
end: endPosition(document.text),
|
|
637
|
-
},
|
|
638
|
-
text,
|
|
639
|
-
},
|
|
640
|
-
]
|
|
641
|
-
: [{ text }],
|
|
642
|
-
})
|
|
643
|
-
return next
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
logger.info("workspace/didChangeWatchedFiles", request)
|
|
647
|
-
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
|
648
|
-
changes: [
|
|
649
|
-
{
|
|
650
|
-
uri: pathToFileURL(request.path).href,
|
|
651
|
-
type: FILE_CHANGE_CREATED,
|
|
652
|
-
},
|
|
653
|
-
],
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
logger.info("textDocument/didOpen", request)
|
|
657
|
-
pushDiagnostics.delete(request.path)
|
|
658
|
-
pullDiagnostics.delete(request.path)
|
|
659
|
-
await connection.sendNotification("textDocument/didOpen", {
|
|
660
|
-
textDocument: {
|
|
661
|
-
uri: pathToFileURL(request.path).href,
|
|
662
|
-
languageId,
|
|
663
|
-
version: 0,
|
|
664
|
-
text,
|
|
665
|
-
},
|
|
666
|
-
})
|
|
667
|
-
files[request.path] = { version: 0, text }
|
|
668
|
-
return 0
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
get diagnostics() {
|
|
672
|
-
const result = new Map<string, Diagnostic[]>()
|
|
673
|
-
for (const key of new Set([...pushDiagnostics.keys(), ...pullDiagnostics.keys()])) {
|
|
674
|
-
result.set(key, mergedDiagnostics(key))
|
|
675
|
-
}
|
|
676
|
-
return result
|
|
677
|
-
},
|
|
678
|
-
async waitForDiagnostics(request: { path: string; version: number; mode?: "document" | "full"; after?: number }) {
|
|
679
|
-
const normalizedPath = Filesystem.normalizePath(
|
|
680
|
-
path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path),
|
|
681
|
-
)
|
|
682
|
-
logger.info("waiting for diagnostics", {
|
|
683
|
-
path: normalizedPath,
|
|
684
|
-
mode: request.mode ?? "full",
|
|
685
|
-
version: request.version,
|
|
686
|
-
})
|
|
687
|
-
if (request.mode === "document") {
|
|
688
|
-
await waitForDocumentDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
|
|
689
|
-
return
|
|
690
|
-
}
|
|
691
|
-
await waitForFullDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
|
|
692
|
-
},
|
|
693
|
-
async shutdown() {
|
|
694
|
-
logger.info("shutting down")
|
|
695
|
-
connection.end()
|
|
696
|
-
connection.dispose()
|
|
697
|
-
await Process.stop(input.server.process)
|
|
698
|
-
logger.info("shutdown")
|
|
699
|
-
},
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
logger.info("initialized")
|
|
703
|
-
|
|
704
|
-
return result
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
export * as LSPClient from "./client"
|
|
1
|
+
import { BusEvent } from "@/bus/bus-event"
|
|
2
|
+
import { Bus } from "@/bus"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import { pathToFileURL, fileURLToPath } from "url"
|
|
5
|
+
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
|
|
6
|
+
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
|
|
7
|
+
import * as Log from "@opencode-ai/core/util/log"
|
|
8
|
+
import { Process } from "@/util/process"
|
|
9
|
+
import { LANGUAGE_EXTENSIONS } from "./language"
|
|
10
|
+
import { Effect, Schema } from "effect"
|
|
11
|
+
import type * as LSPServer from "./server"
|
|
12
|
+
import { withTimeout } from "../util/timeout"
|
|
13
|
+
import { Filesystem } from "@/util/filesystem"
|
|
14
|
+
import { InstanceRef } from "@/effect/instance-ref"
|
|
15
|
+
import { makeRuntime } from "@/effect/run-service"
|
|
16
|
+
import type { InstanceContext } from "@/project/instance-context"
|
|
17
|
+
|
|
18
|
+
const DIAGNOSTICS_DEBOUNCE_MS = 150
|
|
19
|
+
const DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS = 5_000
|
|
20
|
+
const DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS = 10_000
|
|
21
|
+
const DIAGNOSTICS_REQUEST_TIMEOUT_MS = 3_000
|
|
22
|
+
|
|
23
|
+
const INITIALIZE_TIMEOUT_MS = 45_000
|
|
24
|
+
|
|
25
|
+
// LSP spec constants
|
|
26
|
+
const FILE_CHANGE_CREATED = 1
|
|
27
|
+
const FILE_CHANGE_CHANGED = 2
|
|
28
|
+
const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2
|
|
29
|
+
|
|
30
|
+
const log = Log.create({ service: "lsp.client" })
|
|
31
|
+
const busRuntime = makeRuntime(Bus.Service, Bus.layer)
|
|
32
|
+
|
|
33
|
+
export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
|
|
34
|
+
|
|
35
|
+
export type Diagnostic = VSCodeDiagnostic
|
|
36
|
+
|
|
37
|
+
export class InitializeError extends Schema.TaggedErrorClass<InitializeError>()("LSPInitializeError", {
|
|
38
|
+
serverID: Schema.String,
|
|
39
|
+
cause: Schema.optional(Schema.Defect),
|
|
40
|
+
}) {}
|
|
41
|
+
|
|
42
|
+
export const Event = {
|
|
43
|
+
Diagnostics: BusEvent.define(
|
|
44
|
+
"lsp.client.diagnostics",
|
|
45
|
+
Schema.Struct({
|
|
46
|
+
serverID: Schema.String,
|
|
47
|
+
path: Schema.String,
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type DocumentDiagnosticReport = {
|
|
53
|
+
items?: Diagnostic[]
|
|
54
|
+
relatedDocuments?: Record<string, DocumentDiagnosticReport>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type WorkspaceDiagnosticReport = {
|
|
58
|
+
items?: {
|
|
59
|
+
uri?: string
|
|
60
|
+
items?: Diagnostic[]
|
|
61
|
+
}[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type DiagnosticRequestResult = {
|
|
65
|
+
handled: boolean
|
|
66
|
+
matched: boolean
|
|
67
|
+
byFile: Map<string, Diagnostic[]>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type CapabilityRegistration = {
|
|
71
|
+
id: string
|
|
72
|
+
method: string
|
|
73
|
+
registerOptions?: {
|
|
74
|
+
identifier?: string
|
|
75
|
+
workspaceDiagnostics?: boolean
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type ServerCapabilities = {
|
|
80
|
+
textDocumentSync?:
|
|
81
|
+
| number
|
|
82
|
+
| {
|
|
83
|
+
change?: number
|
|
84
|
+
}
|
|
85
|
+
diagnosticProvider?: unknown
|
|
86
|
+
[key: string]: unknown
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getFilePath(uri: string) {
|
|
90
|
+
if (!uri.startsWith("file://")) return
|
|
91
|
+
return Filesystem.normalizePath(fileURLToPath(uri))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getSyncKind(capabilities?: ServerCapabilities) {
|
|
95
|
+
if (!capabilities) return
|
|
96
|
+
const sync = capabilities.textDocumentSync
|
|
97
|
+
if (typeof sync === "number") return sync
|
|
98
|
+
return sync?.change
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function endPosition(text: string) {
|
|
102
|
+
const lines = text.split(/\r\n|\r|\n/)
|
|
103
|
+
return {
|
|
104
|
+
line: lines.length - 1,
|
|
105
|
+
character: lines.at(-1)?.length ?? 0,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function dedupeDiagnostics(items: Diagnostic[]) {
|
|
110
|
+
const seen = new Set<string>()
|
|
111
|
+
return items.filter((item) => {
|
|
112
|
+
const key = JSON.stringify({
|
|
113
|
+
code: item.code,
|
|
114
|
+
severity: item.severity,
|
|
115
|
+
message: item.message,
|
|
116
|
+
source: item.source,
|
|
117
|
+
range: item.range,
|
|
118
|
+
})
|
|
119
|
+
if (seen.has(key)) return false
|
|
120
|
+
seen.add(key)
|
|
121
|
+
return true
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function configurationValue(settings: unknown, section?: string) {
|
|
126
|
+
if (!section) return settings ?? null
|
|
127
|
+
const result = section.split(".").reduce<unknown>((acc, key) => {
|
|
128
|
+
if (!acc || typeof acc !== "object" || !(key in acc)) return undefined
|
|
129
|
+
return (acc as Record<string, unknown>)[key]
|
|
130
|
+
}, settings)
|
|
131
|
+
return result ?? null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// TypeScript's built-in LSP pushes diagnostics aggressively on first open.
|
|
135
|
+
// We seed the push cache on the very first publish so waitForFreshPush can
|
|
136
|
+
// resolve immediately instead of waiting for a second debounced push.
|
|
137
|
+
function shouldSeedDiagnosticsOnFirstPush(serverID: string) {
|
|
138
|
+
return serverID === "typescript"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function create(input: {
|
|
142
|
+
serverID: string
|
|
143
|
+
server: LSPServer.Handle
|
|
144
|
+
root: string
|
|
145
|
+
directory: string
|
|
146
|
+
instance: InstanceContext
|
|
147
|
+
}) {
|
|
148
|
+
const logger = log.clone().tag("serverID", input.serverID)
|
|
149
|
+
logger.info("starting client")
|
|
150
|
+
const instance = input.instance
|
|
151
|
+
|
|
152
|
+
const connection = createMessageConnection(
|
|
153
|
+
new StreamMessageReader(input.server.process.stdout as any),
|
|
154
|
+
new StreamMessageWriter(input.server.process.stdin as any),
|
|
155
|
+
)
|
|
156
|
+
// Server stderr can contain both real errors and routine informational logs,
|
|
157
|
+
// which is normal stderr practice for some tools. Keep the raw stream at
|
|
158
|
+
// debug so users can opt in with --print-logs --log-level DEBUG without
|
|
159
|
+
// polluting normal logs.
|
|
160
|
+
input.server.process.stderr?.on("data", (data: Buffer) => {
|
|
161
|
+
const text = data.toString().trim()
|
|
162
|
+
if (text) logger.debug("server stderr", { text: text.slice(0, 1000) })
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// --- Connection state ---
|
|
166
|
+
|
|
167
|
+
const pushDiagnostics = new Map<string, Diagnostic[]>()
|
|
168
|
+
const pullDiagnostics = new Map<string, Diagnostic[]>()
|
|
169
|
+
const published = new Map<string, { at: number; version?: number }>()
|
|
170
|
+
const diagnosticRegistrations = new Map<string, CapabilityRegistration>()
|
|
171
|
+
const registrationListeners = new Set<() => void>()
|
|
172
|
+
const mergedDiagnostics = (filePath: string) =>
|
|
173
|
+
dedupeDiagnostics([...(pushDiagnostics.get(filePath) ?? []), ...(pullDiagnostics.get(filePath) ?? [])])
|
|
174
|
+
const updatePushDiagnostics = (filePath: string, next: Diagnostic[]) => {
|
|
175
|
+
pushDiagnostics.set(filePath, next)
|
|
176
|
+
void busRuntime.runPromise((svc) =>
|
|
177
|
+
svc
|
|
178
|
+
.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
|
|
179
|
+
.pipe(Effect.provideService(InstanceRef, instance)),
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
const updatePullDiagnostics = (filePath: string, next: Diagnostic[]) => {
|
|
183
|
+
pullDiagnostics.set(filePath, next)
|
|
184
|
+
}
|
|
185
|
+
const emitRegistrationChange = () => {
|
|
186
|
+
for (const listener of [...registrationListeners]) listener()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- LSP connection handlers ---
|
|
190
|
+
|
|
191
|
+
connection.onNotification("textDocument/publishDiagnostics", (params) => {
|
|
192
|
+
const filePath = getFilePath(params.uri)
|
|
193
|
+
if (!filePath) return
|
|
194
|
+
logger.info("textDocument/publishDiagnostics", {
|
|
195
|
+
path: filePath,
|
|
196
|
+
count: params.diagnostics.length,
|
|
197
|
+
version: params.version,
|
|
198
|
+
})
|
|
199
|
+
published.set(filePath, {
|
|
200
|
+
at: Date.now(),
|
|
201
|
+
version: typeof params.version === "number" ? params.version : undefined,
|
|
202
|
+
})
|
|
203
|
+
if (shouldSeedDiagnosticsOnFirstPush(input.serverID) && !pushDiagnostics.has(filePath)) {
|
|
204
|
+
pushDiagnostics.set(filePath, params.diagnostics)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
updatePushDiagnostics(filePath, params.diagnostics)
|
|
208
|
+
})
|
|
209
|
+
connection.onRequest("window/workDoneProgress/create", (params) => {
|
|
210
|
+
logger.info("window/workDoneProgress/create", params)
|
|
211
|
+
return null
|
|
212
|
+
})
|
|
213
|
+
connection.onRequest("workspace/configuration", async (params) => {
|
|
214
|
+
const items = (params as { items?: { section?: string }[] }).items ?? []
|
|
215
|
+
return items.map((item) => configurationValue(input.server.initialization, item.section))
|
|
216
|
+
})
|
|
217
|
+
connection.onRequest("client/registerCapability", async (params) => {
|
|
218
|
+
const registrations = (params as { registrations?: CapabilityRegistration[] }).registrations ?? []
|
|
219
|
+
let changed = false
|
|
220
|
+
for (const registration of registrations) {
|
|
221
|
+
if (registration.method !== "textDocument/diagnostic") continue
|
|
222
|
+
diagnosticRegistrations.set(registration.id, registration)
|
|
223
|
+
changed = true
|
|
224
|
+
}
|
|
225
|
+
if (changed) emitRegistrationChange()
|
|
226
|
+
})
|
|
227
|
+
connection.onRequest("client/unregisterCapability", async (params) => {
|
|
228
|
+
const registrations = (params as { unregisterations?: { id: string; method: string }[] }).unregisterations ?? []
|
|
229
|
+
let changed = false
|
|
230
|
+
for (const registration of registrations) {
|
|
231
|
+
if (registration.method !== "textDocument/diagnostic") continue
|
|
232
|
+
diagnosticRegistrations.delete(registration.id)
|
|
233
|
+
changed = true
|
|
234
|
+
}
|
|
235
|
+
if (changed) emitRegistrationChange()
|
|
236
|
+
})
|
|
237
|
+
connection.onRequest("workspace/workspaceFolders", async () => [
|
|
238
|
+
{
|
|
239
|
+
name: "workspace",
|
|
240
|
+
uri: pathToFileURL(input.root).href,
|
|
241
|
+
},
|
|
242
|
+
])
|
|
243
|
+
connection.onRequest("workspace/diagnostic/refresh", async () => null)
|
|
244
|
+
connection.listen()
|
|
245
|
+
|
|
246
|
+
// --- Initialize handshake ---
|
|
247
|
+
|
|
248
|
+
logger.info("sending initialize")
|
|
249
|
+
const initialized = await withTimeout(
|
|
250
|
+
connection.sendRequest<{ capabilities?: ServerCapabilities }>("initialize", {
|
|
251
|
+
rootUri: pathToFileURL(input.root).href,
|
|
252
|
+
processId: input.server.process.pid,
|
|
253
|
+
workspaceFolders: [
|
|
254
|
+
{
|
|
255
|
+
name: "workspace",
|
|
256
|
+
uri: pathToFileURL(input.root).href,
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
initializationOptions: {
|
|
260
|
+
...input.server.initialization,
|
|
261
|
+
},
|
|
262
|
+
capabilities: {
|
|
263
|
+
window: {
|
|
264
|
+
workDoneProgress: true,
|
|
265
|
+
},
|
|
266
|
+
workspace: {
|
|
267
|
+
configuration: true,
|
|
268
|
+
didChangeWatchedFiles: {
|
|
269
|
+
dynamicRegistration: true,
|
|
270
|
+
},
|
|
271
|
+
diagnostics: {
|
|
272
|
+
refreshSupport: false,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
textDocument: {
|
|
276
|
+
synchronization: {
|
|
277
|
+
didOpen: true,
|
|
278
|
+
didChange: true,
|
|
279
|
+
},
|
|
280
|
+
diagnostic: {
|
|
281
|
+
dynamicRegistration: true,
|
|
282
|
+
relatedDocumentSupport: true,
|
|
283
|
+
},
|
|
284
|
+
publishDiagnostics: {
|
|
285
|
+
versionSupport: false,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
}),
|
|
290
|
+
INITIALIZE_TIMEOUT_MS,
|
|
291
|
+
).catch((err) => {
|
|
292
|
+
logger.error("initialize error", { error: err })
|
|
293
|
+
throw new InitializeError({ serverID: input.serverID, cause: err })
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const syncKind = getSyncKind(initialized.capabilities)
|
|
297
|
+
const hasStaticPullDiagnostics = Boolean(initialized.capabilities?.diagnosticProvider)
|
|
298
|
+
|
|
299
|
+
await connection.sendNotification("initialized", {})
|
|
300
|
+
|
|
301
|
+
if (input.server.initialization) {
|
|
302
|
+
await connection.sendNotification("workspace/didChangeConfiguration", {
|
|
303
|
+
settings: input.server.initialization,
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const files: Record<string, { version: number; text: string }> = {}
|
|
308
|
+
|
|
309
|
+
// --- Diagnostic helpers ---
|
|
310
|
+
|
|
311
|
+
const mergeResults = (filePath: string, results: DiagnosticRequestResult[]) => {
|
|
312
|
+
const handled = results.some((result) => result.handled)
|
|
313
|
+
const matched = results.some((result) => result.matched)
|
|
314
|
+
if (!handled) return { handled: false, matched: false }
|
|
315
|
+
|
|
316
|
+
const merged = new Map<string, Diagnostic[]>()
|
|
317
|
+
for (const result of results) {
|
|
318
|
+
for (const [target, items] of result.byFile.entries()) {
|
|
319
|
+
const existing = merged.get(target) ?? []
|
|
320
|
+
merged.set(target, existing.concat(items))
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (matched && !merged.has(filePath)) merged.set(filePath, [])
|
|
325
|
+
for (const [target, items] of merged.entries()) {
|
|
326
|
+
updatePullDiagnostics(target, dedupeDiagnostics(items))
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { handled, matched }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function requestDiagnosticReport(filePath: string, identifier?: string): Promise<DiagnosticRequestResult> {
|
|
333
|
+
const report = await withTimeout(
|
|
334
|
+
connection.sendRequest<DocumentDiagnosticReport | null>("textDocument/diagnostic", {
|
|
335
|
+
...(identifier ? { identifier } : {}),
|
|
336
|
+
textDocument: {
|
|
337
|
+
uri: pathToFileURL(filePath).href,
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
DIAGNOSTICS_REQUEST_TIMEOUT_MS,
|
|
341
|
+
).catch(() => null)
|
|
342
|
+
if (!report) return { handled: false, matched: false, byFile: new Map<string, Diagnostic[]>() }
|
|
343
|
+
|
|
344
|
+
const byFile = new Map<string, Diagnostic[]>()
|
|
345
|
+
const push = (target: string, items: Diagnostic[]) => {
|
|
346
|
+
const existing = byFile.get(target) ?? []
|
|
347
|
+
byFile.set(target, existing.concat(items))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let handled = false
|
|
351
|
+
let matched = false
|
|
352
|
+
if (Array.isArray(report.items)) {
|
|
353
|
+
push(filePath, report.items)
|
|
354
|
+
handled = true
|
|
355
|
+
matched = true
|
|
356
|
+
}
|
|
357
|
+
for (const [uri, related] of Object.entries(report.relatedDocuments ?? {})) {
|
|
358
|
+
const relatedPath = getFilePath(uri)
|
|
359
|
+
if (!relatedPath || !Array.isArray(related.items)) continue
|
|
360
|
+
push(relatedPath, related.items)
|
|
361
|
+
handled = true
|
|
362
|
+
matched = matched || relatedPath === filePath
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { handled, matched, byFile }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function requestWorkspaceDiagnosticReport(
|
|
369
|
+
filePath: string,
|
|
370
|
+
identifier?: string,
|
|
371
|
+
): Promise<DiagnosticRequestResult> {
|
|
372
|
+
const report = await withTimeout(
|
|
373
|
+
connection.sendRequest<WorkspaceDiagnosticReport | null>("workspace/diagnostic", {
|
|
374
|
+
...(identifier ? { identifier } : {}),
|
|
375
|
+
previousResultIds: [],
|
|
376
|
+
}),
|
|
377
|
+
DIAGNOSTICS_REQUEST_TIMEOUT_MS,
|
|
378
|
+
).catch(() => null)
|
|
379
|
+
if (!report) return { handled: false, matched: false, byFile: new Map<string, Diagnostic[]>() }
|
|
380
|
+
|
|
381
|
+
const byFile = new Map<string, Diagnostic[]>()
|
|
382
|
+
let matched = false
|
|
383
|
+
for (const item of report.items ?? []) {
|
|
384
|
+
const relatedPath = item.uri ? getFilePath(item.uri) : undefined
|
|
385
|
+
if (!relatedPath || !Array.isArray(item.items)) continue
|
|
386
|
+
const existing = byFile.get(relatedPath) ?? []
|
|
387
|
+
byFile.set(relatedPath, existing.concat(item.items))
|
|
388
|
+
matched = matched || relatedPath === filePath
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return { handled: true, matched, byFile }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function documentPullState() {
|
|
395
|
+
const documentRegistrations = [...diagnosticRegistrations.values()].filter(
|
|
396
|
+
(registration) => registration.registerOptions?.workspaceDiagnostics !== true,
|
|
397
|
+
)
|
|
398
|
+
return {
|
|
399
|
+
documentIdentifiers: [
|
|
400
|
+
...new Set(documentRegistrations.flatMap((registration) => registration.registerOptions?.identifier ?? [])),
|
|
401
|
+
],
|
|
402
|
+
supported: hasStaticPullDiagnostics || documentRegistrations.length > 0,
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function workspacePullState() {
|
|
407
|
+
const workspaceRegistrations = [...diagnosticRegistrations.values()].filter(
|
|
408
|
+
(registration) => registration.registerOptions?.workspaceDiagnostics === true,
|
|
409
|
+
)
|
|
410
|
+
return {
|
|
411
|
+
workspaceIdentifiers: [
|
|
412
|
+
...new Set(workspaceRegistrations.flatMap((registration) => registration.registerOptions?.identifier ?? [])),
|
|
413
|
+
],
|
|
414
|
+
supported: workspaceRegistrations.length > 0,
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const hasCurrentFileDiagnostics = (filePath: string, results: DiagnosticRequestResult[]) =>
|
|
419
|
+
results.some((result) => (result.byFile.get(filePath)?.length ?? 0) > 0)
|
|
420
|
+
|
|
421
|
+
async function requestDiagnostics(
|
|
422
|
+
filePath: string,
|
|
423
|
+
requests: Promise<DiagnosticRequestResult>[],
|
|
424
|
+
done: (results: DiagnosticRequestResult[]) => boolean,
|
|
425
|
+
) {
|
|
426
|
+
if (!requests.length) return { handled: false, matched: false }
|
|
427
|
+
|
|
428
|
+
const results: DiagnosticRequestResult[] = []
|
|
429
|
+
return new Promise<{ handled: boolean; matched: boolean }>((resolve) => {
|
|
430
|
+
let pending = requests.length
|
|
431
|
+
let resolved = false
|
|
432
|
+
const finish = (merged: { handled: boolean; matched: boolean }, force = false) => {
|
|
433
|
+
if (resolved) return
|
|
434
|
+
if (!force && !done(results)) return
|
|
435
|
+
resolved = true
|
|
436
|
+
resolve(merged)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
for (const request of requests) {
|
|
440
|
+
request.then((result) => {
|
|
441
|
+
results.push(result)
|
|
442
|
+
pending -= 1
|
|
443
|
+
const merged = mergeResults(filePath, results)
|
|
444
|
+
finish(merged)
|
|
445
|
+
if (pending === 0) finish(merged, true)
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// LATENCY-CRITICAL: dispatch identifier pulls in parallel and unblock once one
|
|
452
|
+
// batch already produced diagnostics for the current file. Let slower pulls keep
|
|
453
|
+
// merging in the background; do not sequence identifier-by-identifier, and do
|
|
454
|
+
// not add a post-match settle/debounce delay. See PR #23771.
|
|
455
|
+
async function requestDocumentDiagnostics(filePath: string) {
|
|
456
|
+
const state = documentPullState()
|
|
457
|
+
if (!state.supported) return { handled: false, matched: false }
|
|
458
|
+
return requestDiagnostics(
|
|
459
|
+
filePath,
|
|
460
|
+
[
|
|
461
|
+
requestDiagnosticReport(filePath),
|
|
462
|
+
...state.documentIdentifiers.map((identifier) => requestDiagnosticReport(filePath, identifier)),
|
|
463
|
+
],
|
|
464
|
+
(results) => hasCurrentFileDiagnostics(filePath, results),
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function requestFullDiagnostics(filePath: string) {
|
|
469
|
+
const documentState = documentPullState()
|
|
470
|
+
const workspaceState = workspacePullState()
|
|
471
|
+
if (!documentState.supported && !workspaceState.supported) return { handled: false, matched: false }
|
|
472
|
+
return mergeResults(
|
|
473
|
+
filePath,
|
|
474
|
+
await Promise.all([
|
|
475
|
+
...(documentState.supported ? [requestDiagnosticReport(filePath)] : []),
|
|
476
|
+
...documentState.documentIdentifiers.map((identifier) => requestDiagnosticReport(filePath, identifier)),
|
|
477
|
+
...(workspaceState.supported ? [requestWorkspaceDiagnosticReport(filePath)] : []),
|
|
478
|
+
...workspaceState.workspaceIdentifiers.map((identifier) =>
|
|
479
|
+
requestWorkspaceDiagnosticReport(filePath, identifier),
|
|
480
|
+
),
|
|
481
|
+
]),
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function waitForRegistrationChange(timeout: number) {
|
|
486
|
+
if (timeout <= 0) return Promise.resolve(false)
|
|
487
|
+
return new Promise<boolean>((resolve) => {
|
|
488
|
+
let finished = false
|
|
489
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
490
|
+
const finish = (result: boolean) => {
|
|
491
|
+
if (finished) return
|
|
492
|
+
finished = true
|
|
493
|
+
if (timer) clearTimeout(timer)
|
|
494
|
+
registrationListeners.delete(listener)
|
|
495
|
+
resolve(result)
|
|
496
|
+
}
|
|
497
|
+
const listener = () => finish(true)
|
|
498
|
+
registrationListeners.add(listener)
|
|
499
|
+
timer = setTimeout(() => finish(false), timeout)
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function waitForFreshPush(request: { path: string; version: number; after: number; timeout: number }) {
|
|
504
|
+
if (request.timeout <= 0) return Promise.resolve(false)
|
|
505
|
+
return new Promise<boolean>((resolve) => {
|
|
506
|
+
let finished = false
|
|
507
|
+
let debounceTimer: ReturnType<typeof setTimeout> | undefined
|
|
508
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | undefined
|
|
509
|
+
let unsub: (() => void) | undefined
|
|
510
|
+
const finish = (result: boolean) => {
|
|
511
|
+
if (finished) return
|
|
512
|
+
finished = true
|
|
513
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
514
|
+
if (timeoutTimer) clearTimeout(timeoutTimer)
|
|
515
|
+
unsub?.()
|
|
516
|
+
resolve(result)
|
|
517
|
+
}
|
|
518
|
+
const schedule = () => {
|
|
519
|
+
const hit = published.get(request.path)
|
|
520
|
+
if (!hit) return
|
|
521
|
+
if (typeof hit.version === "number" && hit.version !== request.version) return
|
|
522
|
+
if (hit.at < request.after && hit.version !== request.version) return
|
|
523
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
524
|
+
debounceTimer = setTimeout(() => finish(true), Math.max(0, DIAGNOSTICS_DEBOUNCE_MS - (Date.now() - hit.at)))
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
timeoutTimer = setTimeout(() => finish(false), request.timeout)
|
|
528
|
+
unsub = busRuntime.runSync((svc) =>
|
|
529
|
+
svc
|
|
530
|
+
.subscribeCallback(Event.Diagnostics, (event) => {
|
|
531
|
+
if (event.properties.path !== request.path || event.properties.serverID !== input.serverID) return
|
|
532
|
+
schedule()
|
|
533
|
+
})
|
|
534
|
+
.pipe(Effect.provideService(InstanceRef, instance)),
|
|
535
|
+
)
|
|
536
|
+
schedule()
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function waitForDocumentDiagnostics(request: { path: string; version: number; after?: number }) {
|
|
541
|
+
const startedAt = request.after ?? Date.now()
|
|
542
|
+
const pushWait = waitForFreshPush({
|
|
543
|
+
path: request.path,
|
|
544
|
+
version: request.version,
|
|
545
|
+
after: startedAt,
|
|
546
|
+
timeout: DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS,
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
while (Date.now() - startedAt < DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS) {
|
|
550
|
+
const result = await requestDocumentDiagnostics(request.path)
|
|
551
|
+
if (result.matched) return
|
|
552
|
+
const remaining = DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS - (Date.now() - startedAt)
|
|
553
|
+
if (remaining <= 0) return
|
|
554
|
+
const next = await Promise.race([
|
|
555
|
+
pushWait.then((ready) => (ready ? "push" : ("timeout" as const))),
|
|
556
|
+
waitForRegistrationChange(remaining).then((changed) => (changed ? "registration" : ("timeout" as const))),
|
|
557
|
+
])
|
|
558
|
+
if (next !== "registration") return
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function waitForFullDiagnostics(request: { path: string; version: number; after?: number }) {
|
|
563
|
+
const startedAt = request.after ?? Date.now()
|
|
564
|
+
const pushWait = waitForFreshPush({
|
|
565
|
+
path: request.path,
|
|
566
|
+
version: request.version,
|
|
567
|
+
after: startedAt,
|
|
568
|
+
timeout: DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS,
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
while (Date.now() - startedAt < DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS) {
|
|
572
|
+
const result = await requestFullDiagnostics(request.path)
|
|
573
|
+
if (result.handled || result.matched) return
|
|
574
|
+
const remaining = DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS - (Date.now() - startedAt)
|
|
575
|
+
if (remaining <= 0) return
|
|
576
|
+
const next = await Promise.race([
|
|
577
|
+
pushWait.then((ready) => (ready ? "push" : ("timeout" as const))),
|
|
578
|
+
waitForRegistrationChange(remaining).then((changed) => (changed ? "registration" : ("timeout" as const))),
|
|
579
|
+
])
|
|
580
|
+
if (next !== "registration") return
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// --- Public API ---
|
|
585
|
+
|
|
586
|
+
const result = {
|
|
587
|
+
root: input.root,
|
|
588
|
+
get serverID() {
|
|
589
|
+
return input.serverID
|
|
590
|
+
},
|
|
591
|
+
get connection() {
|
|
592
|
+
return connection
|
|
593
|
+
},
|
|
594
|
+
notify: {
|
|
595
|
+
async open(request: { path: string }) {
|
|
596
|
+
request.path = Filesystem.normalizePath(
|
|
597
|
+
path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path),
|
|
598
|
+
)
|
|
599
|
+
const text = await Filesystem.readText(request.path)
|
|
600
|
+
const extension = path.extname(request.path)
|
|
601
|
+
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
|
|
602
|
+
|
|
603
|
+
const document = files[request.path]
|
|
604
|
+
if (document !== undefined) {
|
|
605
|
+
// Do not wipe diagnostics on didChange. Some servers (e.g. clangd) only
|
|
606
|
+
// re-emit diagnostics when the content actually changes, so clearing
|
|
607
|
+
// here would lose errors for no-op touchFile calls. Let the server's
|
|
608
|
+
// next push/pull overwrite naturally.
|
|
609
|
+
logger.info("workspace/didChangeWatchedFiles", request)
|
|
610
|
+
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
|
611
|
+
changes: [
|
|
612
|
+
{
|
|
613
|
+
uri: pathToFileURL(request.path).href,
|
|
614
|
+
type: FILE_CHANGE_CHANGED,
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
const next = document.version + 1
|
|
620
|
+
files[request.path] = { version: next, text }
|
|
621
|
+
logger.info("textDocument/didChange", {
|
|
622
|
+
path: request.path,
|
|
623
|
+
version: next,
|
|
624
|
+
})
|
|
625
|
+
await connection.sendNotification("textDocument/didChange", {
|
|
626
|
+
textDocument: {
|
|
627
|
+
uri: pathToFileURL(request.path).href,
|
|
628
|
+
version: next,
|
|
629
|
+
},
|
|
630
|
+
contentChanges:
|
|
631
|
+
syncKind === TEXT_DOCUMENT_SYNC_INCREMENTAL
|
|
632
|
+
? [
|
|
633
|
+
{
|
|
634
|
+
range: {
|
|
635
|
+
start: { line: 0, character: 0 },
|
|
636
|
+
end: endPosition(document.text),
|
|
637
|
+
},
|
|
638
|
+
text,
|
|
639
|
+
},
|
|
640
|
+
]
|
|
641
|
+
: [{ text }],
|
|
642
|
+
})
|
|
643
|
+
return next
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
logger.info("workspace/didChangeWatchedFiles", request)
|
|
647
|
+
await connection.sendNotification("workspace/didChangeWatchedFiles", {
|
|
648
|
+
changes: [
|
|
649
|
+
{
|
|
650
|
+
uri: pathToFileURL(request.path).href,
|
|
651
|
+
type: FILE_CHANGE_CREATED,
|
|
652
|
+
},
|
|
653
|
+
],
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
logger.info("textDocument/didOpen", request)
|
|
657
|
+
pushDiagnostics.delete(request.path)
|
|
658
|
+
pullDiagnostics.delete(request.path)
|
|
659
|
+
await connection.sendNotification("textDocument/didOpen", {
|
|
660
|
+
textDocument: {
|
|
661
|
+
uri: pathToFileURL(request.path).href,
|
|
662
|
+
languageId,
|
|
663
|
+
version: 0,
|
|
664
|
+
text,
|
|
665
|
+
},
|
|
666
|
+
})
|
|
667
|
+
files[request.path] = { version: 0, text }
|
|
668
|
+
return 0
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
get diagnostics() {
|
|
672
|
+
const result = new Map<string, Diagnostic[]>()
|
|
673
|
+
for (const key of new Set([...pushDiagnostics.keys(), ...pullDiagnostics.keys()])) {
|
|
674
|
+
result.set(key, mergedDiagnostics(key))
|
|
675
|
+
}
|
|
676
|
+
return result
|
|
677
|
+
},
|
|
678
|
+
async waitForDiagnostics(request: { path: string; version: number; mode?: "document" | "full"; after?: number }) {
|
|
679
|
+
const normalizedPath = Filesystem.normalizePath(
|
|
680
|
+
path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path),
|
|
681
|
+
)
|
|
682
|
+
logger.info("waiting for diagnostics", {
|
|
683
|
+
path: normalizedPath,
|
|
684
|
+
mode: request.mode ?? "full",
|
|
685
|
+
version: request.version,
|
|
686
|
+
})
|
|
687
|
+
if (request.mode === "document") {
|
|
688
|
+
await waitForDocumentDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
|
|
689
|
+
return
|
|
690
|
+
}
|
|
691
|
+
await waitForFullDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
|
|
692
|
+
},
|
|
693
|
+
async shutdown() {
|
|
694
|
+
logger.info("shutting down")
|
|
695
|
+
connection.end()
|
|
696
|
+
connection.dispose()
|
|
697
|
+
await Process.stop(input.server.process)
|
|
698
|
+
logger.info("shutdown")
|
|
699
|
+
},
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
logger.info("initialized")
|
|
703
|
+
|
|
704
|
+
return result
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export * as LSPClient from "./client"
|