dodo-mcp 1.0.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 +425 -0
- package/dist/auth/adapter.js +43 -0
- package/dist/auth/adapter.js.map +1 -0
- package/dist/auth/clientDeletion.js +45 -0
- package/dist/auth/clientDeletion.js.map +1 -0
- package/dist/auth/clients.js +55 -0
- package/dist/auth/clients.js.map +1 -0
- package/dist/auth/constants.js +10 -0
- package/dist/auth/constants.js.map +1 -0
- package/dist/auth/interactions.js +184 -0
- package/dist/auth/interactions.js.map +1 -0
- package/dist/auth/keys.js +56 -0
- package/dist/auth/keys.js.map +1 -0
- package/dist/auth/provider.js +131 -0
- package/dist/auth/provider.js.map +1 -0
- package/dist/auth/verifier.js +74 -0
- package/dist/auth/verifier.js.map +1 -0
- package/dist/cli/kill.js +124 -0
- package/dist/cli/kill.js.map +1 -0
- package/dist/cli/main.js +1393 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/menu.js +127 -0
- package/dist/cli/menu.js.map +1 -0
- package/dist/cli/terminal.js +30 -0
- package/dist/cli/terminal.js.map +1 -0
- package/dist/config/globalConfig.js +144 -0
- package/dist/config/globalConfig.js.map +1 -0
- package/dist/config/limits.js +42 -0
- package/dist/config/limits.js.map +1 -0
- package/dist/config/paths.js +94 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/projectConfig.js +63 -0
- package/dist/config/projectConfig.js.map +1 -0
- package/dist/config/stateImport.js +306 -0
- package/dist/config/stateImport.js.map +1 -0
- package/dist/config/tunnelConfig.js +23 -0
- package/dist/config/tunnelConfig.js.map +1 -0
- package/dist/errors.js +82 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluation/contracts.js +86 -0
- package/dist/evaluation/contracts.js.map +1 -0
- package/dist/evaluation/dodoBench.js +69 -0
- package/dist/evaluation/dodoBench.js.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/authentication.js +117 -0
- package/dist/ipc/authentication.js.map +1 -0
- package/dist/ipc/client.js +86 -0
- package/dist/ipc/client.js.map +1 -0
- package/dist/ipc/installationClient.js +83 -0
- package/dist/ipc/installationClient.js.map +1 -0
- package/dist/ipc/ownerControl.js +46 -0
- package/dist/ipc/ownerControl.js.map +1 -0
- package/dist/ipc/protocol.js +2 -0
- package/dist/ipc/protocol.js.map +1 -0
- package/dist/ipc/server.js +108 -0
- package/dist/ipc/server.js.map +1 -0
- package/dist/platform/execResolve.js +64 -0
- package/dist/platform/execResolve.js.map +1 -0
- package/dist/platform/fileIdentity.js +26 -0
- package/dist/platform/fileIdentity.js.map +1 -0
- package/dist/platform/fsRetry.js +22 -0
- package/dist/platform/fsRetry.js.map +1 -0
- package/dist/platform/pathPolicy.js +29 -0
- package/dist/platform/pathPolicy.js.map +1 -0
- package/dist/platform/privateFs.js +116 -0
- package/dist/platform/privateFs.js.map +1 -0
- package/dist/platform/processTree.js +31 -0
- package/dist/platform/processTree.js.map +1 -0
- package/dist/platform/shell.js +74 -0
- package/dist/platform/shell.js.map +1 -0
- package/dist/platform/speech.js +94 -0
- package/dist/platform/speech.js.map +1 -0
- package/dist/platform/system.js +28 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/platform/windowsSandbox.js +129 -0
- package/dist/platform/windowsSandbox.js.map +1 -0
- package/dist/projects/federation.js +312 -0
- package/dist/projects/federation.js.map +1 -0
- package/dist/projects/registry.js +291 -0
- package/dist/projects/registry.js.map +1 -0
- package/dist/security/accessMode.js +19 -0
- package/dist/security/accessMode.js.map +1 -0
- package/dist/security/approvals.js +56 -0
- package/dist/security/approvals.js.map +1 -0
- package/dist/security/env.js +57 -0
- package/dist/security/env.js.map +1 -0
- package/dist/security/mutationQueue.js +78 -0
- package/dist/security/mutationQueue.js.map +1 -0
- package/dist/security/policy.js +22 -0
- package/dist/security/policy.js.map +1 -0
- package/dist/security/projectAuthority.js +22 -0
- package/dist/security/projectAuthority.js.map +1 -0
- package/dist/security/redact.js +30 -0
- package/dist/security/redact.js.map +1 -0
- package/dist/server/aiAdmin.js +201 -0
- package/dist/server/aiAdmin.js.map +1 -0
- package/dist/server/appServer.js +461 -0
- package/dist/server/appServer.js.map +1 -0
- package/dist/server/bootstrap.js +185 -0
- package/dist/server/bootstrap.js.map +1 -0
- package/dist/server/configUi/app.css +347 -0
- package/dist/server/configUi/app.js +958 -0
- package/dist/server/configUi/index.html +379 -0
- package/dist/server/configUi/ui/alerts.js +92 -0
- package/dist/server/configUi/ui/dom.js +19 -0
- package/dist/server/configUi/ui/tooltips.js +115 -0
- package/dist/server/configUi/vendor/VERSION.txt +1 -0
- package/dist/server/configUi/vendor/sweetalert2.min.css +1 -0
- package/dist/server/configUi/vendor/sweetalert2.min.js +5 -0
- package/dist/server/configUi/workbench.css +62 -0
- package/dist/server/configUi/workbench.js +394 -0
- package/dist/server/installationRuntime.js +155 -0
- package/dist/server/installationRuntime.js.map +1 -0
- package/dist/server/instructions.js +17 -0
- package/dist/server/instructions.js.map +1 -0
- package/dist/server/ipcDispatch.js +225 -0
- package/dist/server/ipcDispatch.js.map +1 -0
- package/dist/server/localConfig.js +561 -0
- package/dist/server/localConfig.js.map +1 -0
- package/dist/server/optionalServices.js +30 -0
- package/dist/server/optionalServices.js.map +1 -0
- package/dist/server/projectLease.js +46 -0
- package/dist/server/projectLease.js.map +1 -0
- package/dist/server/stdioServer.js +75 -0
- package/dist/server/stdioServer.js.map +1 -0
- package/dist/server/version.js +13 -0
- package/dist/server/version.js.map +1 -0
- package/dist/server/workspaceHost.js +194 -0
- package/dist/server/workspaceHost.js.map +1 -0
- package/dist/services/agent/agentService.js +598 -0
- package/dist/services/agent/agentService.js.map +1 -0
- package/dist/services/agent/contracts.js +102 -0
- package/dist/services/agent/contracts.js.map +1 -0
- package/dist/services/ai/adapters.js +287 -0
- package/dist/services/ai/adapters.js.map +1 -0
- package/dist/services/ai/contracts.js +39 -0
- package/dist/services/ai/contracts.js.map +1 -0
- package/dist/services/ai/credentials.js +77 -0
- package/dist/services/ai/credentials.js.map +1 -0
- package/dist/services/ai/network.js +113 -0
- package/dist/services/ai/network.js.map +1 -0
- package/dist/services/ai/settings.js +178 -0
- package/dist/services/ai/settings.js.map +1 -0
- package/dist/services/ai/subagents.js +468 -0
- package/dist/services/ai/subagents.js.map +1 -0
- package/dist/services/assistance/analysis.js +352 -0
- package/dist/services/assistance/analysis.js.map +1 -0
- package/dist/services/assistance/contracts.js +54 -0
- package/dist/services/assistance/contracts.js.map +1 -0
- package/dist/services/assistance/verification.js +274 -0
- package/dist/services/assistance/verification.js.map +1 -0
- package/dist/services/brain/brainParser.js +97 -0
- package/dist/services/brain/brainParser.js.map +1 -0
- package/dist/services/brain/brainService.js +733 -0
- package/dist/services/brain/brainService.js.map +1 -0
- package/dist/services/brain/brainWorker.js +212 -0
- package/dist/services/brain/brainWorker.js.map +1 -0
- package/dist/services/brain/contracts.js +154 -0
- package/dist/services/brain/contracts.js.map +1 -0
- package/dist/services/brain/protocol.js +2 -0
- package/dist/services/brain/protocol.js.map +1 -0
- package/dist/services/changes/applier.js +544 -0
- package/dist/services/changes/applier.js.map +1 -0
- package/dist/services/changes/contentBudget.js +11 -0
- package/dist/services/changes/contentBudget.js.map +1 -0
- package/dist/services/changes/planner.js +284 -0
- package/dist/services/changes/planner.js.map +1 -0
- package/dist/services/changes/types.js +3 -0
- package/dist/services/changes/types.js.map +1 -0
- package/dist/services/context/contextEngine.js +746 -0
- package/dist/services/context/contextEngine.js.map +1 -0
- package/dist/services/context/contracts.js +88 -0
- package/dist/services/context/contracts.js.map +1 -0
- package/dist/services/desktop/desktopPolicy.js +76 -0
- package/dist/services/desktop/desktopPolicy.js.map +1 -0
- package/dist/services/desktop/desktopService.js +130 -0
- package/dist/services/desktop/desktopService.js.map +1 -0
- package/dist/services/desktop/nativeBackend.js +174 -0
- package/dist/services/desktop/nativeBackend.js.map +1 -0
- package/dist/services/desktop/protocol.js +32 -0
- package/dist/services/desktop/protocol.js.map +1 -0
- package/dist/services/files/readService.js +136 -0
- package/dist/services/files/readService.js.map +1 -0
- package/dist/services/git/gitService.js +369 -0
- package/dist/services/git/gitService.js.map +1 -0
- package/dist/services/intelligence/intelService.js +136 -0
- package/dist/services/intelligence/intelService.js.map +1 -0
- package/dist/services/intelligence/protocol.js +2 -0
- package/dist/services/intelligence/protocol.js.map +1 -0
- package/dist/services/intelligence/tsWorker.js +419 -0
- package/dist/services/intelligence/tsWorker.js.map +1 -0
- package/dist/services/jobs/commandInput.js +34 -0
- package/dist/services/jobs/commandInput.js.map +1 -0
- package/dist/services/jobs/jobManager.js +398 -0
- package/dist/services/jobs/jobManager.js.map +1 -0
- package/dist/services/jobs/sandbox.js +156 -0
- package/dist/services/jobs/sandbox.js.map +1 -0
- package/dist/services/jobs/sandboxWiring.js +37 -0
- package/dist/services/jobs/sandboxWiring.js.map +1 -0
- package/dist/services/jobs/spool.js +112 -0
- package/dist/services/jobs/spool.js.map +1 -0
- package/dist/services/jobs/windowsSandboxChild.js +24 -0
- package/dist/services/jobs/windowsSandboxChild.js.map +1 -0
- package/dist/services/jobs/windowsSandboxWorker.js +155 -0
- package/dist/services/jobs/windowsSandboxWorker.js.map +1 -0
- package/dist/services/lsp/documentUri.js +16 -0
- package/dist/services/lsp/documentUri.js.map +1 -0
- package/dist/services/lsp/jsonrpc.js +382 -0
- package/dist/services/lsp/jsonrpc.js.map +1 -0
- package/dist/services/lsp/lspService.js +1051 -0
- package/dist/services/lsp/lspService.js.map +1 -0
- package/dist/services/memory/contracts.js +94 -0
- package/dist/services/memory/contracts.js.map +1 -0
- package/dist/services/memory/memoryService.js +683 -0
- package/dist/services/memory/memoryService.js.map +1 -0
- package/dist/services/multimodal/browserService.js +407 -0
- package/dist/services/multimodal/browserService.js.map +1 -0
- package/dist/services/multimodal/byteRange.js +31 -0
- package/dist/services/multimodal/byteRange.js.map +1 -0
- package/dist/services/multimodal/contracts.js +59 -0
- package/dist/services/multimodal/contracts.js.map +1 -0
- package/dist/services/multimodal/gameService.js +95 -0
- package/dist/services/multimodal/gameService.js.map +1 -0
- package/dist/services/multimodal/images.js +107 -0
- package/dist/services/multimodal/images.js.map +1 -0
- package/dist/services/multimodal/mediaService.js +279 -0
- package/dist/services/multimodal/mediaService.js.map +1 -0
- package/dist/services/multimodal/mediaWorker.js +143 -0
- package/dist/services/multimodal/mediaWorker.js.map +1 -0
- package/dist/services/multimodal/multimodalService.js +51 -0
- package/dist/services/multimodal/multimodalService.js.map +1 -0
- package/dist/services/multimodal/network.js +69 -0
- package/dist/services/multimodal/network.js.map +1 -0
- package/dist/services/multimodal/operations.js +40 -0
- package/dist/services/multimodal/operations.js.map +1 -0
- package/dist/services/multimodal/storage.js +160 -0
- package/dist/services/multimodal/storage.js.map +1 -0
- package/dist/services/multimodal/workflowService.js +179 -0
- package/dist/services/multimodal/workflowService.js.map +1 -0
- package/dist/services/overview.js +284 -0
- package/dist/services/overview.js.map +1 -0
- package/dist/services/resources/casStore.js +304 -0
- package/dist/services/resources/casStore.js.map +1 -0
- package/dist/services/resources/contracts.js +75 -0
- package/dist/services/resources/contracts.js.map +1 -0
- package/dist/services/resources/mime.js +77 -0
- package/dist/services/resources/mime.js.map +1 -0
- package/dist/services/resources/providerRegistry.js +22 -0
- package/dist/services/resources/providerRegistry.js.map +1 -0
- package/dist/services/resources/providers.js +120 -0
- package/dist/services/resources/providers.js.map +1 -0
- package/dist/services/resources/resourceService.js +282 -0
- package/dist/services/resources/resourceService.js.map +1 -0
- package/dist/services/runtime/contracts.js +65 -0
- package/dist/services/runtime/contracts.js.map +1 -0
- package/dist/services/runtime/runtimeService.js +364 -0
- package/dist/services/runtime/runtimeService.js.map +1 -0
- package/dist/services/schedules/scheduleService.js +198 -0
- package/dist/services/schedules/scheduleService.js.map +1 -0
- package/dist/services/search/searchService.js +434 -0
- package/dist/services/search/searchService.js.map +1 -0
- package/dist/services/search/searchWorker.js +58 -0
- package/dist/services/search/searchWorker.js.map +1 -0
- package/dist/setup/componentProbes.js +259 -0
- package/dist/setup/componentProbes.js.map +1 -0
- package/dist/setup/download.js +139 -0
- package/dist/setup/download.js.map +1 -0
- package/dist/setup/managedTools.js +118 -0
- package/dist/setup/managedTools.js.map +1 -0
- package/dist/setup/sandboxProbe.js +172 -0
- package/dist/setup/sandboxProbe.js.map +1 -0
- package/dist/setup/setup.js +522 -0
- package/dist/setup/setup.js.map +1 -0
- package/dist/setup/windowsChromiumAcl.js +56 -0
- package/dist/setup/windowsChromiumAcl.js.map +1 -0
- package/dist/setup/windowsSandboxInstall.js +94 -0
- package/dist/setup/windowsSandboxInstall.js.map +1 -0
- package/dist/setup/windowsSpeechInstall.js +86 -0
- package/dist/setup/windowsSpeechInstall.js.map +1 -0
- package/dist/store/db.js +711 -0
- package/dist/store/db.js.map +1 -0
- package/dist/store/scopeEncoding.js +15 -0
- package/dist/store/scopeEncoding.js.map +1 -0
- package/dist/store/store.js +467 -0
- package/dist/store/store.js.map +1 -0
- package/dist/tools/agentRuntimeTools.js +216 -0
- package/dist/tools/agentRuntimeTools.js.map +1 -0
- package/dist/tools/agentTools.js +317 -0
- package/dist/tools/agentTools.js.map +1 -0
- package/dist/tools/assistanceTools.js +126 -0
- package/dist/tools/assistanceTools.js.map +1 -0
- package/dist/tools/brainTools.js +130 -0
- package/dist/tools/brainTools.js.map +1 -0
- package/dist/tools/catalog.js +30 -0
- package/dist/tools/catalog.js.map +1 -0
- package/dist/tools/changeTools.js +188 -0
- package/dist/tools/changeTools.js.map +1 -0
- package/dist/tools/context.js +243 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/contextTools.js +57 -0
- package/dist/tools/contextTools.js.map +1 -0
- package/dist/tools/coreCatalog.js +82 -0
- package/dist/tools/coreCatalog.js.map +1 -0
- package/dist/tools/desktopTools.js +41 -0
- package/dist/tools/desktopTools.js.map +1 -0
- package/dist/tools/directTools.js +273 -0
- package/dist/tools/directTools.js.map +1 -0
- package/dist/tools/envelope.js +71 -0
- package/dist/tools/envelope.js.map +1 -0
- package/dist/tools/extraTools.js +298 -0
- package/dist/tools/extraTools.js.map +1 -0
- package/dist/tools/gitTools.js +93 -0
- package/dist/tools/gitTools.js.map +1 -0
- package/dist/tools/jobTools.js +454 -0
- package/dist/tools/jobTools.js.map +1 -0
- package/dist/tools/memoryTools.js +102 -0
- package/dist/tools/memoryTools.js.map +1 -0
- package/dist/tools/metaTools.js +153 -0
- package/dist/tools/metaTools.js.map +1 -0
- package/dist/tools/multimodalTools.js +262 -0
- package/dist/tools/multimodalTools.js.map +1 -0
- package/dist/tools/projectTools.js +261 -0
- package/dist/tools/projectTools.js.map +1 -0
- package/dist/tools/resourceTools.js +81 -0
- package/dist/tools/resourceTools.js.map +1 -0
- package/dist/tools/runtimeTools.js +137 -0
- package/dist/tools/runtimeTools.js.map +1 -0
- package/dist/tools/scheduleTools.js +10 -0
- package/dist/tools/scheduleTools.js.map +1 -0
- package/dist/tools/semanticTools.js +141 -0
- package/dist/tools/semanticTools.js.map +1 -0
- package/dist/tools/subagentTools.js +32 -0
- package/dist/tools/subagentTools.js.map +1 -0
- package/dist/tools/surface.js +644 -0
- package/dist/tools/surface.js.map +1 -0
- package/dist/tunnel/control.js +113 -0
- package/dist/tunnel/control.js.map +1 -0
- package/dist/tunnel/credentials.js +392 -0
- package/dist/tunnel/credentials.js.map +1 -0
- package/dist/tunnel/log.js +84 -0
- package/dist/tunnel/log.js.map +1 -0
- package/dist/tunnel/runtime.js +55 -0
- package/dist/tunnel/runtime.js.map +1 -0
- package/dist/tunnel/supervisor.js +280 -0
- package/dist/tunnel/supervisor.js.map +1 -0
- package/dist/util/bytes.js +53 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/hash.js +53 -0
- package/dist/util/hash.js.map +1 -0
- package/dist/workspace/fs.js +305 -0
- package/dist/workspace/fs.js.map +1 -0
- package/dist/workspace/identity.js +17 -0
- package/dist/workspace/identity.js.map +1 -0
- package/dist/workspace/ignores.js +173 -0
- package/dist/workspace/ignores.js.map +1 -0
- package/dist/workspace/root.js +59 -0
- package/dist/workspace/root.js.map +1 -0
- package/docs/AGENT_RUNTIME.md +154 -0
- package/docs/AI_PROVIDERS.md +110 -0
- package/docs/ARCHITECTURE.md +331 -0
- package/docs/ASSISTANCE.md +31 -0
- package/docs/AUTH.md +59 -0
- package/docs/BRAIN.md +92 -0
- package/docs/COMPATIBILITY.md +92 -0
- package/docs/CONFIRMATION.md +13 -0
- package/docs/CONTEXT.md +87 -0
- package/docs/DESKTOP.md +35 -0
- package/docs/EVALUATION.md +86 -0
- package/docs/LARGE_INPUTS.md +21 -0
- package/docs/LSP.md +21 -0
- package/docs/MANUAL_ACCEPTANCE.md +283 -0
- package/docs/MEMORY.md +128 -0
- package/docs/MULTIMODAL.md +32 -0
- package/docs/PROJECTS.md +146 -0
- package/docs/RELEASE_1.0.0.md +111 -0
- package/docs/RELEASE_NOTES.md +92 -0
- package/docs/RESOURCES.md +83 -0
- package/docs/RUNTIME.md +125 -0
- package/docs/SCHEDULES.md +23 -0
- package/docs/SECURITY.md +250 -0
- package/docs/TEST_REPORT.md +312 -0
- package/docs/TUNNEL.md +86 -0
- package/docs/WEB_CLIENTS.md +43 -0
- package/docs/WINDOWS.md +79 -0
- package/docs/adr/001-one-process-one-workspace.md +22 -0
- package/docs/adr/002-external-tunnel.md +19 -0
- package/docs/adr/003-sdk-v2-legacy-stateless.md +20 -0
- package/docs/adr/004-embedded-oauth-provider.md +19 -0
- package/docs/adr/005-static-client-baseline.md +19 -0
- package/docs/adr/006-guarded-fs-explicit-exec.md +16 -0
- package/docs/adr/007-preview-hash-journal.md +18 -0
- package/docs/adr/008-ts-js-semantics-first.md +17 -0
- package/docs/adr/009-jobs-polling.md +18 -0
- package/docs/adr/010-sqlite-better-sqlite3.md +18 -0
- package/docs/adr/011-env-example-secure-default.md +19 -0
- package/docs/adr/012-workspace-scope-grant.md +24 -0
- package/docs/adr/013-loopback-proxy-trust.md +18 -0
- package/docs/adr/014-direct-coding-tier.md +48 -0
- package/docs/adr/015-coding-agent-parity.md +56 -0
- package/docs/adr/016-oidc-openid-consent.md +15 -0
- package/docs/adr/017-installation-identity-local-config.md +44 -0
- package/docs/adr/018-access-scope-encoding.md +11 -0
- package/docs/adr/019-runtime-workspace-switch.md +59 -0
- package/docs/adr/020-desktop-access.md +50 -0
- package/docs/adr/021-persistent-desktop-consent.md +39 -0
- package/docs/adr/022-owner-kill-preserves-login.md +30 -0
- package/docs/adr/023-large-coding-inputs.md +49 -0
- package/docs/adr/024-usage-rounds-and-schedules.md +72 -0
- package/docs/adr/025-owner-chat-whitelist.md +53 -0
- package/docs/adr/026-remove-conversation-confirmation.md +35 -0
- package/docs/adr/027-workspace-client-list.md +20 -0
- package/docs/adr/028-owner-client-deletion.md +25 -0
- package/docs/adr/029-compact-tool-surface.md +121 -0
- package/docs/adr/030-windows-native-candidate.md +71 -0
- package/docs/adr/031-owner-selected-tunnel-supervision.md +13 -0
- package/docs/adr/032-owner-project-registry.md +43 -0
- package/docs/adr/033-read-only-project-federation.md +61 -0
- package/docs/adr/034-universal-resource-cas.md +60 -0
- package/docs/adr/035-project-brain-incremental-index.md +51 -0
- package/docs/adr/036-context-engine-evidence.md +53 -0
- package/docs/adr/037-owner-reviewed-memory.md +46 -0
- package/docs/adr/038-runtime-intelligence.md +46 -0
- package/docs/adr/039-advanced-agent-runtime.md +57 -0
- package/docs/adr/040-dodobench-release-gate.md +43 -0
- package/docs/adr/041-local-macos-docker-linux-gates.md +32 -0
- package/docs/adr/042-project-directory-generation-identity.md +36 -0
- package/docs/adr/043-trusted-self-hosted-platform-gates.md +38 -0
- package/docs/adr/044-global-launcher-and-cli-menu.md +43 -0
- package/docs/adr/045-ai-providers-multiproject.md +56 -0
- package/docs/adr/046-personal-access-mode.md +32 -0
- package/docs/adr/README.md +64 -0
- package/examples/.dodo.json.example +20 -0
- package/examples/cloudflared.yml +23 -0
- package/examples/reverse-proxy-fixture.mjs +37 -0
- package/native/desktop-linux.py +387 -0
- package/native/desktop-windows.cs +180 -0
- package/native/desktop.swift +212 -0
- package/package.json +145 -0
- package/schemas/global-config.schema.json +497 -0
- package/schemas/project-config.schema.json +75 -0
- package/schemas/tools.compact.json +3236 -0
- package/schemas/tools.hybrid.json +8194 -0
- package/schemas/tools.json +34518 -0
- package/scripts/guard-publish.mjs +12 -0
- package/scripts/setup-multimodal.mjs +70 -0
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
+
import { DodoError } from '../../errors.js';
|
|
4
|
+
import { buildChildEnv } from '../../security/env.js';
|
|
5
|
+
import { JsonRpcStdioClient, RpcResponseError } from './jsonrpc.js';
|
|
6
|
+
import { resolveTrustedExecutable } from '../../platform/execResolve.js';
|
|
7
|
+
import { isWithinPath } from '../../platform/pathPolicy.js';
|
|
8
|
+
import { documentUri } from './documentUri.js';
|
|
9
|
+
function isRecord(v) {
|
|
10
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
11
|
+
}
|
|
12
|
+
function isUint(v) {
|
|
13
|
+
return typeof v === 'number' && Number.isInteger(v) && v >= 0;
|
|
14
|
+
}
|
|
15
|
+
function isLspPosition(v) {
|
|
16
|
+
return isRecord(v) && isUint(v['line']) && isUint(v['character']);
|
|
17
|
+
}
|
|
18
|
+
function isLspRange(v) {
|
|
19
|
+
return isRecord(v) && isLspPosition(v['start']) && isLspPosition(v['end']);
|
|
20
|
+
}
|
|
21
|
+
function isLspLocation(v) {
|
|
22
|
+
return isRecord(v) && typeof v['uri'] === 'string' && isLspRange(v['range']);
|
|
23
|
+
}
|
|
24
|
+
function isLspTextEdit(v) {
|
|
25
|
+
return isRecord(v) && isLspRange(v['range']) && typeof v['newText'] === 'string';
|
|
26
|
+
}
|
|
27
|
+
function isLspDiagnostic(v) {
|
|
28
|
+
return isRecord(v) && isLspRange(v['range']) && typeof v['message'] === 'string';
|
|
29
|
+
}
|
|
30
|
+
const SYMBOL_KIND_NAMES = [
|
|
31
|
+
'unknown',
|
|
32
|
+
'file',
|
|
33
|
+
'module',
|
|
34
|
+
'namespace',
|
|
35
|
+
'package',
|
|
36
|
+
'class',
|
|
37
|
+
'method',
|
|
38
|
+
'property',
|
|
39
|
+
'field',
|
|
40
|
+
'constructor',
|
|
41
|
+
'enum',
|
|
42
|
+
'interface',
|
|
43
|
+
'function',
|
|
44
|
+
'variable',
|
|
45
|
+
'constant',
|
|
46
|
+
'string',
|
|
47
|
+
'number',
|
|
48
|
+
'boolean',
|
|
49
|
+
'array',
|
|
50
|
+
'object',
|
|
51
|
+
'key',
|
|
52
|
+
'null',
|
|
53
|
+
'enum member',
|
|
54
|
+
'struct',
|
|
55
|
+
'event',
|
|
56
|
+
'operator',
|
|
57
|
+
'type parameter',
|
|
58
|
+
];
|
|
59
|
+
function symbolKindName(kind) {
|
|
60
|
+
return (isUint(kind) ? SYMBOL_KIND_NAMES[kind] : undefined) ?? 'unknown';
|
|
61
|
+
}
|
|
62
|
+
const ALL_SYMBOL_KINDS = Array.from({ length: 26 }, (_, i) => i + 1);
|
|
63
|
+
const CLIENT_CAPABILITIES = {
|
|
64
|
+
textDocument: {
|
|
65
|
+
synchronization: { dynamicRegistration: false, willSave: false, willSaveWaitUntil: false, didSave: false },
|
|
66
|
+
documentSymbol: { dynamicRegistration: false, hierarchicalDocumentSymbolSupport: true, symbolKind: { valueSet: ALL_SYMBOL_KINDS } },
|
|
67
|
+
references: { dynamicRegistration: false },
|
|
68
|
+
definition: { dynamicRegistration: false, linkSupport: false },
|
|
69
|
+
rename: { dynamicRegistration: false, prepareSupport: true },
|
|
70
|
+
publishDiagnostics: { relatedInformation: false, versionSupport: true, codeDescriptionSupport: false, dataSupport: false },
|
|
71
|
+
diagnostic: { dynamicRegistration: false, relatedDocumentSupport: false },
|
|
72
|
+
},
|
|
73
|
+
workspace: {
|
|
74
|
+
symbol: { dynamicRegistration: false, symbolKind: { valueSet: ALL_SYMBOL_KINDS } },
|
|
75
|
+
workspaceFolders: true,
|
|
76
|
+
configuration: true,
|
|
77
|
+
applyEdit: false,
|
|
78
|
+
workspaceEdit: { documentChanges: true, resourceOperations: [], failureHandling: 'abort' },
|
|
79
|
+
},
|
|
80
|
+
general: { positionEncodings: ['utf-16'] },
|
|
81
|
+
// Progress lets us see indexing/analysis work ($/progress begin..end) and
|
|
82
|
+
// wait for it instead of answering from a half-built program.
|
|
83
|
+
window: { workDoneProgress: true },
|
|
84
|
+
};
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Text model: LSP (0-based line, UTF-16 character) <-> char offsets <-> UTF-8 bytes.
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
const MAX_LINE_TEXT = 300;
|
|
89
|
+
class TextDoc {
|
|
90
|
+
text;
|
|
91
|
+
lineStarts = [0];
|
|
92
|
+
lineEnds = [];
|
|
93
|
+
constructor(text) {
|
|
94
|
+
this.text = text;
|
|
95
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
96
|
+
const c = text.charCodeAt(i);
|
|
97
|
+
if (c === 10) {
|
|
98
|
+
this.lineEnds.push(i);
|
|
99
|
+
this.lineStarts.push(i + 1);
|
|
100
|
+
}
|
|
101
|
+
else if (c === 13) {
|
|
102
|
+
this.lineEnds.push(i);
|
|
103
|
+
if (text.charCodeAt(i + 1) === 10)
|
|
104
|
+
i += 1;
|
|
105
|
+
this.lineStarts.push(i + 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.lineEnds.push(text.length);
|
|
109
|
+
}
|
|
110
|
+
get lineCount() {
|
|
111
|
+
return this.lineStarts.length;
|
|
112
|
+
}
|
|
113
|
+
lineLength(line0) {
|
|
114
|
+
return (this.lineEnds[line0] ?? 0) - (this.lineStarts[line0] ?? 0);
|
|
115
|
+
}
|
|
116
|
+
lineText(line0) {
|
|
117
|
+
if (line0 < 0 || line0 >= this.lineCount)
|
|
118
|
+
return '';
|
|
119
|
+
const t = this.text.slice(this.lineStarts[line0], this.lineEnds[line0]);
|
|
120
|
+
return t.length > MAX_LINE_TEXT ? t.slice(0, MAX_LINE_TEXT) : t;
|
|
121
|
+
}
|
|
122
|
+
/** Server-supplied position -> char offset, clamped to the document (LSP semantics). */
|
|
123
|
+
clampOffset(pos) {
|
|
124
|
+
if (pos.line >= this.lineCount)
|
|
125
|
+
return this.text.length;
|
|
126
|
+
const start = this.lineStarts[pos.line];
|
|
127
|
+
return Math.min(start + pos.character, this.lineEnds[pos.line]);
|
|
128
|
+
}
|
|
129
|
+
/** Char offset -> UTF-16 LSP position. */
|
|
130
|
+
positionAt(offset) {
|
|
131
|
+
const off = Math.max(0, Math.min(offset, this.text.length));
|
|
132
|
+
let lo = 0;
|
|
133
|
+
let hi = this.lineStarts.length - 1;
|
|
134
|
+
while (lo < hi) {
|
|
135
|
+
const mid = (lo + hi + 1) >> 1;
|
|
136
|
+
if (this.lineStarts[mid] <= off)
|
|
137
|
+
lo = mid;
|
|
138
|
+
else
|
|
139
|
+
hi = mid - 1;
|
|
140
|
+
}
|
|
141
|
+
return { line: lo, character: off - this.lineStarts[lo] };
|
|
142
|
+
}
|
|
143
|
+
/** Identifier-ish word containing the offset (used only as a display fallback). */
|
|
144
|
+
wordAt(offset) {
|
|
145
|
+
const { line } = this.positionAt(offset);
|
|
146
|
+
const lineStart = this.lineStarts[line];
|
|
147
|
+
const text = this.lineText(line);
|
|
148
|
+
const re = /[\p{L}\p{N}_$]+/gu;
|
|
149
|
+
let m;
|
|
150
|
+
while ((m = re.exec(text)) !== null) {
|
|
151
|
+
const s = lineStart + m.index;
|
|
152
|
+
const e = s + m[0].length;
|
|
153
|
+
if (offset >= s && offset <= e)
|
|
154
|
+
return m[0];
|
|
155
|
+
if (s > offset)
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const EXCLUDED_DIR_SEGMENTS = new Set(['node_modules', '.venv', 'site-packages', 'target', 'vendor']);
|
|
162
|
+
const IDLE_SHUTDOWN_MS = 10 * 60 * 1000;
|
|
163
|
+
const DEFAULT_DIAGNOSTICS_WAIT_MS = 3000;
|
|
164
|
+
const SHUTDOWN_GRACE_MS = 2000;
|
|
165
|
+
/**
|
|
166
|
+
* Startup gate. LSP has no "workspace indexed" signal; servers (pyright
|
|
167
|
+
* included) enumerate workspace files asynchronously AFTER `initialize`
|
|
168
|
+
* completes, so an immediate references/rename would silently miss files in
|
|
169
|
+
* other modules. We wait until the server has no active progress and has
|
|
170
|
+
* been quiet for STARTUP_QUIET_MS, bounded by STARTUP_SETTLE_MAX_MS. The
|
|
171
|
+
* minimum observation window matters because some servers acknowledge
|
|
172
|
+
* `initialize` before they emit their first indexing progress notification.
|
|
173
|
+
*/
|
|
174
|
+
const STARTUP_QUIET_MS = 400;
|
|
175
|
+
const STARTUP_MIN_OBSERVE_MS = 2000;
|
|
176
|
+
const STARTUP_SETTLE_MAX_MS = 5000;
|
|
177
|
+
/** Per request: bounded wait for reported background work to finish before asking. */
|
|
178
|
+
const PRE_REQUEST_IDLE_MAX_MS = 2000;
|
|
179
|
+
const MAX_TRACKED_PROGRESS = 64;
|
|
180
|
+
const MAX_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
181
|
+
const MAX_OPEN_DOCS = 32;
|
|
182
|
+
const MAX_DIAGNOSTIC_FILES = 50;
|
|
183
|
+
const MAX_STORED_DIAGNOSTIC_URIS = 256;
|
|
184
|
+
const MAX_EDITS_PER_FILE = 5000;
|
|
185
|
+
const MAX_QUERY_LENGTH = 256;
|
|
186
|
+
const MAX_SERVER_MESSAGE = 300;
|
|
187
|
+
const IDENTIFIER_RE = /^[\p{L}_$][\p{L}\p{N}_$]{0,127}$/u;
|
|
188
|
+
export class LspService {
|
|
189
|
+
wfs;
|
|
190
|
+
limits;
|
|
191
|
+
registry;
|
|
192
|
+
log;
|
|
193
|
+
diagnosticsWaitMs;
|
|
194
|
+
extToLanguage = new Map();
|
|
195
|
+
servers = new Map();
|
|
196
|
+
closed = false;
|
|
197
|
+
constructor(opts) {
|
|
198
|
+
this.wfs = opts.wfs;
|
|
199
|
+
this.limits = opts.limits;
|
|
200
|
+
this.registry = {};
|
|
201
|
+
this.log = opts.log ?? (() => { });
|
|
202
|
+
this.diagnosticsWaitMs = opts.diagnosticsWaitMs ?? DEFAULT_DIAGNOSTICS_WAIT_MS;
|
|
203
|
+
for (const [language, config] of Object.entries(opts.registry)) {
|
|
204
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_+.-]{0,63}$/.test(language))
|
|
205
|
+
continue;
|
|
206
|
+
if (!isRecord(config) || typeof config['command'] !== 'string' || !Array.isArray(config['args']) || !Array.isArray(config['extensions']))
|
|
207
|
+
continue;
|
|
208
|
+
const extensions = [];
|
|
209
|
+
for (const ext of config['extensions']) {
|
|
210
|
+
if (typeof ext !== 'string' || ext.length === 0 || ext.includes('/'))
|
|
211
|
+
continue;
|
|
212
|
+
const norm = (ext.startsWith('.') ? ext : `.${ext}`).toLowerCase();
|
|
213
|
+
extensions.push(norm);
|
|
214
|
+
if (!this.extToLanguage.has(norm))
|
|
215
|
+
this.extToLanguage.set(norm, language);
|
|
216
|
+
}
|
|
217
|
+
this.registry[language] = { command: config['command'], args: config['args'].filter((a) => typeof a === 'string'), extensions };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Public API
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
languageFor(rel) {
|
|
224
|
+
if (typeof rel !== 'string')
|
|
225
|
+
return undefined;
|
|
226
|
+
const ext = path.posix.extname(rel).toLowerCase();
|
|
227
|
+
if (ext === '')
|
|
228
|
+
return undefined;
|
|
229
|
+
return this.extToLanguage.get(ext);
|
|
230
|
+
}
|
|
231
|
+
available(language) {
|
|
232
|
+
const config = this.registry[language];
|
|
233
|
+
if (!config)
|
|
234
|
+
return { ok: false, reason: `no language server registered for "${language}"` };
|
|
235
|
+
const resolved = this.resolveCommand(config);
|
|
236
|
+
return resolved.ok ? { ok: true } : { ok: false, reason: resolved.reason };
|
|
237
|
+
}
|
|
238
|
+
async symbolsInFile(rel, maxItems) {
|
|
239
|
+
const max = boundedMax(maxItems);
|
|
240
|
+
const language = this.requireLanguage(rel);
|
|
241
|
+
return this.withServer(language, async (entry) => {
|
|
242
|
+
this.requireCapability(entry, 'documentSymbolProvider', 'textDocument/documentSymbol');
|
|
243
|
+
const doc = this.readDoc(rel);
|
|
244
|
+
this.openDoc(entry, doc);
|
|
245
|
+
const result = await this.req(entry, 'textDocument/documentSymbol', { textDocument: { uri: doc.uri } });
|
|
246
|
+
const symbols = [];
|
|
247
|
+
if (Array.isArray(result))
|
|
248
|
+
this.flattenSymbols(result, doc.rel, undefined, symbols, max, 0);
|
|
249
|
+
return { symbols, meta: this.meta(entry) };
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async symbolsQuery(language, query, maxItems) {
|
|
253
|
+
const max = boundedMax(maxItems);
|
|
254
|
+
if (typeof query !== 'string' || query.length > MAX_QUERY_LENGTH) {
|
|
255
|
+
throw new DodoError('INVALID_INPUT', `query must be a string of at most ${MAX_QUERY_LENGTH} characters`);
|
|
256
|
+
}
|
|
257
|
+
if (!this.registry[language])
|
|
258
|
+
throw this.unsupported(language);
|
|
259
|
+
return this.withServer(language, async (entry) => {
|
|
260
|
+
this.requireCapability(entry, 'workspaceSymbolProvider', 'workspace/symbol');
|
|
261
|
+
const result = await this.req(entry, 'workspace/symbol', { query });
|
|
262
|
+
const symbols = [];
|
|
263
|
+
if (Array.isArray(result))
|
|
264
|
+
this.flattenSymbols(result, undefined, undefined, symbols, max, 0);
|
|
265
|
+
return { symbols, meta: this.meta(entry) };
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async references(rel, position, maxItems) {
|
|
269
|
+
const max = boundedMax(maxItems);
|
|
270
|
+
const language = this.requireLanguage(rel);
|
|
271
|
+
return this.withServer(language, async (entry) => {
|
|
272
|
+
this.requireCapability(entry, 'referencesProvider', 'textDocument/references');
|
|
273
|
+
const doc = this.readDoc(rel);
|
|
274
|
+
const lspPos = toLspPosition(doc.doc, position);
|
|
275
|
+
this.openDoc(entry, doc);
|
|
276
|
+
const result = await this.req(entry, 'textDocument/references', {
|
|
277
|
+
textDocument: { uri: doc.uri },
|
|
278
|
+
position: lspPos,
|
|
279
|
+
context: { includeDeclaration: true },
|
|
280
|
+
});
|
|
281
|
+
const definitionKeys = await this.definitionKeys(entry, doc.uri, lspPos);
|
|
282
|
+
const docs = new Map([[doc.rel, doc.doc]]);
|
|
283
|
+
const references = [];
|
|
284
|
+
let outOfScopeCount = 0;
|
|
285
|
+
for (const item of Array.isArray(result) ? result : []) {
|
|
286
|
+
if (!isLspLocation(item))
|
|
287
|
+
continue;
|
|
288
|
+
const target = this.uriToRel(item.uri);
|
|
289
|
+
const text = target === undefined ? undefined : this.textOf(target, docs);
|
|
290
|
+
if (target === undefined || text === undefined) {
|
|
291
|
+
outOfScopeCount += 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const isDefinition = definitionKeys.has(locationKey(item.uri, item.range.start));
|
|
295
|
+
references.push({
|
|
296
|
+
path: target,
|
|
297
|
+
line: item.range.start.line + 1,
|
|
298
|
+
column: item.range.start.character + 1,
|
|
299
|
+
endLine: item.range.end.line + 1,
|
|
300
|
+
endColumn: item.range.end.character + 1,
|
|
301
|
+
lineText: text.lineText(item.range.start.line),
|
|
302
|
+
isDefinition,
|
|
303
|
+
isWriteAccess: isDefinition, // LSP exposes no read/write classification
|
|
304
|
+
});
|
|
305
|
+
if (references.length >= max)
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
return { references, outOfScopeCount, meta: this.meta(entry) };
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async rename(rel, position, newName) {
|
|
312
|
+
if (typeof newName !== 'string' || !IDENTIFIER_RE.test(newName))
|
|
313
|
+
throw new DodoError('INVALID_INPUT', 'newName is not a valid identifier');
|
|
314
|
+
const language = this.requireLanguage(rel);
|
|
315
|
+
return this.withServer(language, async (entry) => {
|
|
316
|
+
const renameCap = entry.capabilities['renameProvider'];
|
|
317
|
+
this.requireCapability(entry, 'renameProvider', 'textDocument/rename');
|
|
318
|
+
const doc = this.readDoc(rel);
|
|
319
|
+
const lspPos = toLspPosition(doc.doc, position);
|
|
320
|
+
this.openDoc(entry, doc);
|
|
321
|
+
let placeholder;
|
|
322
|
+
if (isRecord(renameCap) && renameCap['prepareProvider'] === true) {
|
|
323
|
+
const prep = await this.req(entry, 'textDocument/prepareRename', { textDocument: { uri: doc.uri }, position: lspPos });
|
|
324
|
+
if (prep === null || prep === undefined)
|
|
325
|
+
throw new DodoError('INVALID_INPUT', 'cannot rename here: not a renameable symbol');
|
|
326
|
+
if (isRecord(prep) && typeof prep['placeholder'] === 'string')
|
|
327
|
+
placeholder = prep['placeholder'];
|
|
328
|
+
else if (isLspRange(prep))
|
|
329
|
+
placeholder = doc.doc.text.slice(doc.doc.clampOffset(prep.start), doc.doc.clampOffset(prep.end));
|
|
330
|
+
}
|
|
331
|
+
const edit = await this.req(entry, 'textDocument/rename', { textDocument: { uri: doc.uri }, position: lspPos, newName });
|
|
332
|
+
if (edit === null || edit === undefined)
|
|
333
|
+
throw new DodoError('INVALID_INPUT', 'cannot rename here: the language server produced no edits');
|
|
334
|
+
if (!isRecord(edit))
|
|
335
|
+
throw new DodoError('INTERNAL_ERROR', 'language server returned a malformed WorkspaceEdit');
|
|
336
|
+
const byUri = collectWorkspaceEdit(edit);
|
|
337
|
+
const docs = new Map([[doc.rel, doc.doc]]);
|
|
338
|
+
const locations = [];
|
|
339
|
+
let outOfScopeCount = countResourceOperations(edit);
|
|
340
|
+
const outOfScopeSample = [];
|
|
341
|
+
let symbolName = placeholder;
|
|
342
|
+
const cursor = doc.doc.clampOffset(lspPos);
|
|
343
|
+
for (const [uri, edits] of byUri) {
|
|
344
|
+
const target = this.uriToRel(uri);
|
|
345
|
+
const text = target === undefined ? undefined : this.textOf(target, docs);
|
|
346
|
+
if (target === undefined || text === undefined) {
|
|
347
|
+
outOfScopeCount += edits.length;
|
|
348
|
+
if (outOfScopeSample.length < 5)
|
|
349
|
+
outOfScopeSample.push(target ?? 'outside-workspace');
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const converted = toByteEdits(text, edits.slice(0, MAX_EDITS_PER_FILE));
|
|
353
|
+
if (target === doc.rel && symbolName === undefined) {
|
|
354
|
+
const hit = converted.find((e) => e.startChar <= cursor && cursor <= e.endChar);
|
|
355
|
+
if (hit)
|
|
356
|
+
symbolName = hit.originalText;
|
|
357
|
+
}
|
|
358
|
+
locations.push({ path: target, edits: converted.map(({ start, end, newText, originalText }) => ({ start, end, newText, originalText })) });
|
|
359
|
+
}
|
|
360
|
+
locations.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
361
|
+
const result = {
|
|
362
|
+
locations,
|
|
363
|
+
outOfScopeCount,
|
|
364
|
+
outOfScopeSample,
|
|
365
|
+
symbolName: symbolName ?? doc.doc.wordAt(cursor) ?? '',
|
|
366
|
+
};
|
|
367
|
+
return { result, meta: this.meta(entry) };
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
async diagnostics(rels, maxItems) {
|
|
371
|
+
const max = boundedMax(maxItems);
|
|
372
|
+
if (!Array.isArray(rels) || rels.length === 0)
|
|
373
|
+
throw new DodoError('INVALID_INPUT', 'at least one file is required');
|
|
374
|
+
const degraded = [];
|
|
375
|
+
const targets = rels.slice(0, MAX_DIAGNOSTIC_FILES);
|
|
376
|
+
if (rels.length > MAX_DIAGNOSTIC_FILES)
|
|
377
|
+
degraded.push(`diagnostics limited to the first ${MAX_DIAGNOSTIC_FILES} files`);
|
|
378
|
+
const groups = new Map();
|
|
379
|
+
for (const rel of targets) {
|
|
380
|
+
const language = this.requireLanguage(rel);
|
|
381
|
+
const list = groups.get(language) ?? [];
|
|
382
|
+
list.push(rel);
|
|
383
|
+
groups.set(language, list);
|
|
384
|
+
}
|
|
385
|
+
const out = [];
|
|
386
|
+
let meta;
|
|
387
|
+
for (const [language, files] of groups) {
|
|
388
|
+
if (out.length >= max)
|
|
389
|
+
break;
|
|
390
|
+
const partial = await this.withServer(language, async (entry) => {
|
|
391
|
+
const collected = [];
|
|
392
|
+
const supportsPull = Boolean(entry.capabilities['diagnosticProvider']);
|
|
393
|
+
for (const rel of files) {
|
|
394
|
+
if (collected.length + out.length >= max)
|
|
395
|
+
break;
|
|
396
|
+
const doc = this.readDoc(rel);
|
|
397
|
+
let items;
|
|
398
|
+
if (supportsPull) {
|
|
399
|
+
this.openDoc(entry, doc);
|
|
400
|
+
const report = await this.req(entry, 'textDocument/diagnostic', { textDocument: { uri: doc.uri } });
|
|
401
|
+
if (isRecord(report) && report['kind'] === 'full' && Array.isArray(report['items']))
|
|
402
|
+
items = report['items'];
|
|
403
|
+
else if (isRecord(report) && report['kind'] === 'unchanged')
|
|
404
|
+
items = entry.diagnostics.get(doc.uri) ?? [];
|
|
405
|
+
else {
|
|
406
|
+
items = [];
|
|
407
|
+
degraded.push(`${doc.rel}: language server returned no diagnostic report`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
let arrived = Promise.resolve(false);
|
|
412
|
+
this.openDoc(entry, doc, (version) => {
|
|
413
|
+
arrived = this.waitForDiagnostics(entry, doc.uri, version);
|
|
414
|
+
});
|
|
415
|
+
if (!(await arrived))
|
|
416
|
+
degraded.push(`${doc.rel}: no diagnostics published within ${this.diagnosticsWaitMs} ms`);
|
|
417
|
+
items = entry.diagnostics.get(doc.uri) ?? [];
|
|
418
|
+
}
|
|
419
|
+
for (const item of items) {
|
|
420
|
+
if (collected.length + out.length >= max)
|
|
421
|
+
break;
|
|
422
|
+
if (!isLspDiagnostic(item))
|
|
423
|
+
continue;
|
|
424
|
+
collected.push(toDiagnosticInfo(doc.rel, item));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return { collected, meta: this.meta(entry) };
|
|
428
|
+
});
|
|
429
|
+
out.push(...partial.collected);
|
|
430
|
+
meta ??= partial.meta;
|
|
431
|
+
}
|
|
432
|
+
const finalMeta = meta ?? { language: [...groups.keys()].join(','), server: '', degraded: false };
|
|
433
|
+
if (groups.size > 1)
|
|
434
|
+
finalMeta.language = [...groups.keys()].join(',');
|
|
435
|
+
if (degraded.length > 0) {
|
|
436
|
+
finalMeta.degraded = true;
|
|
437
|
+
finalMeta.degradedReason = degraded.join('; ').slice(0, 500);
|
|
438
|
+
}
|
|
439
|
+
return { diagnostics: out, meta: finalMeta };
|
|
440
|
+
}
|
|
441
|
+
async shutdown() {
|
|
442
|
+
this.closed = true;
|
|
443
|
+
const entries = [...this.servers.values()];
|
|
444
|
+
this.servers.clear();
|
|
445
|
+
await Promise.all(entries.map(async (entry) => {
|
|
446
|
+
if (entry.idleTimer)
|
|
447
|
+
clearTimeout(entry.idleTimer);
|
|
448
|
+
await entry.client.shutdown(SHUTDOWN_GRACE_MS);
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// Server lifecycle
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
requireLanguage(rel) {
|
|
455
|
+
const normalized = this.wfs.normalizeRel(rel);
|
|
456
|
+
const language = this.languageFor(normalized);
|
|
457
|
+
if (language === undefined) {
|
|
458
|
+
const ext = path.posix.extname(normalized) || '(none)';
|
|
459
|
+
throw new DodoError('UNSUPPORTED_LANGUAGE', `no language server is registered for files with extension ${ext}`, {
|
|
460
|
+
recovery: 'register a language server for this extension in the owner config (lsp section)',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return language;
|
|
464
|
+
}
|
|
465
|
+
unsupported(language, reason) {
|
|
466
|
+
return new DodoError('UNSUPPORTED_LANGUAGE', reason ?? `no language server registered for "${language}"`, {
|
|
467
|
+
recovery: 'install the language server and register its command in the owner config (lsp section)',
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
resolveCommand(config) {
|
|
471
|
+
// Preserve the specific denial used by clients, before the shared resolver
|
|
472
|
+
// performs its additional canonical/symlink/PATHEXT checks.
|
|
473
|
+
if (path.isAbsolute(config.command) && this.insideRoot(config.command)) {
|
|
474
|
+
return { ok: false, reason: 'LSP executable is inside the workspace root; install the server outside the workspace' };
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
const abs = resolveTrustedExecutable(config.command, this.wfs.root, { allowAbsolute: true, allowBatch: false });
|
|
478
|
+
if (process.platform === 'win32' && !/\.exe$/i.test(abs))
|
|
479
|
+
throw new Error('Windows LSP requires an .exe, never a .cmd/.bat shim');
|
|
480
|
+
return { ok: true, abs };
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
return { ok: false, reason: `${error instanceof Error ? error.message : 'unavailable'}; install the server outside the workspace; for Windows npm shims register node.exe with the server JavaScript entrypoint in args` };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
insideRoot(abs) {
|
|
487
|
+
return isWithinPath(this.wfs.root, abs);
|
|
488
|
+
}
|
|
489
|
+
async ensureServer(language) {
|
|
490
|
+
if (this.closed)
|
|
491
|
+
throw new DodoError('INTERNAL_ERROR', 'LSP service is shut down');
|
|
492
|
+
const existing = this.servers.get(language);
|
|
493
|
+
if (existing) {
|
|
494
|
+
if (existing.client.alive) {
|
|
495
|
+
await existing.ready;
|
|
496
|
+
return existing;
|
|
497
|
+
}
|
|
498
|
+
this.dropServer(existing, existing.client.exit?.reason ?? 'exited');
|
|
499
|
+
}
|
|
500
|
+
const config = this.registry[language];
|
|
501
|
+
if (!config)
|
|
502
|
+
throw this.unsupported(language);
|
|
503
|
+
const resolved = this.resolveCommand(config);
|
|
504
|
+
if (!resolved.ok)
|
|
505
|
+
throw this.unsupported(language, `language server for ${language} is unavailable: ${resolved.reason}`);
|
|
506
|
+
let client;
|
|
507
|
+
try {
|
|
508
|
+
client = new JsonRpcStdioClient({
|
|
509
|
+
command: resolved.abs,
|
|
510
|
+
args: config.args,
|
|
511
|
+
cwd: this.wfs.root,
|
|
512
|
+
env: buildChildEnv({ parentEnv: process.env, workspaceRoot: this.wfs.root, extraAllowlist: [] }),
|
|
513
|
+
maxMessageBytes: MAX_MESSAGE_BYTES,
|
|
514
|
+
log: (line) => this.log(`[lsp:${language}] ${line}`),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
throw this.unsupported(language, `cannot start language server "${config.command}": ${err.message}`);
|
|
519
|
+
}
|
|
520
|
+
const entry = {
|
|
521
|
+
language,
|
|
522
|
+
command: config.command,
|
|
523
|
+
client,
|
|
524
|
+
ready: Promise.resolve(),
|
|
525
|
+
capabilities: {},
|
|
526
|
+
serverName: path.basename(config.command),
|
|
527
|
+
open: new Map(),
|
|
528
|
+
diagnostics: new Map(),
|
|
529
|
+
diagWaiters: new Map(),
|
|
530
|
+
activeProgress: new Set(),
|
|
531
|
+
queue: Promise.resolve(),
|
|
532
|
+
idleTimer: undefined,
|
|
533
|
+
};
|
|
534
|
+
entry.ready = this.initialize(entry);
|
|
535
|
+
this.servers.set(language, entry);
|
|
536
|
+
client.onExit((info) => {
|
|
537
|
+
if (this.servers.get(language) === entry) {
|
|
538
|
+
this.log(`[lsp:${language}] server ${info.reason}; it will be restarted on the next request`);
|
|
539
|
+
this.dropServer(entry, info.reason);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
try {
|
|
543
|
+
await entry.ready;
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
this.dropServer(entry, 'initialize failed');
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
549
|
+
this.touch(entry);
|
|
550
|
+
return entry;
|
|
551
|
+
}
|
|
552
|
+
async initialize(entry) {
|
|
553
|
+
const { client, language, command } = entry;
|
|
554
|
+
try {
|
|
555
|
+
await client.waitForSpawn();
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
throw this.unsupported(language, `cannot start language server "${command}": ${err.message}`);
|
|
559
|
+
}
|
|
560
|
+
const rootUri = pathToFileURL(this.wfs.root).href;
|
|
561
|
+
const folders = [{ uri: rootUri, name: path.basename(this.wfs.root) || 'workspace' }];
|
|
562
|
+
client.onNotification('textDocument/publishDiagnostics', (params) => this.onPublishDiagnostics(entry, params));
|
|
563
|
+
client.onNotification('window/logMessage', (params) => {
|
|
564
|
+
if (isRecord(params) && typeof params['message'] === 'string')
|
|
565
|
+
this.log(`[lsp:${language}] ${params['message'].slice(0, 300)}`);
|
|
566
|
+
});
|
|
567
|
+
client.onNotification('$/progress', (params) => {
|
|
568
|
+
if (!isRecord(params) || !isRecord(params['value']))
|
|
569
|
+
return;
|
|
570
|
+
const token = params['token'];
|
|
571
|
+
if (typeof token !== 'string' && typeof token !== 'number')
|
|
572
|
+
return;
|
|
573
|
+
const kind = params['value']['kind'];
|
|
574
|
+
if (kind === 'begin' && entry.activeProgress.size < MAX_TRACKED_PROGRESS)
|
|
575
|
+
entry.activeProgress.add(token);
|
|
576
|
+
else if (kind === 'end')
|
|
577
|
+
entry.activeProgress.delete(token);
|
|
578
|
+
});
|
|
579
|
+
client.onRequest('workspace/configuration', (params) => {
|
|
580
|
+
const items = isRecord(params) && Array.isArray(params['items']) ? params['items'] : [];
|
|
581
|
+
return items.map(() => null);
|
|
582
|
+
});
|
|
583
|
+
client.onRequest('workspace/workspaceFolders', () => folders);
|
|
584
|
+
client.onRequest('workspace/applyEdit', () => ({ applied: false, failureReason: 'DODO never applies server-initiated edits' }));
|
|
585
|
+
client.onRequest('window/showDocument', () => ({ success: false }));
|
|
586
|
+
for (const method of [
|
|
587
|
+
'client/registerCapability',
|
|
588
|
+
'client/unregisterCapability',
|
|
589
|
+
'window/workDoneProgress/create',
|
|
590
|
+
'window/showMessageRequest',
|
|
591
|
+
'workspace/diagnostic/refresh',
|
|
592
|
+
'workspace/semanticTokens/refresh',
|
|
593
|
+
'workspace/inlayHint/refresh',
|
|
594
|
+
'workspace/inlineValue/refresh',
|
|
595
|
+
'workspace/codeLens/refresh',
|
|
596
|
+
'workspace/foldingRange/refresh',
|
|
597
|
+
]) {
|
|
598
|
+
client.onRequest(method, () => null);
|
|
599
|
+
}
|
|
600
|
+
let result;
|
|
601
|
+
try {
|
|
602
|
+
result = await client.request('initialize', {
|
|
603
|
+
processId: process.pid,
|
|
604
|
+
clientInfo: { name: 'dodo' },
|
|
605
|
+
locale: 'en',
|
|
606
|
+
rootUri,
|
|
607
|
+
rootPath: this.wfs.root,
|
|
608
|
+
workspaceFolders: folders,
|
|
609
|
+
capabilities: CLIENT_CAPABILITIES,
|
|
610
|
+
initializationOptions: {},
|
|
611
|
+
trace: 'off',
|
|
612
|
+
}, this.limits.semanticRequestTimeoutMs);
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
const detail = err instanceof Error ? err.message : 'unknown error';
|
|
616
|
+
const stderr = client.stderrSnippet;
|
|
617
|
+
throw this.unsupported(language, `language server "${command}" failed to initialize: ${detail}${stderr ? ` (stderr: ${stderr.slice(-200)})` : ''}`);
|
|
618
|
+
}
|
|
619
|
+
if (isRecord(result) && isRecord(result['capabilities']))
|
|
620
|
+
entry.capabilities = result['capabilities'];
|
|
621
|
+
if (isRecord(result) && isRecord(result['serverInfo']) && typeof result['serverInfo']['name'] === 'string') {
|
|
622
|
+
const version = typeof result['serverInfo']['version'] === 'string' ? ` ${result['serverInfo']['version'].slice(0, 32)}` : '';
|
|
623
|
+
entry.serverName = `${result['serverInfo']['name'].slice(0, 64)}${version}`;
|
|
624
|
+
}
|
|
625
|
+
client.notify('initialized', {});
|
|
626
|
+
this.log(`[lsp:${language}] started ${entry.serverName} (pid ${client.pid ?? '?'})`);
|
|
627
|
+
const settleMax = Math.min(STARTUP_SETTLE_MAX_MS, this.limits.semanticRequestTimeoutMs);
|
|
628
|
+
const settled = await this.awaitIdle(entry, settleMax, STARTUP_QUIET_MS, Math.min(STARTUP_MIN_OBSERVE_MS, settleMax));
|
|
629
|
+
if (!settled)
|
|
630
|
+
this.log(`[lsp:${language}] server still busy ${STARTUP_SETTLE_MAX_MS} ms after start; continuing (results may be incomplete until it settles)`);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Resolve true once the server has no active progress work and (when
|
|
634
|
+
* quietMs > 0) no inbound traffic for quietMs; false when maxMs elapses.
|
|
635
|
+
*/
|
|
636
|
+
awaitIdle(entry, maxMs, quietMs, minimumMs = 0) {
|
|
637
|
+
const started = Date.now();
|
|
638
|
+
return new Promise((resolve) => {
|
|
639
|
+
const check = () => {
|
|
640
|
+
if (!entry.client.alive) {
|
|
641
|
+
resolve(true);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const now = Date.now();
|
|
645
|
+
const quietFor = now - entry.client.lastInboundAt;
|
|
646
|
+
if (entry.activeProgress.size === 0 && quietFor >= quietMs && now - started >= minimumMs) {
|
|
647
|
+
resolve(true);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (now - started >= maxMs) {
|
|
651
|
+
resolve(false);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const delay = Math.max(25, Math.min(quietMs > 0 ? quietMs - quietFor : 100, maxMs - (now - started)));
|
|
655
|
+
const timer = setTimeout(check, delay);
|
|
656
|
+
timer.unref();
|
|
657
|
+
};
|
|
658
|
+
check();
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
dropServer(entry, reason) {
|
|
662
|
+
if (this.servers.get(entry.language) === entry)
|
|
663
|
+
this.servers.delete(entry.language);
|
|
664
|
+
if (entry.idleTimer)
|
|
665
|
+
clearTimeout(entry.idleTimer);
|
|
666
|
+
entry.idleTimer = undefined;
|
|
667
|
+
for (const waiters of entry.diagWaiters.values())
|
|
668
|
+
for (const w of waiters)
|
|
669
|
+
w.resolve();
|
|
670
|
+
entry.diagWaiters.clear();
|
|
671
|
+
if (entry.client.alive) {
|
|
672
|
+
this.log(`[lsp:${entry.language}] stopping server (${reason})`);
|
|
673
|
+
entry.client.kill();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
touch(entry) {
|
|
677
|
+
if (entry.idleTimer)
|
|
678
|
+
clearTimeout(entry.idleTimer);
|
|
679
|
+
entry.idleTimer = setTimeout(() => {
|
|
680
|
+
if (this.servers.get(entry.language) !== entry)
|
|
681
|
+
return;
|
|
682
|
+
this.servers.delete(entry.language);
|
|
683
|
+
this.log(`[lsp:${entry.language}] idle for ${IDLE_SHUTDOWN_MS / 60000} minutes; shutting down`);
|
|
684
|
+
void entry.client.shutdown(SHUTDOWN_GRACE_MS);
|
|
685
|
+
}, IDLE_SHUTDOWN_MS);
|
|
686
|
+
entry.idleTimer.unref();
|
|
687
|
+
}
|
|
688
|
+
/** Run one operation against a language's server, serialized per server. */
|
|
689
|
+
async withServer(language, fn) {
|
|
690
|
+
const entry = await this.ensureServer(language);
|
|
691
|
+
const run = entry.queue.then(async () => {
|
|
692
|
+
// Give reported background work (indexing/analysis) a bounded chance to finish first.
|
|
693
|
+
if (entry.activeProgress.size > 0)
|
|
694
|
+
await this.awaitIdle(entry, PRE_REQUEST_IDLE_MAX_MS, 0);
|
|
695
|
+
return fn(entry);
|
|
696
|
+
});
|
|
697
|
+
entry.queue = run.then(() => undefined, () => undefined);
|
|
698
|
+
try {
|
|
699
|
+
return await run;
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
if (err instanceof DodoError && err.code === 'TIMEOUT')
|
|
703
|
+
this.dropServer(entry, 'request timed out');
|
|
704
|
+
throw err;
|
|
705
|
+
}
|
|
706
|
+
finally {
|
|
707
|
+
if (this.servers.get(language) === entry)
|
|
708
|
+
this.touch(entry);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async req(entry, method, params) {
|
|
712
|
+
try {
|
|
713
|
+
return await entry.client.request(method, params, this.limits.semanticRequestTimeoutMs);
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
if (err instanceof DodoError)
|
|
717
|
+
throw err;
|
|
718
|
+
if (err instanceof RpcResponseError) {
|
|
719
|
+
const msg = err.message.slice(0, MAX_SERVER_MESSAGE);
|
|
720
|
+
if (err.rpcCode === -32602)
|
|
721
|
+
throw new DodoError('INVALID_INPUT', `language server rejected ${method}: ${msg}`);
|
|
722
|
+
if (err.rpcCode === -32801 || err.rpcCode === -32800) {
|
|
723
|
+
throw new DodoError('INTERNAL_ERROR', `language server could not complete ${method}: ${msg}`, { retryable: true });
|
|
724
|
+
}
|
|
725
|
+
throw new DodoError('INTERNAL_ERROR', `language server error on ${method}: ${msg}`);
|
|
726
|
+
}
|
|
727
|
+
throw new DodoError('INTERNAL_ERROR', `language server failure on ${method}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
requireCapability(entry, capability, method) {
|
|
731
|
+
const value = entry.capabilities[capability];
|
|
732
|
+
if (value === undefined || value === null || value === false) {
|
|
733
|
+
throw new DodoError('UNSUPPORTED_LANGUAGE', `language server ${entry.serverName} does not support ${method}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
meta(entry, degradedReason) {
|
|
737
|
+
const reasons = [];
|
|
738
|
+
if (degradedReason !== undefined)
|
|
739
|
+
reasons.push(degradedReason);
|
|
740
|
+
if (entry.activeProgress.size > 0)
|
|
741
|
+
reasons.push('language server still reports background work (indexing/analysis); results may be incomplete');
|
|
742
|
+
const m = { language: entry.language, server: entry.serverName, degraded: reasons.length > 0 };
|
|
743
|
+
if (reasons.length > 0)
|
|
744
|
+
m.degradedReason = reasons.join('; ');
|
|
745
|
+
return m;
|
|
746
|
+
}
|
|
747
|
+
// ---------------------------------------------------------------------------
|
|
748
|
+
// Documents
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
readDoc(rel) {
|
|
751
|
+
const { rel: normalized, text } = this.wfs.readTextFile(rel, this.limits.readFileBytes);
|
|
752
|
+
const uri = documentUri(pathToFileURL(this.wfs.absOf(normalized)).href);
|
|
753
|
+
if (!uri)
|
|
754
|
+
throw new DodoError('PATH_DENIED', 'cannot create a file URI for the document');
|
|
755
|
+
return { rel: normalized, uri, doc: new TextDoc(text) };
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Tell the server about the file's CURRENT content: didOpen the first time,
|
|
759
|
+
* a full-content didChange afterwards (documents stay open, LRU-bounded).
|
|
760
|
+
* `onVersion` runs before the notification is sent so callers can arm a
|
|
761
|
+
* diagnostics waiter for exactly this version.
|
|
762
|
+
*/
|
|
763
|
+
openDoc(entry, doc, onVersion) {
|
|
764
|
+
const existing = entry.open.get(doc.uri);
|
|
765
|
+
if (existing) {
|
|
766
|
+
const version = existing.version + 1;
|
|
767
|
+
entry.open.delete(doc.uri);
|
|
768
|
+
entry.open.set(doc.uri, { version });
|
|
769
|
+
onVersion?.(version);
|
|
770
|
+
entry.client.notify('textDocument/didChange', { textDocument: { uri: doc.uri, version }, contentChanges: [{ text: doc.doc.text }] });
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
while (entry.open.size >= MAX_OPEN_DOCS) {
|
|
774
|
+
const oldest = entry.open.keys().next().value;
|
|
775
|
+
if (oldest === undefined)
|
|
776
|
+
break;
|
|
777
|
+
entry.open.delete(oldest);
|
|
778
|
+
entry.client.notify('textDocument/didClose', { textDocument: { uri: oldest } });
|
|
779
|
+
}
|
|
780
|
+
const version = 1;
|
|
781
|
+
entry.open.set(doc.uri, { version });
|
|
782
|
+
onVersion?.(version);
|
|
783
|
+
entry.client.notify('textDocument/didOpen', { textDocument: { uri: doc.uri, languageId: entry.language, version, text: doc.doc.text } });
|
|
784
|
+
}
|
|
785
|
+
onPublishDiagnostics(entry, params) {
|
|
786
|
+
if (!isRecord(params) || typeof params['uri'] !== 'string' || !Array.isArray(params['diagnostics']))
|
|
787
|
+
return;
|
|
788
|
+
const uri = documentUri(params['uri']);
|
|
789
|
+
if (!uri || !entry.open.has(uri))
|
|
790
|
+
return;
|
|
791
|
+
entry.diagnostics.delete(uri);
|
|
792
|
+
entry.diagnostics.set(uri, params['diagnostics']);
|
|
793
|
+
while (entry.diagnostics.size > MAX_STORED_DIAGNOSTIC_URIS) {
|
|
794
|
+
const oldest = entry.diagnostics.keys().next().value;
|
|
795
|
+
if (oldest === undefined)
|
|
796
|
+
break;
|
|
797
|
+
entry.diagnostics.delete(oldest);
|
|
798
|
+
}
|
|
799
|
+
const version = typeof params['version'] === 'number' ? params['version'] : undefined;
|
|
800
|
+
const waiters = entry.diagWaiters.get(uri);
|
|
801
|
+
if (!waiters)
|
|
802
|
+
return;
|
|
803
|
+
const keep = [];
|
|
804
|
+
for (const w of waiters) {
|
|
805
|
+
if (version === undefined || version >= w.version)
|
|
806
|
+
w.resolve();
|
|
807
|
+
else
|
|
808
|
+
keep.push(w);
|
|
809
|
+
}
|
|
810
|
+
if (keep.length > 0)
|
|
811
|
+
entry.diagWaiters.set(uri, keep);
|
|
812
|
+
else
|
|
813
|
+
entry.diagWaiters.delete(uri);
|
|
814
|
+
}
|
|
815
|
+
waitForDiagnostics(entry, uri, version) {
|
|
816
|
+
return new Promise((resolve) => {
|
|
817
|
+
const waiter = { version, resolve: () => resolve(true) };
|
|
818
|
+
const timer = setTimeout(() => {
|
|
819
|
+
const list = entry.diagWaiters.get(uri);
|
|
820
|
+
if (list) {
|
|
821
|
+
const rest = list.filter((w) => w !== waiter);
|
|
822
|
+
if (rest.length > 0)
|
|
823
|
+
entry.diagWaiters.set(uri, rest);
|
|
824
|
+
else
|
|
825
|
+
entry.diagWaiters.delete(uri);
|
|
826
|
+
}
|
|
827
|
+
resolve(false);
|
|
828
|
+
}, this.diagnosticsWaitMs);
|
|
829
|
+
timer.unref();
|
|
830
|
+
waiter.resolve = () => {
|
|
831
|
+
clearTimeout(timer);
|
|
832
|
+
resolve(true);
|
|
833
|
+
};
|
|
834
|
+
const list = entry.diagWaiters.get(uri) ?? [];
|
|
835
|
+
list.push(waiter);
|
|
836
|
+
entry.diagWaiters.set(uri, list);
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
// ---------------------------------------------------------------------------
|
|
840
|
+
// Scope + mapping
|
|
841
|
+
// ---------------------------------------------------------------------------
|
|
842
|
+
/** Server URI -> workspace-relative POSIX path, or undefined when out of scope (never disclosed). */
|
|
843
|
+
uriToRel(uri) {
|
|
844
|
+
if (!uri.startsWith('file:'))
|
|
845
|
+
return undefined;
|
|
846
|
+
let abs;
|
|
847
|
+
try {
|
|
848
|
+
abs = fileURLToPath(uri);
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
return undefined;
|
|
852
|
+
}
|
|
853
|
+
const rel = path.relative(this.wfs.root, abs);
|
|
854
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel))
|
|
855
|
+
return undefined;
|
|
856
|
+
const posix = rel.split(path.sep).join('/');
|
|
857
|
+
let normalized;
|
|
858
|
+
try {
|
|
859
|
+
normalized = this.wfs.normalizeRel(posix);
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
return undefined;
|
|
863
|
+
}
|
|
864
|
+
if (normalized === '.')
|
|
865
|
+
return undefined;
|
|
866
|
+
for (const segment of normalized.split('/'))
|
|
867
|
+
if (EXCLUDED_DIR_SEGMENTS.has(segment))
|
|
868
|
+
return undefined;
|
|
869
|
+
if (this.wfs.ignores.isSecret(normalized) || this.wfs.ignores.isProtected(normalized))
|
|
870
|
+
return undefined;
|
|
871
|
+
return normalized;
|
|
872
|
+
}
|
|
873
|
+
/** Current text of an in-scope file (policy-checked read), cached per operation. */
|
|
874
|
+
textOf(rel, cache) {
|
|
875
|
+
const cached = cache.get(rel);
|
|
876
|
+
if (cached)
|
|
877
|
+
return cached;
|
|
878
|
+
try {
|
|
879
|
+
const doc = new TextDoc(this.wfs.readTextFile(rel, this.limits.readFileBytes).text);
|
|
880
|
+
cache.set(rel, doc);
|
|
881
|
+
return doc;
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
return undefined;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async definitionKeys(entry, uri, position) {
|
|
888
|
+
const keys = new Set();
|
|
889
|
+
if (!entry.capabilities['definitionProvider'])
|
|
890
|
+
return keys;
|
|
891
|
+
try {
|
|
892
|
+
const res = await this.req(entry, 'textDocument/definition', { textDocument: { uri }, position });
|
|
893
|
+
const list = Array.isArray(res) ? res : res === null || res === undefined ? [] : [res];
|
|
894
|
+
for (const item of list) {
|
|
895
|
+
if (isLspLocation(item))
|
|
896
|
+
keys.add(locationKey(item.uri, item.range.start));
|
|
897
|
+
else if (isRecord(item) && typeof item['targetUri'] === 'string' && isLspRange(item['targetSelectionRange'])) {
|
|
898
|
+
keys.add(locationKey(item['targetUri'], item['targetSelectionRange'].start));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
if (err instanceof DodoError && err.code === 'TIMEOUT')
|
|
904
|
+
throw err;
|
|
905
|
+
/* best effort: isDefinition stays false */
|
|
906
|
+
}
|
|
907
|
+
return keys;
|
|
908
|
+
}
|
|
909
|
+
/** Flattens DocumentSymbol[] (hierarchical) or SymbolInformation[]/WorkspaceSymbol[] (flat). */
|
|
910
|
+
flattenSymbols(items, fileRel, container, out, max, depth) {
|
|
911
|
+
for (const item of items) {
|
|
912
|
+
if (out.length >= max)
|
|
913
|
+
return;
|
|
914
|
+
if (!isRecord(item) || typeof item['name'] !== 'string')
|
|
915
|
+
continue;
|
|
916
|
+
const name = item['name'].slice(0, 256);
|
|
917
|
+
if (isLspRange(item['range'])) {
|
|
918
|
+
// Hierarchical DocumentSymbol (always about the requested file).
|
|
919
|
+
if (fileRel === undefined)
|
|
920
|
+
continue;
|
|
921
|
+
out.push(makeSymbol(name, item['kind'], fileRel, item['range'], container));
|
|
922
|
+
if (Array.isArray(item['children']) && depth < 32)
|
|
923
|
+
this.flattenSymbols(item['children'], fileRel, name, out, max, depth + 1);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const location = item['location'];
|
|
927
|
+
if (!isRecord(location) || typeof location['uri'] !== 'string')
|
|
928
|
+
continue;
|
|
929
|
+
const rel = this.uriToRel(location['uri']);
|
|
930
|
+
if (rel === undefined)
|
|
931
|
+
continue; // never leak paths outside the root
|
|
932
|
+
const range = isLspRange(location['range']) ? location['range'] : { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
|
|
933
|
+
const containerName = typeof item['containerName'] === 'string' ? item['containerName'] : container;
|
|
934
|
+
out.push(makeSymbol(name, item['kind'], rel, range, containerName));
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// ---------------------------------------------------------------------------
|
|
939
|
+
// Pure helpers
|
|
940
|
+
// ---------------------------------------------------------------------------
|
|
941
|
+
function boundedMax(maxItems) {
|
|
942
|
+
if (!Number.isInteger(maxItems) || maxItems < 1)
|
|
943
|
+
throw new DodoError('INVALID_INPUT', 'maxItems must be a positive integer');
|
|
944
|
+
return Math.min(maxItems, 10_000);
|
|
945
|
+
}
|
|
946
|
+
function toLspPosition(doc, pos) {
|
|
947
|
+
if (!isRecord(pos) || !Number.isInteger(pos['line']) || !Number.isInteger(pos['column']) || pos.line < 1 || pos.column < 1) {
|
|
948
|
+
throw new DodoError('INVALID_INPUT', 'position must have 1-based integer line and column');
|
|
949
|
+
}
|
|
950
|
+
const line0 = pos.line - 1;
|
|
951
|
+
if (line0 >= doc.lineCount || pos.column - 1 > doc.lineLength(line0))
|
|
952
|
+
throw new DodoError('INVALID_INPUT', 'position out of range');
|
|
953
|
+
return { line: line0, character: pos.column - 1 };
|
|
954
|
+
}
|
|
955
|
+
function locationKey(uri, pos) {
|
|
956
|
+
return `${uri} ${pos.line}:${pos.character}`;
|
|
957
|
+
}
|
|
958
|
+
function makeSymbol(name, kind, rel, range, containerName) {
|
|
959
|
+
const info = {
|
|
960
|
+
name,
|
|
961
|
+
kind: symbolKindName(kind),
|
|
962
|
+
path: rel,
|
|
963
|
+
line: range.start.line + 1,
|
|
964
|
+
column: range.start.character + 1,
|
|
965
|
+
endLine: range.end.line + 1,
|
|
966
|
+
endColumn: range.end.character + 1,
|
|
967
|
+
};
|
|
968
|
+
if (containerName !== undefined && containerName !== '')
|
|
969
|
+
info.containerName = containerName.slice(0, 256);
|
|
970
|
+
return info;
|
|
971
|
+
}
|
|
972
|
+
/** WorkspaceEdit -> uri -> TextEdit[] from both `changes` and `documentChanges` (TextDocumentEdit). */
|
|
973
|
+
function collectWorkspaceEdit(edit) {
|
|
974
|
+
const byUri = new Map();
|
|
975
|
+
const add = (uri, edits) => {
|
|
976
|
+
if (!Array.isArray(edits))
|
|
977
|
+
return;
|
|
978
|
+
const list = byUri.get(uri) ?? [];
|
|
979
|
+
for (const e of edits)
|
|
980
|
+
if (isLspTextEdit(e))
|
|
981
|
+
list.push(e);
|
|
982
|
+
byUri.set(uri, list);
|
|
983
|
+
};
|
|
984
|
+
if (isRecord(edit['changes'])) {
|
|
985
|
+
for (const [uri, edits] of Object.entries(edit['changes']))
|
|
986
|
+
add(uri, edits);
|
|
987
|
+
}
|
|
988
|
+
if (Array.isArray(edit['documentChanges'])) {
|
|
989
|
+
for (const change of edit['documentChanges']) {
|
|
990
|
+
if (!isRecord(change))
|
|
991
|
+
continue;
|
|
992
|
+
const td = change['textDocument'];
|
|
993
|
+
if (isRecord(td) && typeof td['uri'] === 'string')
|
|
994
|
+
add(td['uri'], change['edits']);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return byUri;
|
|
998
|
+
}
|
|
999
|
+
/** Create/rename/delete-file operations are never applied; they count as out of scope. */
|
|
1000
|
+
function countResourceOperations(edit) {
|
|
1001
|
+
if (!Array.isArray(edit['documentChanges']))
|
|
1002
|
+
return 0;
|
|
1003
|
+
let n = 0;
|
|
1004
|
+
for (const change of edit['documentChanges'])
|
|
1005
|
+
if (isRecord(change) && typeof change['kind'] === 'string')
|
|
1006
|
+
n += 1;
|
|
1007
|
+
return n;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* LSP ranges (UTF-16 positions) -> UTF-8 BYTE offsets in the file's current
|
|
1011
|
+
* bytes, sorted by start - exactly the contract of the TypeScript worker.
|
|
1012
|
+
*/
|
|
1013
|
+
function toByteEdits(doc, edits) {
|
|
1014
|
+
const chars = edits
|
|
1015
|
+
.map((e) => {
|
|
1016
|
+
const startChar = doc.clampOffset(e.range.start);
|
|
1017
|
+
const endChar = Math.max(startChar, doc.clampOffset(e.range.end));
|
|
1018
|
+
return { startChar, endChar, newText: e.newText };
|
|
1019
|
+
})
|
|
1020
|
+
.sort((a, b) => a.startChar - b.startChar || a.endChar - b.endChar);
|
|
1021
|
+
const out = [];
|
|
1022
|
+
let prevChar = 0;
|
|
1023
|
+
let prevByte = 0;
|
|
1024
|
+
for (const e of chars) {
|
|
1025
|
+
if (e.startChar < prevChar) {
|
|
1026
|
+
// Overlapping edit: recompute from the beginning rather than trust the running total.
|
|
1027
|
+
prevChar = 0;
|
|
1028
|
+
prevByte = 0;
|
|
1029
|
+
}
|
|
1030
|
+
const start = prevByte + Buffer.byteLength(doc.text.slice(prevChar, e.startChar), 'utf8');
|
|
1031
|
+
const originalText = doc.text.slice(e.startChar, e.endChar);
|
|
1032
|
+
const end = start + Buffer.byteLength(originalText, 'utf8');
|
|
1033
|
+
out.push({ start, end, newText: e.newText, originalText, startChar: e.startChar, endChar: e.endChar });
|
|
1034
|
+
prevChar = e.endChar;
|
|
1035
|
+
prevByte = end;
|
|
1036
|
+
}
|
|
1037
|
+
return out;
|
|
1038
|
+
}
|
|
1039
|
+
function toDiagnosticInfo(rel, d) {
|
|
1040
|
+
const raw = d;
|
|
1041
|
+
const severity = typeof raw.severity === 'number' ? raw.severity : 1;
|
|
1042
|
+
const category = severity === 1 ? 'error' : severity === 2 ? 'warning' : severity === 4 ? 'suggestion' : 'message';
|
|
1043
|
+
let code = 0;
|
|
1044
|
+
let message = d.message.slice(0, 500);
|
|
1045
|
+
if (typeof raw.code === 'number' && Number.isFinite(raw.code))
|
|
1046
|
+
code = raw.code;
|
|
1047
|
+
else if (typeof raw.code === 'string' && raw.code !== '')
|
|
1048
|
+
message = `${message} (${raw.code.slice(0, 80)})`;
|
|
1049
|
+
return { path: rel, line: d.range.start.line + 1, column: d.range.start.character + 1, category, code, message };
|
|
1050
|
+
}
|
|
1051
|
+
//# sourceMappingURL=lspService.js.map
|