From 047cac209bbd171d556c543dc771359a3c4a9071 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:40:40 +0800 Subject: [PATCH] feat(plugins): plugin API 1.4 (#2207) Plugin API 1.4: grants for unencrypted HTTP, local sockets, IDE commands and owned database sources; window status items, output channels, content tabs, editor selections, decorations, search, Problems and VCS reads; runs, keyed terminals with exit events, plugin run targets, dynamic run configurations and test parsers; language features, injections, snippets and menu locations; plugin MCP tools on the built-in server; four official API examples. Released 1.3 plugins keep working. --- Cargo.lock | 145 +- crates/ide-core/src/event.rs | 2 + crates/ide-core/src/remote_settings.rs | 13 + crates/ide-core/src/settings.rs | 7 + crates/ide-core/src/workflow_settings.rs | 10 +- crates/ide-db/Cargo.toml | 3 + crates/ide-db/src/lib.rs | 38 +- crates/ide-db/src/plugin.rs | 271 ++++ crates/ide-db/src/source.rs | 8 + crates/ide-extension/src/capability.rs | 189 ++- crates/ide-extension/src/ledger.rs | 71 + crates/ide-extension/src/lib.rs | 2 +- crates/ide-extension/src/manifest.rs | 69 +- crates/ide-host/Cargo.toml | 1 + crates/ide-host/src/formatter.rs | 32 + crates/ide-host/src/lib.rs | 30 + crates/ide-host/src/mcp/mod.rs | 57 +- crates/ide-host/src/mcp/reports.rs | 6 +- crates/ide-host/src/ops/db.rs | 7 +- crates/ide-host/src/ops/doc/commands.rs | 51 +- crates/ide-host/src/ops/lsp.rs | 282 +++- crates/ide-host/src/ops/run.rs | 73 +- crates/ide-host/src/ops/search.rs | 123 +- crates/ide-host/src/plugins/api.rs | 1402 ++++++++++++++++- crates/ide-host/src/plugins/auth.rs | 205 +++ crates/ide-host/src/plugins/database.rs | 225 +++ crates/ide-host/src/plugins/diagnostics.rs | 27 +- crates/ide-host/src/plugins/endpoints.rs | 2 +- crates/ide-host/src/plugins/execution.rs | 534 +++++++ crates/ide-host/src/plugins/index_ops.rs | 1 + crates/ide-host/src/plugins/injections.rs | 126 ++ crates/ide-host/src/plugins/ipc.rs | 174 ++ .../ide-host/src/plugins/language_features.rs | 462 ++++++ crates/ide-host/src/plugins/mcp.rs | 176 +++ crates/ide-host/src/plugins/mod.rs | 197 ++- crates/ide-host/src/plugins/navigation.rs | 1 + crates/ide-host/src/plugins/points.rs | 26 +- crates/ide-host/src/plugins/providers.rs | 175 +- crates/ide-host/src/plugins/remote_host.rs | 334 +++- crates/ide-host/src/plugins/run_detectors.rs | 85 +- crates/ide-host/src/plugins/run_targets.rs | 150 ++ crates/ide-host/src/plugins/snippets.rs | 106 ++ crates/ide-host/src/plugins/types.rs | 74 + crates/ide-host/src/plugins/ui_points.rs | 31 +- crates/ide-host/src/plugins/web.rs | 24 +- crates/ide-host/src/rpc.rs | 2 +- crates/ide-host/src/trust.rs | 5 + crates/ide-host/tests/distribution.rs | 1 + crates/ide-host/tests/plugin_data_tools.rs | 262 +++ crates/ide-host/tests/plugin_indexing.rs | 45 +- .../tests/plugin_language_features.rs | 234 +++ crates/ide-host/tests/plugin_refactoring.rs | 6 +- crates/ide-host/tests/plugin_workbench.rs | 354 +++++ crates/ide-host/tests/plugins.rs | 383 +++++ crates/ide-host/tests/remote_plugins.rs | 461 +++++- crates/ide-http/src/client.rs | 6 + crates/ide-plugin/Cargo.toml | 2 + crates/ide-plugin/src/execution.rs | 153 ++ crates/ide-plugin/src/language_features.rs | 109 ++ crates/ide-plugin/src/lib.rs | 7 + crates/ide-plugin/src/runtime.rs | 764 ++++++++- crates/ide-plugin/src/sandbox.rs | 86 +- crates/ide-plugin/src/types.rs | 189 ++- crates/ide-plugin/src/wit_host.rs | 793 +++++++++- crates/ide-plugin/tests/api_compat.rs | 11 +- .../tests/fixtures/api14/Cargo.lock | 337 ++++ .../tests/fixtures/api14/Cargo.toml | 21 + .../tests/fixtures/api14/dist/plugin.wasm | Bin 0 -> 234726 bytes .../fixtures/api14/dist/plugin.wasm.sources | 1 + .../tests/fixtures/api14/plugin.toml | 137 ++ .../tests/fixtures/api14/settings.json | 1 + .../tests/fixtures/api14/src/lib.rs | 158 ++ .../tests/fixtures/commit/dist/plugin.wasm | Bin 159965 -> 160029 bytes .../fixtures/commit/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/data-tools/Cargo.lock | 337 ++++ .../tests/fixtures/data-tools/Cargo.toml | 21 + .../tests/fixtures/data-tools/arguments.json | 1 + .../fixtures/data-tools/dist/plugin.wasm | Bin 0 -> 173468 bytes .../data-tools/dist/plugin.wasm.sources | 1 + .../tests/fixtures/data-tools/plugin.toml | 68 + .../tests/fixtures/data-tools/src/lib.rs | 39 + .../fixtures/detail-views/dist/plugin.wasm | Bin 178062 -> 178126 bytes .../detail-views/dist/plugin.wasm.sources | 2 +- .../fixtures/diagnostics/dist/plugin.wasm | Bin 162366 -> 162302 bytes .../diagnostics/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/endpoints/dist/plugin.wasm | Bin 151193 -> 151257 bytes .../endpoints/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/execution/Cargo.lock | 337 ++++ .../tests/fixtures/execution/Cargo.toml | 21 + .../tests/fixtures/execution/dist/plugin.wasm | Bin 0 -> 199189 bytes .../execution/dist/plugin.wasm.sources | 1 + .../tests/fixtures/execution/plugin.toml | 52 + .../tests/fixtures/execution/src/lib.rs | 113 ++ .../fixtures/file-templates/dist/plugin.wasm | Bin 142018 -> 142082 bytes .../file-templates/dist/plugin.wasm.sources | 2 +- .../fixtures/files-events/dist/plugin.wasm | Bin 160431 -> 160698 bytes .../files-events/dist/plugin.wasm.sources | 2 +- .../fixtures/grouped-views/dist/plugin.wasm | Bin 163159 -> 163223 bytes .../grouped-views/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/hello/dist/plugin.wasm | Bin 203335 -> 203399 bytes .../fixtures/hello/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/hover/dist/plugin.wasm | Bin 149305 -> 149369 bytes .../fixtures/hover/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/indexer/dist/plugin.wasm | Bin 152732 -> 152796 bytes .../fixtures/indexer/dist/plugin.wasm.sources | 2 +- .../fixtures/inlay-hints/dist/plugin.wasm | Bin 151108 -> 151172 bytes .../inlay-hints/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/jobs/dist/plugin.wasm | Bin 219582 -> 219646 bytes .../fixtures/jobs/dist/plugin.wasm.sources | 2 +- .../fixtures/language-features/Cargo.lock | 336 ++++ .../fixtures/language-features/Cargo.toml | 20 + .../language-features/dist/plugin.wasm | Bin 0 -> 224349 bytes .../dist/plugin.wasm.sources | 1 + .../fixtures/language-features/plugin.toml | 31 + .../fixtures/language-features/snippets.json | 1 + .../fixtures/language-features/src/lib.rs | 144 ++ .../tests/fixtures/misbehave/dist/plugin.wasm | Bin 138800 -> 138840 bytes .../misbehave/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/misbehave/src/lib.rs | 6 +- .../fixtures/path-events/dist/plugin.wasm | Bin 154410 -> 154658 bytes .../path-events/dist/plugin.wasm.sources | 2 +- .../fixtures/private-dir/dist/plugin.wasm | Bin 179330 -> 179394 bytes .../private-dir/dist/plugin.wasm.sources | 2 +- .../fixtures/semantic-tokens/dist/plugin.wasm | Bin 151641 -> 151705 bytes .../semantic-tokens/dist/plugin.wasm.sources | 2 +- .../fixtures/settings-pages/dist/plugin.wasm | Bin 171304 -> 171368 bytes .../settings-pages/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/storage/dist/plugin.wasm | Bin 144925 -> 144989 bytes .../fixtures/storage/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/symbols/dist/plugin.wasm | Bin 176296 -> 176360 bytes .../fixtures/symbols/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/tables/dist/plugin.wasm | Bin 178466 -> 178530 bytes .../fixtures/tables/dist/plugin.wasm.sources | 2 +- .../fixtures/web-content/dist/plugin.wasm | Bin 142991 -> 143055 bytes .../web-content/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/web-log/dist/plugin.wasm | Bin 147728 -> 147792 bytes .../fixtures/web-log/dist/plugin.wasm.sources | 2 +- .../tests/fixtures/web-pty/dist/plugin.wasm | Bin 142008 -> 142072 bytes .../fixtures/web-pty/dist/plugin.wasm.sources | 2 +- crates/ide-remote/Cargo.toml | 1 + crates/ide-remote/src/lib.rs | 2 + crates/ide-remote/src/plugin_target.rs | 185 +++ crates/ide-remote/src/target.rs | 18 +- crates/ide-sdk/src/apply.rs | 46 +- crates/ide-sdk/src/tests.rs | 28 + crates/ide-vcs/src/cli.rs | 55 +- crates/ide-vcs/src/compare.rs | 14 +- crates/ide-vcs/src/lib.rs | 24 + crates/ide-vcs/src/write.rs | 7 +- deny.toml | 2 +- plugins/README.md | 713 +++++++-- .../official/ai-completion/dist/plugin.wasm | Bin 213845 -> 213909 bytes .../ai-completion/dist/plugin.wasm.sources | 2 +- plugins/official/blame-annotations/Cargo.lock | 336 ++++ plugins/official/blame-annotations/Cargo.toml | 21 + plugins/official/blame-annotations/README.md | 21 + .../blame-annotations/dist/plugin.wasm | Bin 0 -> 187372 bytes .../dist/plugin.wasm.sources | 1 + .../official/blame-annotations/plugin.toml | 18 + plugins/official/blame-annotations/src/lib.rs | 74 + plugins/official/checkstyle/dist/plugin.wasm | Bin 229775 -> 229859 bytes .../checkstyle/dist/plugin.wasm.sources | 2 +- plugins/official/container-runner/Cargo.lock | 336 ++++ plugins/official/container-runner/Cargo.toml | 21 + plugins/official/container-runner/README.md | 33 + .../container-runner/dist/plugin.wasm | Bin 0 -> 161182 bytes .../container-runner/dist/plugin.wasm.sources | 1 + .../container-runner/openidea-example-wrapper | 9 + plugins/official/container-runner/plugin.toml | 15 + plugins/official/container-runner/src/lib.rs | 59 + plugins/official/csv-editor/dist/plugin.wasm | Bin 192664 -> 192728 bytes .../csv-editor/dist/plugin.wasm.sources | 2 +- plugins/official/git-toolbox/dist/plugin.wasm | Bin 170663 -> 170736 bytes .../git-toolbox/dist/plugin.wasm.sources | 2 +- plugins/official/graphviz/dist/plugin.wasm | Bin 183604 -> 183688 bytes .../graphviz/dist/plugin.wasm.sources | 2 +- .../official/key-promoter/dist/plugin.wasm | Bin 161560 -> 161624 bytes .../key-promoter/dist/plugin.wasm.sources | 2 +- .../official/license-header/dist/plugin.wasm | Bin 210785 -> 210869 bytes .../license-header/dist/plugin.wasm.sources | 2 +- plugins/official/mybatis/dist/plugin.wasm | Bin 226511 -> 226575 bytes .../official/mybatis/dist/plugin.wasm.sources | 2 +- .../official/regex-tester/dist/plugin.wasm | Bin 972002 -> 972066 bytes .../regex-tester/dist/plugin.wasm.sources | 2 +- plugins/official/solidity/dist/plugin.wasm | Bin 167525 -> 167589 bytes .../solidity/dist/plugin.wasm.sources | 2 +- .../official/string-tools/dist/plugin.wasm | Bin 227136 -> 227200 bytes .../string-tools/dist/plugin.wasm.sources | 2 +- plugins/official/test-runner/Cargo.lock | 336 ++++ plugins/official/test-runner/Cargo.toml | 21 + plugins/official/test-runner/README.md | 25 + plugins/official/test-runner/dist/plugin.wasm | Bin 0 -> 194786 bytes .../test-runner/dist/plugin.wasm.sources | 1 + plugins/official/test-runner/example.tests | 2 + .../test-runner/openidea-example-tests | 10 + plugins/official/test-runner/plugin.toml | 20 + plugins/official/test-runner/src/lib.rs | 99 ++ plugins/official/todo-finder/Cargo.lock | 337 ++++ plugins/official/todo-finder/Cargo.toml | 23 + plugins/official/todo-finder/README.md | 23 + plugins/official/todo-finder/dist/plugin.wasm | Bin 0 -> 192815 bytes .../todo-finder/dist/plugin.wasm.sources | 1 + plugins/official/todo-finder/plugin.toml | 23 + plugins/official/todo-finder/src/lib.rs | 79 + sdk/rust/openidea-plugin/src/execution.rs | 193 +++ sdk/rust/openidea-plugin/src/host.rs | 512 +++++- .../openidea-plugin/src/language_features.rs | 347 ++++ sdk/rust/openidea-plugin/src/lib.rs | 253 ++- sdk/rust/openidea-plugin/src/mock.rs | 655 +++++++- sdk/rust/openidea-plugin/src/types.rs | 107 +- sdk/rust/openidea-plugin/tests/api14.rs | 62 + .../openidea-plugin/tests/api14_stage2.rs | 94 ++ .../openidea-plugin/tests/api14_stage3.rs | 55 + .../openidea-plugin/tests/api14_stage4.rs | 39 + .../openidea-plugin/tests/api14_stage5.rs | 34 + sdk/rust/openidea-plugin/wit/commit.wit | 2 +- sdk/rust/openidea-plugin/wit/database.wit | 34 + sdk/rust/openidea-plugin/wit/decorations.wit | 22 + sdk/rust/openidea-plugin/wit/details.wit | 2 +- sdk/rust/openidea-plugin/wit/diagnostics.wit | 2 +- sdk/rust/openidea-plugin/wit/endpoints.wit | 2 +- sdk/rust/openidea-plugin/wit/events.wit | 5 +- sdk/rust/openidea-plugin/wit/exports.wit | 2 +- sdk/rust/openidea-plugin/wit/file-actions.wit | 2 +- sdk/rust/openidea-plugin/wit/fs.wit | 2 +- sdk/rust/openidea-plugin/wit/host.wit | 27 +- sdk/rust/openidea-plugin/wit/hover.wit | 2 +- sdk/rust/openidea-plugin/wit/indexing.wit | 2 +- sdk/rust/openidea-plugin/wit/injections.wit | 12 + sdk/rust/openidea-plugin/wit/inlay.wit | 2 +- sdk/rust/openidea-plugin/wit/io.wit | 52 +- .../openidea-plugin/wit/language-features.wit | 114 ++ sdk/rust/openidea-plugin/wit/mcp.wit | 16 + sdk/rust/openidea-plugin/wit/plugin.wit | 34 +- sdk/rust/openidea-plugin/wit/providers.wit | 2 +- sdk/rust/openidea-plugin/wit/run-targets.wit | 36 + sdk/rust/openidea-plugin/wit/run.wit | 50 + sdk/rust/openidea-plugin/wit/semantic.wit | 2 +- sdk/rust/openidea-plugin/wit/types.wit | 2 +- sdk/rust/openidea-plugin/wit/views.wit | 27 +- sdk/rust/openidea-plugin/wit/workspace.wit | 48 +- src-tauri/src/commands/lsp.rs | 26 + src-tauri/src/commands/mcp.rs | 3 + src-tauri/src/commands/mod.rs | 15 + src-tauri/src/commands/plugin.rs | 191 ++- src-tauri/src/remote_dev.rs | 19 +- src/bindings.ts | 504 +++++- src/editor/api.ts | 6 + src/editor/core/view/EditorArea.tsx | 2 + src/editor/lsp/index.ts | 2 + src/editor/lsp/pluginVisuals.ts | 16 + src/editor/lsp/serverCodeLens.test.ts | 40 + src/editor/lsp/serverCodeLens.ts | 81 + src/features/code-insight/ProblemsView.tsx | 2 + src/features/color-preview/index.ts | 7 +- .../color-preview/pluginColors.test.ts | 55 + src/features/color-preview/pluginColors.ts | 153 ++ src/features/database/DatabaseView.tsx | 2 +- src/features/database/SourceDialog.tsx | 7 +- src/features/database/index.ts | 27 + src/features/database/store.ts | 15 +- src/features/debugger/actions.ts | 40 + src/features/mcp/index.ts | 2 +- src/features/mcp/reporters.test.ts | 5 +- src/features/plugins/PluginsPage.module.css | 35 + src/features/plugins/PluginsPage.tsx | 93 +- .../plugins/__tests__/decorations.test.ts | 93 ++ .../plugins/__tests__/logEditor.test.tsx | 28 + .../plugins/__tests__/plugins.test.tsx | 54 + .../plugins/__tests__/projectUi.test.tsx | 25 + .../plugins/__tests__/runTargets.test.ts | 90 ++ src/features/plugins/__tests__/views.test.tsx | 29 + src/features/plugins/backend.ts | 79 +- src/features/plugins/bridge.ts | 2 + src/features/plugins/consent.tsx | 66 +- src/features/plugins/decorations.module.css | 39 + src/features/plugins/decorations.ts | 149 ++ src/features/plugins/index.ts | 6 + src/features/plugins/languageVisuals.test.ts | 72 + src/features/plugins/languageVisuals.ts | 190 +++ src/features/plugins/pluginText.ts | 2 + src/features/plugins/points.tsx | 44 +- src/features/plugins/runTargets.ts | 79 + src/features/plugins/runtimeUi.tsx | 219 ++- src/features/plugins/uiPoints.tsx | 49 +- src/features/plugins/views/contentTab.ts | 12 +- src/features/plugins/views/logEditor.tsx | 13 +- src/features/plugins/views/logTab.ts | 2 +- src/features/plugins/views/outputStore.ts | 13 + src/features/run/ConfigForm.tsx | 14 +- src/features/run/EditConfigsDialog.tsx | 6 +- src/features/run/index.ts | 24 +- src/features/run/remote.test.tsx | 37 +- src/features/run/remote.ts | 33 +- src/features/run/runtimes.test.ts | 13 + src/features/run/runtimes.ts | 18 +- src/features/run/store.ts | 22 +- src/features/run/testing/plugin.test.ts | 49 + src/features/run/testing/plugin.ts | 146 ++ src/features/search/find/FindToolWindow.tsx | 4 + .../settings/__tests__/languages.test.ts | 5 + .../settings/languages/LanguagePages.tsx | 21 + .../languages/PluginFormattersPage.tsx | 56 + src/features/settings/pages/languages.tsx | 10 + src/features/settings/pages/runTargets.tsx | 30 +- .../templates/__tests__/snippets.test.ts | 33 + .../templates/__tests__/views.test.tsx | 22 + src/features/templates/live/model.ts | 2 + src/features/templates/live/snippets.ts | 41 + src/features/templates/live/store.ts | 13 +- .../templates/settings/LiveTemplatesPage.tsx | 7 +- .../templates/settings/Settings.module.css | 12 + src/features/terminal/index.ts | 13 + src/features/terminal/loader.ts | 6 +- src/features/terminal/persist.test.ts | 50 +- src/features/terminal/runtime.ts | 1 + src/features/terminal/session.ts | 50 +- src/features/terminal/store.ts | 41 + src/features/vim/__tests__/vim.test.ts | 18 +- src/features/vim/engine.ts | 52 +- src/features/vim/index.ts | 4 +- src/platform/actions.test.ts | 21 + src/platform/actions.ts | 12 +- src/platform/languageSettings.ts | 16 +- src/platform/locales/text/en.json | 42 +- src/platform/locales/text/zh.json | 41 +- src/platform/locales/textCatalog.test.ts | 9 +- src/platform/pluginRunTargets.ts | 24 + src/platform/remoteSettings.ts | 2 +- src/platform/runTargetSelection.ts | 24 + src/platform/workflowSettings.ts | 4 +- src/workbench/ContributedToolbar.test.tsx | 33 + src/workbench/ContributedToolbar.tsx | 26 + src/workbench/TitleBar.tsx | 2 + src/workbench/contributions.ts | 32 + wit/commit.wit | 2 +- wit/database.wit | 34 + wit/decorations.wit | 22 + wit/details.wit | 2 +- wit/diagnostics.wit | 2 +- wit/endpoints.wit | 2 +- wit/events.wit | 5 +- wit/exports.wit | 2 +- wit/file-actions.wit | 2 +- wit/fs.wit | 2 +- wit/host.wit | 27 +- wit/hover.wit | 2 +- wit/indexing.wit | 2 +- wit/injections.wit | 12 + wit/inlay.wit | 2 +- wit/io.wit | 52 +- wit/language-features.wit | 114 ++ wit/mcp.wit | 16 + wit/plugin.wit | 34 +- wit/providers.wit | 2 +- wit/run-targets.wit | 36 + wit/run.wit | 50 + wit/semantic.wit | 2 +- wit/types.wit | 2 +- wit/views.wit | 27 +- wit/workspace.wit | 48 +- 361 files changed, 21694 insertions(+), 700 deletions(-) create mode 100644 crates/ide-db/src/plugin.rs create mode 100644 crates/ide-host/src/plugins/auth.rs create mode 100644 crates/ide-host/src/plugins/database.rs create mode 100644 crates/ide-host/src/plugins/execution.rs create mode 100644 crates/ide-host/src/plugins/injections.rs create mode 100644 crates/ide-host/src/plugins/ipc.rs create mode 100644 crates/ide-host/src/plugins/language_features.rs create mode 100644 crates/ide-host/src/plugins/mcp.rs create mode 100644 crates/ide-host/src/plugins/run_targets.rs create mode 100644 crates/ide-host/src/plugins/snippets.rs create mode 100644 crates/ide-host/tests/plugin_data_tools.rs create mode 100644 crates/ide-host/tests/plugin_language_features.rs create mode 100644 crates/ide-host/tests/plugin_workbench.rs create mode 100644 crates/ide-plugin/src/execution.rs create mode 100644 crates/ide-plugin/src/language_features.rs create mode 100644 crates/ide-plugin/tests/fixtures/api14/Cargo.lock create mode 100644 crates/ide-plugin/tests/fixtures/api14/Cargo.toml create mode 100644 crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm create mode 100644 crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm.sources create mode 100644 crates/ide-plugin/tests/fixtures/api14/plugin.toml create mode 100644 crates/ide-plugin/tests/fixtures/api14/settings.json create mode 100644 crates/ide-plugin/tests/fixtures/api14/src/lib.rs create mode 100644 crates/ide-plugin/tests/fixtures/data-tools/Cargo.lock create mode 100644 crates/ide-plugin/tests/fixtures/data-tools/Cargo.toml create mode 100644 crates/ide-plugin/tests/fixtures/data-tools/arguments.json create mode 100644 crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm create mode 100644 crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources create mode 100644 crates/ide-plugin/tests/fixtures/data-tools/plugin.toml create mode 100644 crates/ide-plugin/tests/fixtures/data-tools/src/lib.rs create mode 100644 crates/ide-plugin/tests/fixtures/execution/Cargo.lock create mode 100644 crates/ide-plugin/tests/fixtures/execution/Cargo.toml create mode 100644 crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm create mode 100644 crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm.sources create mode 100644 crates/ide-plugin/tests/fixtures/execution/plugin.toml create mode 100644 crates/ide-plugin/tests/fixtures/execution/src/lib.rs create mode 100644 crates/ide-plugin/tests/fixtures/language-features/Cargo.lock create mode 100644 crates/ide-plugin/tests/fixtures/language-features/Cargo.toml create mode 100644 crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm create mode 100644 crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources create mode 100644 crates/ide-plugin/tests/fixtures/language-features/plugin.toml create mode 100644 crates/ide-plugin/tests/fixtures/language-features/snippets.json create mode 100644 crates/ide-plugin/tests/fixtures/language-features/src/lib.rs create mode 100644 crates/ide-remote/src/plugin_target.rs create mode 100644 plugins/official/blame-annotations/Cargo.lock create mode 100644 plugins/official/blame-annotations/Cargo.toml create mode 100644 plugins/official/blame-annotations/README.md create mode 100644 plugins/official/blame-annotations/dist/plugin.wasm create mode 100644 plugins/official/blame-annotations/dist/plugin.wasm.sources create mode 100644 plugins/official/blame-annotations/plugin.toml create mode 100644 plugins/official/blame-annotations/src/lib.rs create mode 100644 plugins/official/container-runner/Cargo.lock create mode 100644 plugins/official/container-runner/Cargo.toml create mode 100644 plugins/official/container-runner/README.md create mode 100644 plugins/official/container-runner/dist/plugin.wasm create mode 100644 plugins/official/container-runner/dist/plugin.wasm.sources create mode 100755 plugins/official/container-runner/openidea-example-wrapper create mode 100644 plugins/official/container-runner/plugin.toml create mode 100644 plugins/official/container-runner/src/lib.rs create mode 100644 plugins/official/test-runner/Cargo.lock create mode 100644 plugins/official/test-runner/Cargo.toml create mode 100644 plugins/official/test-runner/README.md create mode 100644 plugins/official/test-runner/dist/plugin.wasm create mode 100644 plugins/official/test-runner/dist/plugin.wasm.sources create mode 100644 plugins/official/test-runner/example.tests create mode 100755 plugins/official/test-runner/openidea-example-tests create mode 100644 plugins/official/test-runner/plugin.toml create mode 100644 plugins/official/test-runner/src/lib.rs create mode 100644 plugins/official/todo-finder/Cargo.lock create mode 100644 plugins/official/todo-finder/Cargo.toml create mode 100644 plugins/official/todo-finder/README.md create mode 100644 plugins/official/todo-finder/dist/plugin.wasm create mode 100644 plugins/official/todo-finder/dist/plugin.wasm.sources create mode 100644 plugins/official/todo-finder/plugin.toml create mode 100644 plugins/official/todo-finder/src/lib.rs create mode 100644 sdk/rust/openidea-plugin/src/execution.rs create mode 100644 sdk/rust/openidea-plugin/src/language_features.rs create mode 100644 sdk/rust/openidea-plugin/tests/api14.rs create mode 100644 sdk/rust/openidea-plugin/tests/api14_stage2.rs create mode 100644 sdk/rust/openidea-plugin/tests/api14_stage3.rs create mode 100644 sdk/rust/openidea-plugin/tests/api14_stage4.rs create mode 100644 sdk/rust/openidea-plugin/tests/api14_stage5.rs create mode 100644 sdk/rust/openidea-plugin/wit/database.wit create mode 100644 sdk/rust/openidea-plugin/wit/decorations.wit create mode 100644 sdk/rust/openidea-plugin/wit/injections.wit create mode 100644 sdk/rust/openidea-plugin/wit/language-features.wit create mode 100644 sdk/rust/openidea-plugin/wit/mcp.wit create mode 100644 sdk/rust/openidea-plugin/wit/run-targets.wit create mode 100644 sdk/rust/openidea-plugin/wit/run.wit create mode 100644 src/editor/lsp/pluginVisuals.ts create mode 100644 src/editor/lsp/serverCodeLens.test.ts create mode 100644 src/editor/lsp/serverCodeLens.ts create mode 100644 src/features/color-preview/pluginColors.test.ts create mode 100644 src/features/color-preview/pluginColors.ts create mode 100644 src/features/plugins/__tests__/decorations.test.ts create mode 100644 src/features/plugins/__tests__/runTargets.test.ts create mode 100644 src/features/plugins/decorations.module.css create mode 100644 src/features/plugins/decorations.ts create mode 100644 src/features/plugins/languageVisuals.test.ts create mode 100644 src/features/plugins/languageVisuals.ts create mode 100644 src/features/plugins/runTargets.ts create mode 100644 src/features/plugins/views/outputStore.ts create mode 100644 src/features/run/testing/plugin.test.ts create mode 100644 src/features/run/testing/plugin.ts create mode 100644 src/features/settings/languages/PluginFormattersPage.tsx create mode 100644 src/features/templates/__tests__/snippets.test.ts create mode 100644 src/features/templates/live/snippets.ts create mode 100644 src/platform/actions.test.ts create mode 100644 src/platform/pluginRunTargets.ts create mode 100644 src/platform/runTargetSelection.ts create mode 100644 src/workbench/ContributedToolbar.test.tsx create mode 100644 src/workbench/ContributedToolbar.tsx create mode 100644 wit/database.wit create mode 100644 wit/decorations.wit create mode 100644 wit/injections.wit create mode 100644 wit/language-features.wit create mode 100644 wit/mcp.wit create mode 100644 wit/run-targets.wit create mode 100644 wit/run.wit diff --git a/Cargo.lock b/Cargo.lock index 6f1b98b05..e3d45f110 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -616,6 +630,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "borsh" version = "1.8.1" @@ -690,6 +710,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytemuck" version = "1.25.2" @@ -2030,6 +2056,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "embed-resource" version = "3.0.12" @@ -2187,6 +2222,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set 0.8.0", + "regex-automata", + "regex-syntax", +] + [[package]] name = "faster-hex" version = "0.10.1" @@ -2287,6 +2333,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "flume" version = "0.12.0" @@ -2346,6 +2403,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "fs-set-times" version = "0.20.3" @@ -4256,7 +4323,9 @@ name = "ide-db" version = "0.1.39" dependencies = [ "bb8", + "blake3", "dirs 6.0.0", + "dunce", "futures", "ide-core", "ide-http", @@ -4274,6 +4343,7 @@ dependencies = [ "tokio", "tokio-util", "toml 0.9.12+spec-1.1.0", + "toml_edit 0.25.17+spec-1.1.0", "tracing", "uuid", ] @@ -4380,6 +4450,7 @@ dependencies = [ "ide-vfs", "ide-workspace", "ignore", + "jsonschema", "parking_lot", "quick-xml 0.42.0", "regex-lite", @@ -4587,6 +4658,7 @@ name = "ide-plugin" version = "0.1.39" dependencies = [ "blake3", + "dunce", "ide-core", "ide-extension", "parking_lot", @@ -4598,6 +4670,7 @@ dependencies = [ "thiserror 2.0.21", "tokio", "tracing", + "url", "wasmtime", "wasmtime-wasi", "wat", @@ -4620,6 +4693,7 @@ dependencies = [ name = "ide-remote" version = "0.1.39" dependencies = [ + "dunce", "globset", "ide-core", "ide-run", @@ -5287,6 +5361,32 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonschema" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d46662859bc5f60a145b75f4632fbadc84e829e45df6c5de74cfc8e05acb96b5" +dependencies = [ + "ahash", + "base64 0.22.1", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "num-traits", + "once_cell", + "percent-encoding", + "referencing", + "regex", + "regex-syntax", + "serde", + "serde_json", + "uuid-simd", +] + [[package]] name = "jupyter-protocol" version = "2.0.3" @@ -5520,7 +5620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" dependencies = [ "bitflags 1.3.2", - "fluent-uri", + "fluent-uri 0.1.4", "serde", "serde_json", "serde_repr", @@ -5862,6 +5962,12 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + [[package]] name = "num-complex" version = "0.4.6" @@ -6539,6 +6645,12 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "p256" version = "0.14.0" @@ -7306,6 +7418,20 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "referencing" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9c261f7ce75418b3beadfb3f0eb1299fe8eb9640deba45ffa2cb783098697d" +dependencies = [ + "ahash", + "fluent-uri 0.3.2", + "once_cell", + "parking_lot", + "percent-encoding", + "serde_json", +] + [[package]] name = "regalloc2" version = "0.15.2" @@ -10496,6 +10622,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + [[package]] name = "valuable" version = "0.1.1" @@ -10520,6 +10657,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "vswhom" version = "0.1.0" diff --git a/crates/ide-core/src/event.rs b/crates/ide-core/src/event.rs index c543c0e39..3e786e418 100644 --- a/crates/ide-core/src/event.rs +++ b/crates/ide-core/src/event.rs @@ -20,6 +20,8 @@ pub enum McpUiRequest { #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")] pub enum IdeEvent { + /// Project data source settings changed; reload the Database tree. + DbSourcesChanged, McpRequest { id: u32, request: McpUiRequest, diff --git a/crates/ide-core/src/remote_settings.rs b/crates/ide-core/src/remote_settings.rs index 62442a36f..3d1fc5276 100644 --- a/crates/ide-core/src/remote_settings.rs +++ b/crates/ide-core/src/remote_settings.rs @@ -191,6 +191,19 @@ pub struct RunTarget { pub image_user: bool, } +/// A plugin-owned argv wrapper for project runs (API 1.4). +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct PluginRunTarget { + pub id: String, + pub name: String, + pub program: String, + pub args: Vec, + pub workdir_args: Vec, + pub container_root: Option, + pub commands: Vec<(String, Vec)>, +} + #[cfg(test)] mod deployment_tests { use super::*; diff --git a/crates/ide-core/src/settings.rs b/crates/ide-core/src/settings.rs index 8feb6a512..b249c3453 100644 --- a/crates/ide-core/src/settings.rs +++ b/crates/ide-core/src/settings.rs @@ -629,6 +629,8 @@ impl Default for FileSettings { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] #[serde(default, rename_all = "camelCase")] pub struct LanguagesSettings { + /// Explicit plugin formatter, by language id. Empty entries use the normal formatter policy. + pub plugin_formatters: BTreeMap, pub python: PythonSettings, pub node: NodeSettings, pub php: PhpSettings, @@ -646,6 +648,7 @@ pub struct LanguagesSettings { impl Default for LanguagesSettings { fn default() -> Self { Self { + plugin_formatters: BTreeMap::new(), python: PythonSettings::default(), node: NodeSettings::default(), php: PhpSettings::default(), @@ -767,6 +770,8 @@ impl Default for PythonSettings { pub struct PhpSettings { /// `php` CLI executable; empty = the one on `PATH`. pub interpreter: String, + /// Run PHP/Composer or Node/package-manager commands in this target; empty = project default, `local` = local. + pub run_target: String, /// PHP version the language server checks the code against (`7.4`); empty = from /// `composer.json` (`config.platform.php`, else the lowest version `require.php` allows). pub language_level: String, @@ -794,6 +799,8 @@ pub struct RubySettings { pub struct NodeSettings { /// `node` executable; empty = the one on `PATH`. pub interpreter: String, + /// Run PHP/Composer or Node/package-manager commands in this target; empty = project default, `local` = local. + pub run_target: String, /// `npm` | `pnpm` | `yarn`; empty = detect from the lock file / `packageManager`. pub package_manager: String, } diff --git a/crates/ide-core/src/workflow_settings.rs b/crates/ide-core/src/workflow_settings.rs index 5549be1af..293775a55 100644 --- a/crates/ide-core/src/workflow_settings.rs +++ b/crates/ide-core/src/workflow_settings.rs @@ -272,6 +272,8 @@ impl Default for PythonDebuggerSettings { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] #[serde(default, rename_all = "camelCase")] pub struct RunSettings { + /// Target for configurations without an explicit target. Empty = local. + pub default_target: String, pub confirm_rerun: bool, /// Ask before closing a Run tab whose process is still running. pub confirm_terminate: bool, @@ -282,7 +284,13 @@ pub struct RunSettings { impl Default for RunSettings { fn default() -> Self { - Self { confirm_rerun: true, confirm_terminate: true, show_on_start: true, termination_timeout_ms: 3000 } + Self { + default_target: String::new(), + confirm_rerun: true, + confirm_terminate: true, + show_on_start: true, + termination_timeout_ms: 3000, + } } } diff --git a/crates/ide-db/Cargo.toml b/crates/ide-db/Cargo.toml index 709cacc15..0a19ef0af 100644 --- a/crates/ide-db/Cargo.toml +++ b/crates/ide-db/Cargo.toml @@ -7,10 +7,13 @@ license.workspace = true [dependencies] ide-core.workspace = true +dunce.workspace = true serde.workspace = true serde_json = { workspace = true, features = ["raw_value"] } specta.workspace = true toml.workspace = true +toml_edit.workspace = true +blake3.workspace = true tokio.workspace = true tracing.workspace = true parking_lot.workspace = true diff --git a/crates/ide-db/src/lib.rs b/crates/ide-db/src/lib.rs index 79913f3dc..bab76ac77 100644 --- a/crates/ide-db/src/lib.rs +++ b/crates/ide-db/src/lib.rs @@ -7,6 +7,7 @@ pub mod conn; pub mod history; mod oracle; pub mod plan; +pub mod plugin; pub mod secrets; pub mod source; pub mod split; @@ -381,7 +382,9 @@ impl DbManager { /// Where the saved passwords of data source `id` live: app-wide for a global one. fn saved_for(&self, id: &str) -> &SecretStore { match &self.global { - Some((_, store)) if self.global_sources().iter().any(|s| s.id == id) => store, + Some((_, store)) if self.sources().is_ok_and(|sources| sources.iter().any(|s| s.id == id && s.global)) => { + store + } _ => &self.saved_passwords, } } @@ -412,19 +415,28 @@ impl DbManager { /// Saves the data source list; connections of removed or changed sources are closed. /// Global ones (`global`) go to the app-wide list, the others to the project's. pub async fn save_sources(&self, sources: Vec) -> IdeResult<()> { - let old = self.sources().unwrap_or_default(); - let (global, project): (Vec<_>, Vec<_>) = sources.iter().cloned().partition(|s| s.global); - match &self.global { - // Rewritten only when it changes: other projects' windows edit the same file. - Some((dir, _)) => { - if global != old.iter().filter(|s| s.global).cloned().collect::>() { - source::save_global(dir, &global)?; - } - } - None if !global.is_empty() => return Err(IdeError::invalid("global data sources are not available here")), - None => {} + if sources.iter().any(|s| s.global && s.plugin_owner.is_some()) { + return Err(IdeError::invalid("plugin-owned data sources must stay project-scoped")); } - source::save(&self.root, &project)?; + let old = { + let _guard = plugin::EDIT.lock(); + let old = self.sources().unwrap_or_default(); + let (global, project): (Vec<_>, Vec<_>) = sources.iter().cloned().partition(|s| s.global); + match &self.global { + // Rewritten only when it changes: other projects' windows edit the same file. + Some((dir, _)) => { + if global != old.iter().filter(|s| s.global).cloned().collect::>() { + source::save_global(dir, &global)?; + } + } + None if !global.is_empty() => { + return Err(IdeError::invalid("global data sources are not available here")); + } + None => {} + } + source::save(&self.root, &project)?; + old + }; for o in old { match sources.iter().find(|s| s.id == o.id) { Some(n) if *n == o => {} diff --git a/crates/ide-db/src/plugin.rs b/crates/ide-db/src/plugin.rs new file mode 100644 index 000000000..8de3888da --- /dev/null +++ b/crates/ide-db/src/plugin.rs @@ -0,0 +1,271 @@ +//! Ownership checks and surgical updates of plugin-created project sources. +use crate::{DataSource, DbManager, source}; +use ide_core::{IdeError, IdeResult, project_dir}; +use std::path::Path; + +// All DbManagers of the same root share this file. Never hold this across an await. +pub(crate) static EDIT: parking_lot::Mutex<()> = parking_lot::Mutex::new(()); + +/// Host-owned connection id, unambiguous and safe for console paths. +pub fn source_id(plugin: &str, id: &str) -> String { + format!("plugin-{}", blake3::hash(format!("{plugin}\0{id}").as_bytes()).to_hex()) +} + +fn owned(source: &DataSource, plugin: &str, id: &str) -> bool { + source.id == source_id(plugin, id) + && source.plugin_owner.as_deref() == Some(plugin) + && source.plugin_source_id.as_deref() == Some(id) + && !source.global +} + +/// Edits just matching array tables, retaining unrelated entries, comments and formatting. +fn update(root: &Path, remove: impl Fn(&DataSource) -> bool, add: Option<&DataSource>) -> IdeResult> { + let path = project_dir::path(root, "data-sources.toml"); + if let Some(ancestor) = path.ancestors().find(|p| p.exists()) + && root.exists() + && !dunce::canonicalize(ancestor)?.starts_with(dunce::canonicalize(root)?) + { + return Err(IdeError::invalid("data sources file is outside the project")); + } + let text = match std::fs::read_to_string(&path) { + Ok(text) => text, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(), + Err(e) => return Err(e.into()), + }; + let old = source::load(root)?; + let doc = toml_edit::Document::parse(text.as_str()).map_err(|e| IdeError::invalid(format!("data sources: {e}")))?; + let removed: Vec<_> = old.iter().filter(|s| remove(s)).cloned().collect(); + fn line_span(text: &str, span: std::ops::Range, out: &mut Vec>) { + let start = text[..span.start].rfind('\n').map_or(0, |n| n + 1); + let mut end = text[span.end..].find('\n').map_or(text.len(), |n| span.end + n + 1); + while end < text.len() { + let next = text[end..].find('\n').map_or(text.len(), |n| end + n + 1); + if !text[end..next].trim().is_empty() { + break; + } + end = next; + } + out.push(start..end); + } + fn spans(table: &toml_edit::Table, text: &str, out: &mut Vec>) { + if !table.is_implicit() + && let Some(span) = table.span() + { + line_span(text, span, out); + } + for (_, item) in table.iter() { + if let Some(span) = item.as_value().and_then(toml_edit::Value::span) { + line_span(text, span, out); + } + if let Some(table) = item.as_table() { + spans(table, text, out); + } + if let Some(tables) = item.as_array_of_tables() { + for table in tables.iter() { + spans(table, text, out); + } + } + } + } + if doc.get("data-source").is_some_and(|i| i.as_array_of_tables().is_none()) { + return Err(IdeError::invalid("plugin sources require [[data-source]] tables")); + } + let mut ranges = vec![]; + if let Some(tables) = doc.get("data-source").and_then(toml_edit::Item::as_array_of_tables) { + for (table, source) in tables.iter().zip(&old) { + if remove(source) { + spans(table, &text, &mut ranges); + } + } + } + ranges.sort_by_key(|r| r.start); + let mut merged: Vec> = vec![]; + for range in ranges { + if let Some(last) = merged.last_mut() + && range.start <= last.end + { + last.end = last.end.max(range.end); + continue; + } + merged.push(range); + } + let mut output = text.clone(); + for range in merged.into_iter().rev() { + output.replace_range(range, ""); + } + if let Some(add) = add { + let mut value = toml::Value::try_from(add).map_err(|e| IdeError::internal(e.to_string()))?; + value.as_table_mut().unwrap().remove("global"); + let value = + toml::Value::Table(toml::Table::from_iter([("data-source".into(), toml::Value::Array(vec![value]))])); + if !output.is_empty() && !output.ends_with('\n') { + output.push('\n'); + } + output.push_str(&toml::to_string_pretty(&value).map_err(|e| IdeError::internal(e.to_string()))?); + } + if !removed.is_empty() || add.is_some() { + let dir = path.parent().unwrap(); + std::fs::create_dir_all(dir)?; + let tmp = dir.join(format!(".data-sources-{}.tmp", uuid::Uuid::new_v4())); + std::fs::write(&tmp, output)?; + if let Err(e) = std::fs::rename(&tmp, &path) { + let _ = std::fs::remove_file(&tmp); + return Err(e.into()); + } + } + Ok(removed) +} + +/// Removes only this plugin's sources, including when their project is closed at uninstall. +pub fn remove_all(root: &Path, plugin: &str) -> IdeResult> { + let _guard = EDIT.lock(); + update(root, |s| s.plugin_owner.as_deref() == Some(plugin), None) +} + +impl DbManager { + /// Lists private source ids without exposing any connection settings. + pub fn plugin_sources(&self, plugin: &str) -> IdeResult> { + Ok(source::load(&self.root)? + .into_iter() + .filter(|s| s.plugin_owner.as_deref() == Some(plugin)) + .filter_map(|s| s.plugin_source_id) + .collect()) + } + /// Checks the stored owner, even if a user-created source uses the host's id format. + pub fn plugin_source(&self, plugin: &str, id: &str) -> IdeResult { + source::load(&self.root)? + .into_iter() + .find(|s| owned(s, plugin, id)) + .ok_or_else(|| IdeError::not_found("no owned data source with this id")) + } + /// Replaces only an owned source and disconnects its old pools. + pub async fn upsert_plugin_source(&self, plugin: &str, id: &str, mut source: DataSource) -> IdeResult<()> { + source.id = source_id(plugin, id); + source.plugin_owner = Some(plugin.into()); + source.plugin_source_id = Some(id.into()); + source.global = false; + { + let _guard = EDIT.lock(); + let all = self.sources()?; + if all.iter().any(|s| s.id == source.id && !owned(s, plugin, id)) { + return Err(IdeError::not_found("no owned data source with this id")); + } + if !all.iter().any(|s| owned(s, plugin, id)) && self.plugin_sources(plugin)?.len() >= 64 { + return Err(IdeError::invalid("at most 64 sources per plugin and project")); + } + update(&self.root, |s| owned(s, plugin, id), Some(&source))?; + } + self.disconnect_source(&source.id).await; + Ok(()) + } + /// Deletes an owned source and all its saved/session credentials. + pub async fn remove_plugin_source(&self, plugin: &str, id: &str) -> IdeResult<()> { + let source = { + let _guard = EDIT.lock(); + let source = self.plugin_source(plugin, id)?; + update(&self.root, |s| owned(s, plugin, id), None)?; + source + }; + self.forget_plugin_source(&source.id).await; + Ok(()) + } + /// Completes cleanup after removal from the file. + pub async fn forget_plugin_source(&self, id: &str) { + for key in [id.to_owned(), crate::secrets::ssh_key(id)] { + self.session_passwords.lock().remove(&key); + self.saved_passwords.delete(&key); + } + self.disconnect_source(id).await; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{DbDriver, secrets::SecretStore}; + #[tokio::test] + async fn ownership_preserves_unrelated_bytes_and_cleans_credentials() { + ide_ssh::secrets::testing::use_memory_store(); + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("project"); + std::fs::create_dir_all(root.join(".openidea")).unwrap(); + let original = "# user comment\n[[data-source]] # keep\nid = 'user'\nname='Mine'\ndriver='postgres'\n\n[data-source.options]\ntimeZone = 'UTC' # retain\n"; + let file = root.join(".openidea/data-sources.toml"); + std::fs::write(&file, original).unwrap(); + let secrets = tmp.path().join("passwords.json"); + let db = DbManager::new( + root.clone(), + secrets.clone(), + tmp.path().join("history.json"), + std::sync::Arc::new(ide_ssh::SshManager::new(tmp.path().join("ssh.json"))), + ); + let settings = + DataSource { name: "Plugin".into(), driver: DbDriver::Postgres, save_password: true, ..Default::default() }; + db.upsert_plugin_source("a", "local", settings.clone()).await.unwrap(); + assert!(std::fs::read_to_string(&file).unwrap().starts_with(original)); + db.upsert_plugin_source("b", "local", settings.clone()).await.unwrap(); + assert_eq!(db.plugin_sources("a").unwrap(), ["local"]); + assert!(db.plugin_source("a", "user").is_err()); + assert!(db.remove_plugin_source("a", "user").await.is_err()); + let source = db.plugin_source("a", "local").unwrap(); + db.set_password(&source.id, Some("top-secret".into()), true).await.unwrap(); + let store = SecretStore::new(secrets, &root); + assert_eq!(store.get(&source.id).as_deref(), Some("top-secret")); + db.upsert_plugin_source("a", "local", DataSource { name: "Updated".into(), ..settings }).await.unwrap(); + assert_eq!(store.get(&source.id).as_deref(), Some("top-secret")); + db.remove_plugin_source("a", "local").await.unwrap(); + assert!(store.get(&source.id).is_none()); + assert_eq!(db.plugin_sources("b").unwrap(), ["local"]); + remove_all(&root, "b").unwrap(); + assert_eq!(std::fs::read_to_string(&file).unwrap(), original); + } + #[tokio::test] + async fn project_owned_credentials_do_not_replace_a_shadowed_global_sources_password() { + ide_ssh::secrets::testing::use_memory_store(); + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("project"); + let global = tmp.path().join("global"); + let passwords = tmp.path().join("passwords.json"); + let db = DbManager::new( + root.clone(), + passwords.clone(), + tmp.path().join("history.json"), + std::sync::Arc::new(ide_ssh::SshManager::new(tmp.path().join("ssh.json"))), + ) + .with_global_sources(global.clone()); + db.upsert_plugin_source( + "plugin", + "local", + DataSource { name: "Owned".into(), save_password: true, ..Default::default() }, + ) + .await + .unwrap(); + let owned = db.plugin_source("plugin", "local").unwrap(); + source::save_global( + &global, + &[DataSource { id: owned.id.clone(), name: "Global user source".into(), ..Default::default() }], + ) + .unwrap(); + let global_store = SecretStore::new(passwords.clone(), &global); + global_store.set(&owned.id, "user-secret").unwrap(); + db.set_password(&owned.id, Some("plugin-secret".into()), true).await.unwrap(); + assert_eq!(SecretStore::new(passwords, &root).get(&owned.id).as_deref(), Some("plugin-secret")); + assert_eq!(global_store.get(&owned.id).as_deref(), Some("user-secret")); + db.remove_plugin_source("plugin", "local").await.unwrap(); + assert_eq!(global_store.get(&owned.id).as_deref(), Some("user-secret")); + } + #[tokio::test] + async fn a_user_source_cannot_be_claimed_by_predicting_its_id() { + let tmp = tempfile::tempdir().unwrap(); + let db = DbManager::new( + tmp.path().join("project"), + tmp.path().join("passwords.json"), + tmp.path().join("history.json"), + std::sync::Arc::new(ide_ssh::SshManager::new(tmp.path().join("ssh.json"))), + ); + let source = DataSource { id: source_id("plugin", "x"), name: "User".into(), ..Default::default() }; + db.save_sources(vec![source.clone()]).await.unwrap(); + assert!(db.upsert_plugin_source("plugin", "x", source.clone()).await.is_err()); + assert_eq!(db.sources().unwrap(), [source]); + } +} diff --git a/crates/ide-db/src/source.rs b/crates/ide-db/src/source.rs index da8ddec2f..6294602e9 100644 --- a/crates/ide-db/src/source.rs +++ b/crates/ide-db/src/source.rs @@ -190,6 +190,12 @@ pub struct DataSource { /// Stored app-wide and listed in every project. Implied by the file it is read from, /// so not written to it. pub global: bool, + /// Plugin id owning this source; absent for user-managed sources. + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin_owner: Option, + /// Id private to the owner, never used as a connection target. + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin_source_id: Option, } impl Default for DataSource { @@ -219,6 +225,8 @@ impl Default for DataSource { introspection: DbIntrospection::default(), properties: Vec::new(), global: false, + plugin_owner: None, + plugin_source_id: None, } } } diff --git a/crates/ide-extension/src/capability.rs b/crates/ide-extension/src/capability.rs index 9b4848611..00a79d417 100644 --- a/crates/ide-extension/src/capability.rs +++ b/crates/ide-extension/src/capability.rs @@ -20,6 +20,10 @@ pub enum Capability { EditorWrite, /// Create and replace files inside the project root. ProjectWrite, + /// Run arbitrary IDE actions and other plugins' commands. + IdeCommands, + /// Manage only its own project database sources and write their credentials. + DatabaseSources, VcsRead, VcsWrite, /// The plugin's own key-value store and data directory. @@ -32,8 +36,12 @@ pub enum Capability { InputEvents, /// The system clipboard, from the plugin's web views. Clipboard, - /// HTTP(S) to one host (`*.example.com` matches subdomains). + /// HTTPS (and loopback HTTP) to one host (`*.example.com` matches subdomains). Net(String), + /// Unencrypted HTTP to one host. + NetHttp(String), + /// A local socket or named pipe on the project machine. + Ipc(String), /// Run one external program. Full code execution: never fine-grained. Process(String), /// Read files outside the project under one path. @@ -60,12 +68,14 @@ impl Capability { Self::ProjectRead | Self::EditorRead | Self::Storage | Self::Events | Self::VcsRead => Risk::Low, Self::EditorWrite | Self::ProjectWrite + | Self::DatabaseSources | Self::VcsWrite | Self::Net(_) + | Self::NetHttp(_) | Self::Secrets | Self::InputEvents | Self::Clipboard => Risk::Medium, - Self::Process(_) | Self::FsRead(_) | Self::FsWrite(_) => Risk::High, + Self::IdeCommands | Self::Ipc(_) | Self::Process(_) | Self::FsRead(_) | Self::FsWrite(_) => Risk::High, } } @@ -82,6 +92,8 @@ impl Capability { Self::EditorRead => "Read the focused editor's file and selection".into(), Self::EditorWrite => "Change text in the editor".into(), Self::ProjectWrite => "Create and change files in the open project".into(), + Self::DatabaseSources => "Manage its own database sources and store their passwords in trusted projects".into(), + Self::IdeCommands => "Run any IDE action or other plugins' commands as if you chose them, including running programs, committing and pushing".into(), Self::VcsRead => "Read version control state".into(), Self::VcsWrite => "Change version control state (commit, fetch, checkout)".into(), Self::Storage => "Store its own data".into(), @@ -92,6 +104,12 @@ impl Capability { "Copy to the clipboard from its commands, and read and write it from its web views".into() } Self::Net(host) => format!("Connect to {host} (from the machine the project is on)"), + Self::NetHttp(host) => { + format!("Connect to {host} over unencrypted HTTP (from the machine the project is on)") + } + Self::Ipc(path) => { + format!("Connect to local socket {path} — equivalent to running arbitrary code on the project machine") + } Self::Process(bin) => format!("Run {bin} — equivalent to running arbitrary code on this machine"), Self::FsRead(path) => format!("Read files under {path}, outside the project"), Self::FsWrite(path) => format!("Change files under {path}, outside the project"), @@ -100,8 +118,14 @@ impl Capability { /// Whether `self`, granted, also covers `wanted` (`net:*.x.com` covers `net:a.x.com`). pub fn covers(&self, wanted: &Capability) -> bool { + if self == wanted { + return true; + } match (self, wanted) { - (Self::Net(pattern), Self::Net(host)) => host_matches(pattern, host), + (Self::Ipc(pattern), Self::Ipc(path)) => ipc_matches(pattern, path), + (Self::Net(pattern), Self::Net(host)) | (Self::NetHttp(pattern), Self::NetHttp(host)) => { + host_matches(pattern, host) + } (Self::FsRead(base), Self::FsRead(path)) | (Self::FsWrite(base), Self::FsWrite(path)) => { std::path::Path::new(path).starts_with(std::path::Path::new(base)) } @@ -120,6 +144,75 @@ pub fn host_matches(pattern: &str, host: &str) -> bool { } } +/// Validates a socket path; a grant may use `*` in its last segment only. +pub fn validate_ipc_name(name: &str, pattern: bool) -> Result<(), String> { + let pipe = name.starts_with(r"\\.\pipe\"); + let unix = name.starts_with('/') + || name.starts_with("~/") + || name.starts_with("$TMPDIR/") + || name.starts_with("$XDG_RUNTIME_DIR/"); + let parts: Vec<_> = name.strip_prefix(r"\\.\pipe\").unwrap_or(name).split(if pipe { '\\' } else { '/' }).collect(); + if !(pipe || unix) + || name.contains(['\0', '?']) + || name.len() > 4096 + || parts.last().is_none_or(|p| p.is_empty()) + || parts.iter().any(|p| matches!(*p, "." | "..")) + || parts[..parts.len().saturating_sub(1)].iter().any(|p| p.contains('*')) + || (!pattern && name.contains('*')) + { + return Err( + "ipc: use an absolute socket path or \\\\.\\pipe\\name; only the last grant segment may contain '*'".into(), + ); + } + Ok(()) +} + +/// Expands the three supported host-local prefixes; no shell is involved. +pub fn expand_ipc_name(name: &str) -> Result { + validate_ipc_name(name, true)?; + for (prefix, key) in [("~/", "HOME"), ("$TMPDIR/", "TMPDIR"), ("$XDG_RUNTIME_DIR/", "XDG_RUNTIME_DIR")] { + if let Some(rest) = name.strip_prefix(prefix) { + let base = std::env::var(key).map_err(|_| format!("{key} is not set"))?; + let expanded = format!("{}/{rest}", base.trim_end_matches('/')); + validate_ipc_name(&expanded, true)?; + return Ok(expanded); + } + } + Ok(name.into()) +} + +fn ipc_matches(pattern: &str, name: &str) -> bool { + let (Ok(pattern), Ok(name)) = (expand_ipc_name(pattern), expand_ipc_name(name)) else { return false }; + if validate_ipc_name(&name, false).is_err() { + return false; + } + let separator = if pattern.starts_with(r"\\.\pipe\") { '\\' } else { '/' }; + let (Some((parent, pattern)), Some((other, name))) = (pattern.rsplit_once(separator), name.rsplit_once(separator)) + else { + return false; + }; + if parent != other { + return false; + } + let mut rest = name; + let mut pieces = pattern.split('*'); + let first = pieces.next().unwrap_or_default(); + let Some(tail) = rest.strip_prefix(first) else { return false }; + rest = tail; + let pieces: Vec<_> = pieces.collect(); + if pieces.is_empty() { + return rest.is_empty(); + } + for (i, piece) in pieces.iter().enumerate() { + if i + 1 == pieces.len() { + return rest.ends_with(piece); + } + let Some(at) = rest.find(piece) else { return false }; + rest = &rest[at + piece.len()..]; + } + true +} + impl fmt::Display for Capability { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -127,6 +220,8 @@ impl fmt::Display for Capability { Self::EditorRead => f.write_str("editor.read"), Self::EditorWrite => f.write_str("editor.write"), Self::ProjectWrite => f.write_str("project.write"), + Self::IdeCommands => f.write_str("ide.commands"), + Self::DatabaseSources => f.write_str("database.sources"), Self::VcsRead => f.write_str("vcs.read"), Self::VcsWrite => f.write_str("vcs.write"), Self::Storage => f.write_str("storage"), @@ -135,6 +230,8 @@ impl fmt::Display for Capability { Self::InputEvents => f.write_str("events.input"), Self::Clipboard => f.write_str("clipboard"), Self::Net(h) => write!(f, "net:{h}"), + Self::NetHttp(h) => write!(f, "net.http:{h}"), + Self::Ipc(p) => write!(f, "ipc:{p}"), Self::Process(b) => write!(f, "process:{b}"), Self::FsRead(p) => write!(f, "fs.read:{p}"), Self::FsWrite(p) => write!(f, "fs.write:{p}"), @@ -153,7 +250,7 @@ impl FromStr for Capability { return Err(format!("capability {s:?} needs a value after ':'")); } return match kind { - "net" => { + "net" | "net.http" => { let host = arg.strip_prefix("*.").unwrap_or(arg); let valid = !host.is_empty() && host.len() <= 253 @@ -162,7 +259,19 @@ impl FromStr for Capability { && label.len() <= 63 && label.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') }); - if valid { Ok(Self::Net(arg.to_ascii_lowercase())) } else { Err(format!("invalid host in {s:?}")) } + if valid { + Ok(if kind == "net" { + Self::Net(arg.to_ascii_lowercase()) + } else { + Self::NetHttp(arg.to_ascii_lowercase()) + }) + } else { + Err(format!("invalid host in {s:?}")) + } + } + "ipc" => { + validate_ipc_name(arg, true)?; + Ok(Self::Ipc(arg.to_owned())) } "process" => { if arg.contains(['/', '\\']) && !std::path::Path::new(arg).is_absolute() { @@ -184,6 +293,8 @@ impl FromStr for Capability { "editor.read" | "editor-read" => Self::EditorRead, "editor.write" | "editor-write" => Self::EditorWrite, "project.write" => Self::ProjectWrite, + "ide.commands" => Self::IdeCommands, + "database.sources" => Self::DatabaseSources, "vcs.read" => Self::VcsRead, "vcs.write" => Self::VcsWrite, "storage" => Self::Storage, @@ -292,3 +403,71 @@ mod tests { assert!(!Capability::FsRead("/etc".into()).covers(&Capability::FsRead("/etcx".into()))); } } + +#[cfg(test)] +mod api14_tests { + use super::*; + #[test] + fn http_grants_do_not_imply_https_and_socket_patterns_stay_in_one_directory() { + let http: Capability = "net.http:*.example.com".parse().unwrap(); + assert_eq!(http.to_string(), "net.http:*.example.com"); + assert_eq!(http.risk(), Risk::Medium); + assert!(http.describe().contains("unencrypted")); + assert!(http.covers(&"net.http:a.example.com".parse().unwrap())); + assert!(!http.covers(&"net:a.example.com".parse().unwrap())); + assert!(!"net:*.example.com".parse::().unwrap().covers(&"net.http:a.example.com".parse().unwrap())); + let socket: Capability = "ipc:/tmp/discord-ipc-*-socket".parse().unwrap(); + assert_eq!(socket.risk(), Risk::High); + assert!(socket.covers(&Capability::Ipc("/tmp/discord-ipc-42-socket".into()))); + assert!(!socket.covers(&Capability::Ipc("/tmp/discord-ipc-dir/evil-socket".into()))); + assert!(!socket.covers(&Capability::Ipc("/tmp/../tmp/discord-ipc-42-socket".into()))); + assert_eq!("ipc:$TMPDIR/discord-ipc-*".parse::().unwrap().to_string(), "ipc:$TMPDIR/discord-ipc-*"); + assert!( + r"ipc:\\.\pipe\discord-*" + .parse::() + .unwrap() + .covers(&Capability::Ipc(r"\\.\pipe\discord-0".into())) + ); + for invalid in + ["ipc:relative", "ipc:/tmp/*/socket", "ipc:/tmp/../socket", "ipc:/tmp/", "ipc:/tmp/socket?", "net.http:a b"] + { + assert!(invalid.parse::().is_err(), "{invalid}"); + } + } +} + +#[cfg(test)] +mod command_tests { + use super::*; + #[test] + fn ide_commands_is_explicit_and_high_risk() { + let cap: Capability = "ide.commands".parse().unwrap(); + assert_eq!(cap, Capability::IdeCommands); + assert_eq!(cap.to_string(), "ide.commands"); + assert_eq!(cap.risk(), Risk::High); + assert!(!cap.auto_migrates()); + assert!(!Capability::VcsWrite.covers(&cap)); + assert!(!Capability::Process("git".into()).covers(&cap)); + } +} + +#[cfg(test)] +mod database_tests { + use super::*; + #[test] + fn database_sources_is_a_medium_risk_exact_grant_requiring_api4() { + let cap: Capability = "database.sources".parse().unwrap(); + assert_eq!(cap, Capability::DatabaseSources); + assert_eq!(cap.to_string(), "database.sources"); + assert_eq!(cap.risk(), Risk::Medium); + assert!(!cap.auto_migrates()); + assert!(!Capability::ProjectRead.covers(&cap)); + assert!(!Capability::ProjectWrite.covers(&cap)); + for (api, valid) in [("^1.3", false), ("^1.4", true)] { + let text = format!( + "id='db-test'\nname='DB'\nversion='1.0.0'\nengines.openidea='{api}'\n[capabilities]\nrequired=['database.sources']" + ); + assert_eq!(crate::Manifest::parse(&text).is_ok(), valid); + } + } +} diff --git a/crates/ide-extension/src/ledger.rs b/crates/ide-extension/src/ledger.rs index 89a9f9f6c..95c17cd50 100644 --- a/crates/ide-extension/src/ledger.rs +++ b/crates/ide-extension/src/ledger.rs @@ -59,6 +59,12 @@ pub struct Grant { /// `process:` → the program confirmed for it. #[serde(default)] pub binaries: BTreeMap, + /// Project root → process grant → the executable approved for that project. + #[serde(default)] + pub project_binaries: BTreeMap>, + /// Explicit global executable choices, retained for the next re-approval. + #[serde(default)] + pub program_choices: BTreeMap, /// Migrated from 0.1: capabilities not granted yet are asked for on first /// use instead of blocking the plugin from loading. #[serde(default)] @@ -248,6 +254,9 @@ impl Ledger { } else { if let Capability::Process(_) = cap { grant.binaries.remove(&s); + for binaries in grant.project_binaries.values_mut() { + binaries.remove(&s); + } } grant.granted.remove(&s) } @@ -260,6 +269,58 @@ impl Ledger { } } + /// The user's explicit global executable choice, if any. + pub fn program_choice(&self, subject: &Subject, cap: &Capability) -> Option<&Path> { + self.grants.get(subject)?.program_choices.get(&cap.to_string()).map(PathBuf::as_path) + } + /// Remembers or resets an explicit global executable choice after confirmation. + pub fn set_program_choice(&mut self, subject: &Subject, cap: &Capability, path: Option<&Path>) { + if let Some(grant) = self.grants.get_mut(subject) { + if let Some(path) = path { + grant.program_choices.insert(cap.to_string(), path.into()); + } else { + grant.program_choices.remove(&cap.to_string()); + } + } + } + + /// Updates a project-specific program choice after user confirmation. + pub fn set_project_binary( + &mut self, + subject: &Subject, + cap: &Capability, + root: &Path, + binary: Option, + ) { + if let Some(grant) = self.grants.get_mut(subject) { + if let Some(binary) = binary { + grant.project_binaries.entry(root.into()).or_default().insert(cap.to_string(), binary); + } else if let Some(binaries) = grant.project_binaries.get_mut(root) { + binaries.remove(&cap.to_string()); + } + } + } + /// The project override when present, otherwise the global approved program. + pub fn check_binary_in_project( + &self, + subject: &Subject, + cap: &Capability, + root: Option<&Path>, + ) -> Result { + let grant = self.grants.get(subject).ok_or_else(|| format!("{cap} was not granted"))?; + if !grant.has(cap) { + return Err(format!("{cap} was not granted")); + } + if let Some(bin) = root.and_then(|root| grant.project_binaries.get(root)).and_then(|b| b.get(&cap.to_string())) + { + return match file_hash(&bin.path) { + Ok(hash) if hash == bin.hash => Ok(bin.path.clone()), + _ => Err(format!("{} changed or is missing; confirm {cap} again", bin.path.display())), + }; + } + self.check_binary(subject, cap) + } + /// Checks a `process:` grant at call time: the program confirmed for it, /// unchanged since. Errors name what to do. pub fn check_binary(&self, subject: &Subject, cap: &Capability) -> Result { @@ -306,6 +367,16 @@ pub fn by_risk(caps: &[Capability]) -> Vec<(Risk, Vec)> { groups.into_iter().map(|(r, v)| (r.0, v)).collect() } +/// Pins an explicitly selected executable, retaining a multicall shim's filename. +pub fn approve_program(path: &Path) -> Result { + if !path.is_absolute() || !is_executable(path) { + return Err("choose an absolute executable file".into()); + } + let path = canonical_dir(path); + let hash = file_hash(&path).map_err(|e| e.to_string())?; + Ok(BinaryGrant { path, hash }) +} + /// Resolves a `process:` program to an absolute path once, at grant time: /// absolute names are taken as is, others are looked up in `path_var`. /// The directory is canonicalized but the file name kept as found: `cargo`, diff --git a/crates/ide-extension/src/lib.rs b/crates/ide-extension/src/lib.rs index 7ff7738db..c466abda2 100644 --- a/crates/ide-extension/src/lib.rs +++ b/crates/ide-extension/src/lib.rs @@ -24,7 +24,7 @@ mod registry; mod ui; mod when; -pub use capability::{Capability, CapabilityInfo, Risk, host_matches}; +pub use capability::{Capability, CapabilityInfo, Risk, expand_ipc_name, host_matches, validate_ipc_name}; pub use index::ContributionIndex; pub use manifest::{ ActivationEvent, CATEGORIES, Capabilities, ENGINE_VERSION, EVENT_KINDS, LEGACY_API, MANIFEST_FILE, MAX_CATEGORIES, diff --git a/crates/ide-extension/src/manifest.rs b/crates/ide-extension/src/manifest.rs index 4f07c6e7c..17d3c6992 100644 --- a/crates/ide-extension/src/manifest.rs +++ b/crates/ide-extension/src/manifest.rs @@ -32,7 +32,7 @@ use std::path::{Component, Path, PathBuf}; pub const MANIFEST_FILE: &str = "plugin.toml"; /// The plugin API (engine) version this host implements. -pub const ENGINE_VERSION: &str = "1.3.0"; +pub const ENGINE_VERSION: &str = "1.4.0"; /// The 0.1 API, still accepted for contribution-only packages and 0.1 components. pub const LEGACY_API: &str = "0.1"; /// Marketplace categories, in the order they are shown. @@ -144,8 +144,20 @@ impl ActivationEvent { /// Workbench event kinds (`on-event:`, WIT `events.event-kind`). /// `scheduled` comes from `on-schedule` instead. -pub const EVENT_KINDS: &[&str] = - &["file-opened", "file-saved", "files-changed", "editor-focused", "action-performed", "vcs-changed"]; +pub const EVENT_KINDS: &[&str] = &[ + "run-exited", + "terminal-exited", + "content-tab-action", + "file-closed", + "settings-changed", + "input-mode-changed", + "file-opened", + "file-saved", + "files-changed", + "editor-focused", + "action-performed", + "vcs-changed", +]; impl std::fmt::Display for ActivationEvent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -317,6 +329,12 @@ impl Manifest { check_relative(icon, "icon")?; } for cap in self.capabilities.required.iter().chain(&self.capabilities.optional) { + if matches!( + cap, + Capability::IdeCommands | Capability::DatabaseSources | Capability::NetHttp(_) | Capability::Ipc(_) + ) { + self.needs_api(&format!("capability `{cap}`"), 4)?; + } if matches!(cap, Capability::ProjectWrite | Capability::FsWrite(_)) { self.needs_api(&format!("capability `{cap}`"), 3)?; } @@ -332,6 +350,10 @@ impl Manifest { } let mut seen = BTreeSet::new(); for event in &self.activation { + if matches!(event, ActivationEvent::Event(kind) if matches!(kind.as_str(), "file-closed" | "settings-changed" | "input-mode-changed" | "run-exited" | "terminal-exited" | "content-tab-action")) + { + self.needs_api(&format!("`{event}`"), 4)?; + } if matches!(event, ActivationEvent::Event(kind) if kind == "files-changed") { self.needs_api("`on-event:files-changed`", 3)?; } @@ -655,3 +677,44 @@ mod tests { assert!(!engine_compatible("0.10", true)); } } + +#[cfg(test)] +mod api14_tests { + use super::*; + #[test] + fn new_capabilities_and_activations_require_api_1_4() { + for declaration in [ + "[capabilities]\nrequired = [\"net.http:example.com\"]", + "[capabilities]\noptional = [\"ipc:$TMPDIR/test-*\"]", + "activation = [\"on-event:file-closed\"]", + "activation = [\"on-event:settings-changed\"]", + "activation = [\"on-event:input-mode-changed\"]", + "activation = [\"on-event:run-exited\"]", + "activation = [\"on-event:terminal-exited\"]", + "activation = [\"on-event:content-tab-action\"]", + ] { + for engine in ["^1.0", "^1.3", "^1.4"] { + let manifest = format!( + "id = 'test'\nname = 'Test'\nversion = '0.1.0'\nwasm = 'plugin.wasm'\nengines.openidea = '{engine}'\n{declaration}\n" + ); + let parsed = Manifest::parse(&manifest); + if engine == "^1.4" { + parsed.unwrap(); + } else { + assert!(parsed.unwrap_err().contains("new in plugin API 1.4")); + } + } + } + } +} + +#[cfg(test)] +mod commands_api_tests { + use super::*; + #[test] + fn ide_commands_requires_api_1_4() { + let manifest = "id = \"test\"\nname = \"Test\"\nversion = \"1.0.0\"\nengines.openidea = \"^1.3\"\n[capabilities]\nrequired = [\"ide.commands\"]\n"; + assert!(Manifest::parse(manifest).unwrap_err().contains("1.4")); + assert!(Manifest::parse(&manifest.replace("^1.3", "^1.4")).is_ok()); + } +} diff --git a/crates/ide-host/Cargo.toml b/crates/ide-host/Cargo.toml index af8eec2f1..730e8f766 100644 --- a/crates/ide-host/Cargo.toml +++ b/crates/ide-host/Cargo.toml @@ -52,6 +52,7 @@ ide-tools = { path = "../ide-tools" } ide-docker = { path = "../ide-docker" } semver = "1" serde.workspace = true +jsonschema = { version = "0.33", default-features = false } serde_json = { workspace = true, features = ["raw_value"] } toml.workspace = true specta.workspace = true diff --git a/crates/ide-host/src/formatter.rs b/crates/ide-host/src/formatter.rs index fea37d053..ad3416ac5 100644 --- a/crates/ide-host/src/formatter.rs +++ b/crates/ide-host/src/formatter.rs @@ -130,6 +130,38 @@ pub async fn format( let snap = project.documents.snapshot(doc)?; let settings = project.settings.effective(); let language = project.languages.get(&snap.language); + if options.tool.is_none() + && let Some(id) = settings.languages.plugin_formatters.get(snap.language.as_str()).filter(|s| !s.is_empty()) + { + if options.on_save && !settings.editor.format_on_save { + return Ok(Vec::new()); + } + if snap.read_only { + return Err(IdeError::invalid("The file is read-only")); + } + let plugins = crate::plugins::editor_plugin_host() + .ok_or_else(|| IdeError::not_found("The selected plugin formatter is unavailable"))?; + let d = ide_plugin::ProviderDocument { + path: snap.path.0, + language: snap.language.0, + version: u64::from(snap.version), + text: snap.text, + }; + let id = id.clone(); + return tokio::task::spawn_blocking(move || { + plugins.language_format( + &id, + &d, + range, + ide_plugin::language_features::FormatOptions { + tab_size: options.tab_size, + insert_spaces: options.insert_spaces, + }, + ) + }) + .await + .map_err(|e| IdeError::internal(e.to_string()))?; + } // A committed PHP-CS-Fixer configuration takes over Reformat Code for `.php` files. if options.tool.is_none() && snap.path.extension().is_some_and(|e| e == "php") diff --git a/crates/ide-host/src/lib.rs b/crates/ide-host/src/lib.rs index b3ab936d2..5606766e7 100644 --- a/crates/ide-host/src/lib.rs +++ b/crates/ide-host/src/lib.rs @@ -55,6 +55,8 @@ pub struct HostConfig { } pub struct Host { + pub(crate) mcp_executable: RwLock>, + plugin_host: RwLock>, pub mcp_reports: mcp::Reports, pub mcp_requests: mcp::Requests, pub trust: trust::TrustStore, @@ -99,6 +101,14 @@ pub struct Host { const EXIT_SHUTDOWN_WAIT: std::time::Duration = std::time::Duration::from_secs(10); impl Host { + /// Windows whose current project has this root. + pub(crate) fn windows_for_root(&self, root: &std::path::Path) -> Vec { + let mut windows: Vec<_> = + self.projects.read().iter().filter(|(_, p)| p.root == root).map(|(w, _)| w.clone()).collect(); + windows.sort(); + windows + } + pub fn new(config: HostConfig) -> Self { let HostConfig { config_dir, @@ -115,6 +125,8 @@ impl Host { let ssh = Arc::new(ide_ssh::SshManager::new(data_dir.join("ssh-passwords.json"))); ssh.set_configs(global_settings.effective().ssh.configs); let host = Self { + plugin_host: RwLock::default(), + mcp_executable: RwLock::default(), mcp_reports: Default::default(), mcp_requests: Default::default(), trust: trust::TrustStore::load(&config_dir, &data_dir), @@ -166,6 +178,19 @@ impl Host { } } + /// Associates the plugin runtime that executes this host's project operations. + /// Publishes only the executable of the running built-in MCP service. + pub fn set_mcp_executable(&self, executable: Option) { + *self.mcp_executable.write() = executable; + } + + pub fn set_plugin_host(&self, plugins: &Arc) { + *self.plugin_host.write() = Arc::downgrade(plugins); + } + pub(crate) fn plugin_host(&self) -> Option> { + self.plugin_host.read().upgrade() + } + /// Every open project (all windows). pub fn projects(&self) -> Vec> { self.projects.read().values().cloned().collect() @@ -231,6 +256,11 @@ impl Host { self.cancel_request(request); } let project = self.projects.write().remove(window)?; + if self.windows_for_root(&project.root).is_empty() + && let Some(plugins) = self.plugin_host() + { + plugins.withdraw_project_run_targets(&project.root); + } project.files_events_active.store(false, std::sync::atomic::Ordering::Release); project.plugin_diagnostics.close(); if let Some(plugins) = plugins::editor_plugin_host() { diff --git a/crates/ide-host/src/mcp/mod.rs b/crates/ide-host/src/mcp/mod.rs index ac87c20fd..9c4e7c26d 100644 --- a/crates/ide-host/src/mcp/mod.rs +++ b/crates/ide-host/src/mcp/mod.rs @@ -75,7 +75,11 @@ impl ToolRegistry for Registry { } fn tools(&self, root: &Path) -> Result, String> { let (_, project) = self.target(root)?; - Ok(tools().into_iter().map(|t| t.metadata).filter(|t| allowed(t, project.is_trusted())).collect()) + let mut all: Vec<_> = tools().into_iter().map(|t| t.metadata).collect(); + if let Some(plugins) = self.0.plugin_host() { + all.extend(plugins.mcp_tools(root).into_iter().map(|(_, _, tool)| tool)); + } + Ok(all.into_iter().filter(|t| allowed(t, project.is_trusted())).collect()) } fn call( &self, @@ -95,6 +99,12 @@ impl ToolRegistry for Registry { client: &str, ) -> Result { let (window, project) = self.target(root).map_err(ToolCallError::Failed)?; + if let Some(plugins) = self.0.plugin_host() + && let Some((plugin, tool, metadata)) = + plugins.mcp_tools(root).into_iter().find(|(_, _, t)| t.name == name && allowed(t, project.is_trusted())) + { + return self.call_plugin(window, project, plugins, plugin, tool, metadata, arguments, cancel, client); + } let tool = tools() .into_iter() .find(|t| t.metadata.name == name && allowed(&t.metadata, project.is_trusted())) @@ -129,6 +139,51 @@ impl Host { } } +impl Registry { + #[allow(clippy::too_many_arguments)] + fn call_plugin( + &self, + window: String, + project: Arc, + plugins: Arc, + plugin: String, + tool: String, + metadata: ide_mcp::Tool, + arguments: &str, + cancel: CancellationToken, + client: &str, + ) -> Result { + let run = || -> ide_core::IdeResult { + let args = serde_json::from_str(arguments).map_err(|e| IdeError::invalid(e.to_string()))?; + let call = tool::Call { + host: self.0.clone(), + project: project.clone(), + window: window.clone(), + args, + cancel: cancel.clone(), + client: client.into(), + tool: metadata.name, + }; + if cancel.is_cancelled() { + return Err(IdeError::cancelled()); + } + if metadata.destructive { + self.0.runtime.block_on(call.confirm(&metadata.description, None))?; + } + if !metadata.read_only { + project.require_trust()?; + } + let invocation = ide_plugin::Invocation { + client: Some(window), + workspace_root: Some(project.root.clone()), + ..Default::default() + }; + plugins.call_mcp_tool(&plugin, &tool, arguments, invocation, &cancel) + }; + run().map_err(|e| ToolCallError::Failed(e.message)) + } +} + #[cfg(test)] mod tests { #[test] diff --git a/crates/ide-host/src/mcp/reports.rs b/crates/ide-host/src/mcp/reports.rs index 00b360e23..343ddba77 100644 --- a/crates/ide-host/src/mcp/reports.rs +++ b/crates/ide-host/src/mcp/reports.rs @@ -91,7 +91,7 @@ impl Reports { pub(super) fn focused_window(&self) -> Option { self.0.read().focused.clone() } - pub(super) fn editors(&self, windows: &[String]) -> Vec { + pub(crate) fn editors(&self, windows: &[String]) -> Vec { let state = self.0.read(); let mut paths = Vec::new(); for window in windows.iter().filter_map(|w| state.windows.get(w)) { @@ -103,10 +103,10 @@ impl Reports { } paths } - pub(super) fn focused(&self, window: &str) -> Option { + pub(crate) fn focused(&self, window: &str) -> Option { self.0.read().windows.get(window).and_then(|w| w.focused.clone()) } - pub(super) fn problems(&self, windows: &[String]) -> Vec { + pub(crate) fn problems(&self, windows: &[String]) -> Vec { let state = self.0.read(); let mut files: BTreeMap> = BTreeMap::new(); for window in windows.iter().filter_map(|w| state.windows.get(w)) { diff --git a/crates/ide-host/src/ops/db.rs b/crates/ide-host/src/ops/db.rs index 5ab776d8a..176583b40 100644 --- a/crates/ide-host/src/ops/db.rs +++ b/crates/ide-host/src/ops/db.rs @@ -169,7 +169,12 @@ pub fn sources(host: &Host, window: &str) -> IdeResult> { /// Replaces the project's data sources with `sources`. pub async fn save_sources(host: &Host, window: &str, sources: Vec) -> IdeResult<()> { let db = host.project(window)?.db.clone(); - db.save_sources(sources).await + db.save_sources(sources).await?; + let root = host.project(window)?.root.clone(); + for project in host.projects().into_iter().filter(|p| p.root == root) { + project.sink.emit(ide_core::IdeEvent::DbSourcesChanged); + } + Ok(()) } /// Remembers the password of data source `id` for the session, and in the OS diff --git a/crates/ide-host/src/ops/doc/commands.rs b/crates/ide-host/src/ops/doc/commands.rs index ddb9c8a6f..ad1019036 100644 --- a/crates/ide-host/src/ops/doc/commands.rs +++ b/crates/ide-host/src/ops/doc/commands.rs @@ -52,6 +52,20 @@ pub fn doc_set_line_ending(host: &Host, window: &str, doc: DocId, line_ending: L pub async fn doc_outline(host: &Host, window: &str, doc: DocId) -> IdeResult> { let project = host.project(window)?; let lsp = project.lsp.document_symbols(doc).await.ok().flatten(); + let lsp = if lsp.as_ref().is_none_or(|v| v.is_empty()) { + if let Some((plugins, d)) = + crate::ops::lsp::plugin_providers(host, window, doc, crate::plugins::ProviderKind::DocumentSymbols) + { + let items = + crate::ops::lsp::plugin_call(move || plugins.language_document_symbols(&d)).await.unwrap_or_default(); + if items.is_empty() { lsp } else { Some(crate::plugins::language_features::outline(items)) } + } else { + lsp + } + } else { + lsp + }; + let lsp = lsp.filter(|v| !v.is_empty()); blocking(move || { let snap = match project.documents.text_snapshot(doc) { Ok(snap) => snap, @@ -80,7 +94,27 @@ pub async fn doc_folding(host: &Host, window: &str, doc: DocId) -> IdeResult ide_language::FoldKind::Comment, + "imports" => ide_language::FoldKind::Imports, + _ => ide_language::FoldKind::Region, + }, + })); + folds.sort_by_key(|f| (f.start_line, f.end_line)); + folds.dedup_by_key(|f| (f.start_line, f.end_line)); + } + Ok(folds) }) .await } @@ -105,6 +139,21 @@ pub async fn doc_selection_ranges(host: &Host, window: &str, doc: DocId, offset: let project = host.project(window)?; blocking(move || { let snap = project.documents.text_snapshot(doc)?; + if !snap.large + && let Some(plugins) = crate::plugins::editor_providers_for(crate::plugins::ProviderKind::SelectionRanges) + { + let at = ide_text::Document::new(&snap.text).offset_to_pos(offset); + let d = ide_plugin::ProviderDocument { + path: project.documents.identity(doc)?.0, + language: snap.language.0.clone(), + version: u64::from(snap.version), + text: snap.text.clone(), + }; + let ranges = plugins.language_selection_ranges(&d, at); + if !ranges.is_empty() { + return Ok(ranges); + } + } let language = if snap.large { ide_core::LanguageId::plain_text() } else { snap.language }; Ok(project.syntax.selection_ranges(doc, &language, snap.version, &snap.text, offset)) }) diff --git a/crates/ide-host/src/ops/lsp.rs b/crates/ide-host/src/ops/lsp.rs index 5ed1de270..9533fc464 100644 --- a/crates/ide-host/src/ops/lsp.rs +++ b/crates/ide-host/src/ops/lsp.rs @@ -56,7 +56,7 @@ pub async fn lsp_completion( } /// The plugin host and the document, when some plugin provides `kind`. -fn plugin_providers( +pub(crate) fn plugin_providers( host: &Host, window: &str, doc: DocId, @@ -74,10 +74,48 @@ fn plugin_providers( } /// Plugin calls block (the plugin's actor thread does the work). -async fn plugin_call(f: impl FnOnce() -> T + Send + 'static) -> Option { +pub(crate) async fn plugin_call(f: impl FnOnce() -> T + Send + 'static) -> Option { tokio::task::spawn_blocking(f).await.ok() } +fn provider_server(server: IdeResult) -> IdeResult { + match server { + Ok(v) => Ok(v), + Err(e) if matches!(e.kind, ErrorKind::NotFound | ErrorKind::Unavailable) => Ok(T::default()), + Err(e) => Err(e), + } +} + +fn merge_language_locations(mut server: Vec, plugin: Vec) -> Vec { + let mut seen: std::collections::HashSet<_> = server.iter().cloned().collect(); + for l in plugin { + let loc = Location::file(l.path, l.range); + if seen.insert(loc.clone()) { + server.push(loc); + } + } + server +} +fn merge_language_highlights( + mut server: Vec, + plugin: Vec, +) -> Vec { + let mut seen: std::collections::HashSet<_> = server.iter().map(|h| h.range).collect(); + for h in plugin { + if seen.insert(h.range) { + server.push(Highlight { + range: h.range, + kind: match h.kind.as_str() { + "read" => HighlightKind::Read, + "write" => HighlightKind::Write, + _ => HighlightKind::Text, + }, + }); + } + } + server +} + /// Items from plugins carry no server: commands don't apply, and a code /// action is resolved by the plugin named in its `data` ([`PluginActionRef`]). const PLUGIN_SERVER: ServerId = ServerId(0); @@ -222,7 +260,21 @@ pub async fn lsp_hover( } pub async fn lsp_signature_help(host: &Host, window: &str, doc: DocId, pos: Pos) -> IdeResult> { - host.project(window)?.lsp.signature_help(doc, pos).await + let server = host.project(window)?.lsp.signature_help(doc, pos).await; + if matches!(&server,Ok(Some(h)) if !h.signatures.is_empty()) { + return server; + } + let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::SignatureHelp) else { + return server; + }; + let help = plugin_call(move || plugins.language_signature_help(&d, pos)).await.flatten(); + match help { + Some(h) => { + let _ = provider_server(server)?; + Ok(Some(crate::plugins::language_features::signature_help(h))) + } + None => server, + } } pub async fn lsp_goto(host: &Host, window: &str, doc: DocId, pos: Pos, kind: GotoKind) -> IdeResult> { @@ -238,6 +290,23 @@ pub async fn lsp_goto(host: &Host, window: &str, doc: DocId, pos: Pos, kind: Got } } let result = host.project(window)?.lsp.goto(doc, pos, kind).await; + if matches!(kind, GotoKind::Implementation | GotoKind::TypeDefinition) { + let k = + if kind == GotoKind::Implementation { ProviderKind::Implementation } else { ProviderKind::TypeDefinition }; + let Some((plugins, d)) = plugin_providers(host, window, doc, k) else { + return result; + }; + let locations = plugin_call(move || { + if kind == GotoKind::Implementation { + plugins.language_implementation(&d, pos) + } else { + plugins.language_type_definition(&d, pos) + } + }) + .await + .unwrap_or_default(); + return Ok(merge_language_locations(provider_server(result)?, locations)); + } if kind != GotoKind::Definition { return result; } @@ -358,6 +427,20 @@ pub async fn lsp_implementations( let cancel = host.begin_request(request); let result = lsp.implementations(doc, &positions, cancel).await; host.end_request(request); + if let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::Implementation) { + let count = positions.len(); + let found = plugin_call(move || { + positions.into_iter().map(|pos| plugins.language_implementation(&d, pos)).collect::>() + }) + .await + .unwrap_or_default(); + let mut out = provider_server(result)?; + out.resize_with(count, Vec::new); + for (items, found) in out.iter_mut().zip(found) { + *items = merge_language_locations(std::mem::take(items), found); + } + return Ok(out); + } result } @@ -380,6 +463,10 @@ pub async fn lsp_goto_implementation( }) .await; host.end_request(request); + if let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::Implementation) { + let found = plugin_call(move || plugins.language_implementation(&d, pos)).await.unwrap_or_default(); + return Ok(merge_language_locations(provider_server(result)?, found)); + } result } @@ -505,10 +592,11 @@ fn validate_indexed_symbol( let stale = || { ide_core::IdeError::new(ErrorKind::OutOfSync, "indexed document changed; wait for indexing and try again") }; - if ide_plugin::resolve_in_root(&project.root, &file.path.to_string_lossy()).ok().as_ref() != Some(&file.path) { + let path = dunce::simplified(&file.path); + if ide_plugin::resolve_in_root(&project.root, &path.to_string_lossy()).ok().as_deref() != Some(path) { return Err(stale()); } - let text = if let Some(doc) = project.documents.by_path(&file.path) { + let text = if let Some(doc) = project.documents.by_path(path) { let snapshot = project.documents.text_snapshot(doc)?; if snapshot.large || file.version.is_some_and(|v| v != snapshot.version) { return Err(stale()); @@ -517,7 +605,7 @@ fn validate_indexed_symbol( } else { let loaded = project .documents - .load_file_bounded(&file.path, crate::plugins::navigation::MAX_DOCUMENT_BYTES) + .load_file_bounded(path, crate::plugins::navigation::MAX_DOCUMENT_BYTES) .map_err(|_| stale())?; if loaded.binary || loaded.large { return Err(stale()); @@ -555,7 +643,12 @@ fn merge_indexed_references(mut server: References, symbol: &crate::plugins::nav } pub async fn lsp_document_highlights(host: &Host, window: &str, doc: DocId, pos: Pos) -> IdeResult> { - host.project(window)?.lsp.document_highlights(doc, pos).await + let server = host.project(window)?.lsp.document_highlights(doc, pos).await; + let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::DocumentHighlights) else { + return server; + }; + let items = plugin_call(move || plugins.language_document_highlights(&d, pos)).await.unwrap_or_default(); + Ok(merge_language_highlights(provider_server(server)?, items)) } pub async fn lsp_workspace_symbols( @@ -597,7 +690,7 @@ pub async fn lsp_code_actions( let actions = if plugin_actions.is_empty() { result? } else { - let mut out = result.unwrap_or_default(); + let mut out = provider_server(result)?; out.extend(plugin_actions); out }; @@ -1105,12 +1198,52 @@ pub async fn lsp_semantic_tokens(host: &Host, window: &str, doc: DocId) -> IdeRe } pub async fn lsp_code_lens(host: &Host, window: &str, doc: DocId) -> IdeResult> { - host.project(window)?.lsp.code_lens(doc).await + let server = host.project(window)?.lsp.code_lens(doc).await; + let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::CodeLenses) else { + return server; + }; + let items = plugin_call(move || plugins.language_code_lenses(&d)).await.unwrap_or_default(); + let mut out = provider_server(server)?; + out.extend(items.into_iter().map(|(plugin, i)| CodeLens { + range: i.range, + command: Some(Command { + title: i.title, + command: format!("openidea.plugin-language:{plugin}:{}", i.command), + arguments: vec![serde_json::json!(serde_json::to_string(&i.argument).unwrap_or_default())], + }), + })); + Ok(out) } /// Links in a document (`href`s, Markdown links, `#include`s…) from its language servers. pub async fn lsp_document_links(host: &Host, window: &str, doc: DocId) -> IdeResult> { - host.project(window)?.lsp.document_links(doc).await + let server = host.project(window)?.lsp.document_links(doc).await; + let Some((plugins, d)) = plugin_providers(host, window, doc, ProviderKind::DocumentLinks) else { + return server; + }; + let root = host.project(window)?.root.clone(); + let items = plugin_call(move || plugins.language_document_links(&d)).await.unwrap_or_default(); + let mut out = provider_server(server)?; + out.extend(items.into_iter().filter_map(|i| { + let url = i.target.starts_with("https://") || i.target.starts_with("http://"); + if !url && ide_core::is_virtual_uri(&i.target) { + return None; + } + Some(DocumentLink { + range: i.range, + path: (!url).then(|| { + { + let path = std::path::PathBuf::from(&i.target); + if path.is_absolute() { path } else { root.join(path) } + } + .into() + }), + url: url.then_some(i.target), + fragment: None, + tooltip: None, + }) + })); + Ok(out) } pub async fn lsp_call_hierarchy( @@ -1200,6 +1333,34 @@ pub async fn redo_workspace_edit(host: &Host, window: &str, id: u32) -> IdeResul /// Runs a server command (`workspace/executeCommand`), e.g. a completion item's /// follow-up command. Edits the server requests arrive via `workspace/applyEdit`. pub async fn lsp_execute_command(host: &Host, window: &str, doc: DocId, command: Command) -> IdeResult<()> { + if let Some(rest) = command.command.strip_prefix("openidea.plugin-language:") { + let (plugin, command_id) = + rest.split_once(':').ok_or_else(|| ide_core::IdeError::invalid("Invalid plugin command"))?; + let plugins = crate::plugins::editor_plugin_host() + .ok_or_else(|| ide_core::IdeError::not_found("Plugin host unavailable"))?; + let argument = command + .arguments + .first() + .and_then(|a| a.as_str()) + .and_then(|s| serde_json::from_str::>(s).ok()) + .flatten(); + let project = host.project(window)?; + let snap = project.documents.snapshot(doc)?; + let invocation = ide_plugin::Invocation { + client: Some(window.into()), + workspace_root: Some(project.root.clone()), + editor: Some(ide_plugin::EditorContext { + path: snap.path.0.clone(), + language: snap.language.0, + selections: vec![], + }), + selected_paths: vec![snap.path.0], + ..Default::default() + }; + let (plugin, command_id) = (plugin.to_owned(), command_id.to_owned()); + return blocking(move || plugins.run_language_command(&plugin, &command_id, argument.as_deref(), invocation)) + .await; + } host.project(window)?.lsp.execute_command(doc, command).await } @@ -1262,7 +1423,74 @@ pub async fn lsp_execute_code_action( } /// Commands of this module the remote server answers. +/// Versioned plugin visuals, refreshed after the editor synchronizes its buffer. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub struct PluginLanguageVisuals { + pub version: u64, + pub injections: Vec, + pub colors: Vec<(String, ide_plugin::language_features::ColorInfo)>, + pub gutter_actions: Vec<(String, ide_plugin::language_features::GutterAction)>, +} +pub async fn plugin_language_visuals(host: &Host, window: &str, doc: DocId) -> IdeResult { + let snap = host.project(window)?.documents.snapshot(doc)?; + blocking(move || { + let d = ProviderDocument { + path: snap.path.0, + language: snap.language.0, + version: u64::from(snap.version), + text: snap.text, + }; + let mut out = + PluginLanguageVisuals { version: d.version, injections: vec![], colors: vec![], gutter_actions: vec![] }; + if d.text.len() <= crate::plugins::navigation::MAX_DOCUMENT_BYTES + && let Some(plugins) = crate::plugins::editor_plugin_host() + { + out.injections = plugins.provide_injections(&d); + out.colors = plugins.language_colors(&d); + out.gutter_actions = plugins.language_gutter_actions(&d); + } + Ok(out) + }) + .await +} +pub async fn plugin_color_presentations( + host: &Host, + window: &str, + doc: DocId, + plugin: String, + color: ide_plugin::language_features::ColorInfo, +) -> IdeResult> { + let snap = host.project(window)?.documents.snapshot(doc)?; + blocking(move || { + let d = ProviderDocument { + path: snap.path.0, + language: snap.language.0, + version: u64::from(snap.version), + text: snap.text, + }; + if !crate::plugins::language_features::valid_range(&d, color.range) { + return Err(ide_core::IdeError::invalid("Invalid color range")); + } + Ok(crate::plugins::editor_plugin_host() + .map(|p| p.language_color_presentations(&plugin, &d, color)) + .unwrap_or_default()) + }) + .await +} +pub fn plugin_formatters() -> Vec { + crate::plugins::editor_plugin_host().map(|p| p.formatter_choices()).unwrap_or_default() +} + pub(crate) fn register(d: &mut Dispatcher) { + d.add( + "plugin_language_visuals", + |c| async move { plugin_language_visuals(&c.host, &c.window, c.arg("doc")?).await }, + ); + d.add("plugin_color_presentations", |c| async move { + plugin_color_presentations(&c.host, &c.window, c.arg("doc")?, c.arg("plugin")?, c.arg("color")?).await + }); + d.add("plugin_formatters", |_c| async move { Ok(plugin_formatters()) }); d.add("lsp_completion", |c| async move { lsp_completion(&c.host, &c.window, c.arg("request")?, c.arg("doc")?, c.arg("pos")?, c.arg("trigger")?).await }); @@ -1408,6 +1636,32 @@ pub(crate) fn register(d: &mut Dispatcher) { #[cfg(test)] mod tests { + #[test] + fn language_features_merge_without_overwriting_lsp_ranges_or_masking_errors() { + use super::*; + let range = Range::new(Pos::default(), Pos::new(0, 1)); + let existing = Location::file("/p/a".into(), range); + let merged = merge_language_locations( + vec![existing.clone()], + vec![ + ide_plugin::Location { path: "/p/a".into(), range }, + ide_plugin::Location { path: "/p/b".into(), range }, + ], + ); + assert_eq!(merged.len(), 2); + assert_eq!(merged[0], existing); + let merged = merge_language_highlights( + vec![Highlight { range, kind: HighlightKind::Read }], + vec![ide_plugin::language_features::Highlight { range, kind: "write".into() }], + ); + assert_eq!(merged.len(), 1); + assert_eq!(merged[0].kind, HighlightKind::Read); + assert!( + provider_server::>(Err(ide_core::IdeError::unavailable("no server"))).unwrap().is_empty() + ); + assert!(provider_server::>(Err(ide_core::IdeError::cancelled())).is_err()); + } + use super::*; #[test] @@ -1430,11 +1684,13 @@ mod tests { use crate::plugins::navigation::{IndexedAnchors, IndexedSymbol}; use ide_core::{FileEdit, LanguageId}; let tmp = tempfile::tempdir().unwrap(); - let root = tmp.path().canonicalize().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); let shared = root.join("shared.idx"); let native = root.join("native.txt"); std::fs::write(&shared, "old old").unwrap(); std::fs::write(&native, "old").unwrap(); + // Exercise legacy verbatim index paths alongside ordinary native URI paths. + let shared = std::fs::canonicalize(shared).unwrap(); let range = Range::new(Pos::new(0, 0), Pos::new(0, 3)); let other = Range::new(Pos::new(0, 4), Pos::new(0, 7)); let symbol = IndexedSymbol { @@ -1446,8 +1702,6 @@ mod tests { ranges: vec![range, other], }], }; - // Native file URIs have ordinary Windows paths; index snapshots retain - // canonical verbatim paths. Both identify the same file and edit batch. let server_shared = dunce::simplified(&shared).to_path_buf(); let server_native = dunce::simplified(&native).to_path_buf(); #[cfg(windows)] @@ -1771,7 +2025,7 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path().join("proj"); std::fs::create_dir_all(&root).unwrap(); - let root = root.canonicalize().unwrap(); + let root = dunce::canonicalize(&root).unwrap(); let action = |operations: Vec| ide_plugin::CodeAction { title: "Create view".into(), kind: Some("quickfix".into()), diff --git a/crates/ide-host/src/ops/run.rs b/crates/ide-host/src/ops/run.rs index 558ce392b..e04d3c0b7 100644 --- a/crates/ide-host/src/ops/run.rs +++ b/crates/ide-host/src/ops/run.rs @@ -9,6 +9,9 @@ use std::time::Duration; pub fn configs(host: &Host, window: &str) -> IdeResult> { host.require_trust(window)?; let project = host.project(window)?; + if let Some(plugins) = host.plugin_host() { + plugins.refresh_run_configurations(&project.root); + } project_configs(&project.root, &project.settings.effective(), &host.config_dir) } @@ -199,8 +202,48 @@ pub fn apply_settings(host: &Host, window: &str, configs: Vec) -> Ide } pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit) -> IdeResult { + let operation = config.extras.params.get("plugin.operation").cloned(); + let result = start_inner(host, window, config, on_output).await; + if let (Ok(info), Some(operation), Some(plugins)) = (&result, operation, host.plugin_host()) { + plugins.run_operation_started(window, &operation, info.id); + } + result +} + +async fn start_inner( + host: &Host, + window: &str, + mut config: RunConfig, + on_output: Emit, +) -> IdeResult { host.require_trust(window)?; let project = host.project(window)?; + if config.extras.target.is_none() { + config.extras.target = ide_sdk::apply::run_target(&config, &project.settings.effective()).map(str::to_owned); + } + if config.detected + && config.id.starts_with(ide_run::EXTERNAL_PREFIX) + && let Some(plugins) = host.plugin_host() + { + let provider = plugins.run_detection(); + let binary = + provider.launch(&config).map_err(|e| IdeError::new(ide_core::error::ErrorKind::PermissionDenied, e))?; + config.command = if config.extras.target.as_ref().is_some_and(|t| !t.is_empty()) { + provider + .detect(&project.root) + .into_iter() + .find(|c| c.id == config.id) + .ok_or_else(|| IdeError::not_found("plugin configuration no longer available"))? + .command + } else { + binary.to_string_lossy().into_owned() + }; + config.detected = false; + } + let mut sink = project.sink.clone(); + if let Some(plugins) = host.plugin_host() { + sink = plugins.run_operation_sink(window, &mut config, sink)?; + } if config.command.is_empty() { return Err(IdeError::invalid("run configuration has no command")); } @@ -210,6 +253,25 @@ pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit } if let Some(id) = config.extras.target.as_deref().filter(|t| !t.is_empty()) { let settings = project.settings.effective(); + if id.starts_with("plugin-target:") { + let plugins = + host.plugin_host().ok_or_else(|| IdeError::not_found("plugin run target is no longer available"))?; + let root = project.root.clone(); + let id = id.to_owned(); + let config = host.blocking(move || plugins.prepare_run_target(&root, &id, &config)).await?; + project.require_trust()?; + return host.processes.start( + &project.root, + &config, + &StartOptions { + encoding: Some(settings.console.encoding).filter(|e| !e.trim().is_empty()), + python_paths: Vec::new(), + ruby_reporter: None, + }, + sink, + Box::new(move |c| on_output(c)), + ); + } let target = ide_remote::service::run_target(&settings, id)?; if target.kind != RunTargetKind::Ssh { // A local run of the docker CLI: the command is resolved in the container, and @@ -227,15 +289,12 @@ pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit &project.root, &config, &options, - project.sink.clone(), + sink.clone(), Box::new(move |c| on_output(c)), ); } let out = on_output.clone(); - return host - .remote - .run(&settings, &project.root, &config, project.sink.clone(), Box::new(move |c| out(c))) - .await; + return host.remote.run(&settings, &project.root, &config, sink.clone(), Box::new(move |c| out(c))).await; } let mut config = config; let (root, settings) = settings_for(host, &project, &mut config)?; @@ -286,7 +345,6 @@ pub async fn start(host: &Host, window: &str, config: RunConfig, on_output: Emit ruby_reporter: ruby_reporter_dir(host, &config), }; project.require_trust()?; - let mut sink = project.sink.clone(); if config.kind == "go" && matches!(config.args.first().map(String::as_str), Some("mod" | "work")) { let previous = sink; let root = project.root.clone(); @@ -317,6 +375,9 @@ fn ruby_reporter_dir(host: &Host, config: &RunConfig) -> Option IdeResult<()> { + if let Some(plugins) = host.plugin_host() { + plugins.run_operation_stopped(id); + } let settings = match host.project(window) { Ok(p) => p.settings.effective(), Err(_) => host.global_settings.effective(), diff --git a/crates/ide-host/src/ops/search.rs b/crates/ide-host/src/ops/search.rs index ac2de1168..61d90c345 100644 --- a/crates/ide-host/src/ops/search.rs +++ b/crates/ide-host/src/ops/search.rs @@ -34,67 +34,12 @@ pub async fn start( // Excluded roots (Project Structure marks and detected `target`, `node_modules`, …) and // ignored names are never searched. query.exclude.extend(project.search_excludes()); - let roots = project.roots(); - let set = match query.scope { - ide_search::SearchScope::FileSet { set } => Some(set), - _ => None, - }; let cancel = host.begin_request(request); - let documents = project.documents.clone(); - let root = project.root.clone(); - let workspace = ide_workspace::workspace::Workspace::load(&root)?.is_some(); - // The whole project also covers the Project Structure content roots. - let content_roots = match query.scope { - ide_search::SearchScope::Project - | ide_search::SearchScope::FileSet { .. } - | ide_search::SearchScope::Pattern { .. } => project.content_roots(), - _ => Vec::new(), - }; - let include_root = !workspace - || matches!(query.scope, ide_search::SearchScope::Directory { .. } | ide_search::SearchScope::Files { .. }); - let languages = project.languages.clone(); - let syntax = host.syntax(); let events = on_event.clone(); + let syntax = host.syntax(); let summary = host .blocking(move || { - let filter = |rel: &str| match set { - Some(ProjectFileSet::Production) => !roots.is_test(rel), - Some(ProjectFileSet::Test) => roots.is_test(rel), - Some(ProjectFileSet::All) | None => true, - }; - // Comments and string literals for the search context filter. - let literals = |path: &Path, text: &str| { - let language = languages.detect(path, text.lines().next()); - let found = syntax.literals(&language, text)?; - Some(found.into_iter().map(literal_range).collect()) - }; - let mut total = SearchSummary::default(); - for dir in std::iter::once(&root).filter(|_| include_root).chain(&content_roots) { - let s = ide_search::search_in_context( - dir, - &query, - Some(&filter), - Some(&literals), - &|p| documents.search_text_of(p), - &cancel, - &|mut file| { - if dir != &root { - let name = dir.file_name().unwrap_or_default().to_string_lossy(); - file.rel_path = format!("{name}/{}", file.rel_path); - } - events(SearchEvent::File { file }); - }, - )?; - total.files += s.files; - total.matches += s.matches; - total.elapsed_ms += s.elapsed_ms; - total.limit_hit |= s.limit_hit; - total.cancelled |= s.cancelled; - if total.limit_hit || total.cancelled { - break; - } - } - Ok(total) + search_project(&project, &syntax, &query, &cancel, &|file| events(SearchEvent::File { file })) }) .await; host.end_request(request); @@ -102,5 +47,69 @@ pub async fn start( Ok(()) } +/// Shared Find in Files engine: project excludes are added by the caller. +pub(crate) fn search_project( + project: &crate::project::Project, + syntax: &ide_language::Syntax, + query: &SearchQuery, + cancel: &ide_core::CancellationToken, + emit: &(dyn Fn(FileMatches) + Sync), +) -> IdeResult { + let root = &project.root; + let roots = project.roots(); + let set = match query.scope { + ide_search::SearchScope::FileSet { set } => Some(set), + _ => None, + }; + let workspace = ide_workspace::workspace::Workspace::load(root)?.is_some(); + let include_root = !workspace + || matches!(query.scope, ide_search::SearchScope::Directory { .. } | ide_search::SearchScope::Files { .. }); + let content_roots = match query.scope { + ide_search::SearchScope::Project + | ide_search::SearchScope::FileSet { .. } + | ide_search::SearchScope::Pattern { .. } => project.content_roots(), + _ => Vec::new(), + }; + let filter = |rel: &str| match set { + Some(ProjectFileSet::Production) => !roots.is_test(rel), + Some(ProjectFileSet::Test) => roots.is_test(rel), + _ => true, + }; + let literals = |path: &Path, text: &str| { + let language = project.languages.detect(path, text.lines().next()); + Some(syntax.literals(&language, text)?.into_iter().map(literal_range).collect()) + }; + let mut total = SearchSummary::default(); + for dir in std::iter::once(root).filter(|_| include_root).chain(&content_roots) { + let mut remaining = query.clone(); + let max = if query.max_matches == 0 { 10_000 } else { query.max_matches }; + remaining.max_matches = max.saturating_sub(total.matches).max(1); + let summary = ide_search::search_in_context( + dir, + &remaining, + Some(&filter), + Some(&literals), + &|p| project.documents.search_text_of(p), + cancel, + &|mut file| { + if dir != root { + let name = dir.file_name().unwrap_or_default().to_string_lossy(); + file.rel_path = format!("{name}/{}", file.rel_path); + } + emit(file); + }, + )?; + total.files += summary.files; + total.matches += summary.matches; + total.elapsed_ms += summary.elapsed_ms; + total.limit_hit |= summary.limit_hit || total.matches >= max; + total.cancelled |= summary.cancelled; + if total.limit_hit || total.cancelled { + break; + } + } + Ok(total) +} + mod commands; pub use commands::*; diff --git a/crates/ide-host/src/plugins/api.rs b/crates/ide-host/src/plugins/api.rs index 0b2824730..70892f2de 100644 --- a/crates/ide-host/src/plugins/api.rs +++ b/crates/ide-host/src/plugins/api.rs @@ -4,6 +4,7 @@ use super::{CommitAnswer, CommitRequest, CommitSelection, PluginContextKey, PluginEvent, PluginEventSink}; use ide_core::ipc_path::to_ipc; use ide_core::{CancellationToken, TextEdit}; +use ide_extension::Capability; use ide_extension::Package; use ide_language::ProgramPin; use ide_plugin::{ @@ -37,10 +38,48 @@ type ContextKey = (Option, String, String); /// What plugins see of the open projects; provided by the app /// ([`super::PluginHost::set_workspace`]), over [`crate::Host`] on the desktop. pub trait PluginWorkspace: Send + Sync { + /// Execution host of the Database subsystem; absent on unsupported hosts. + fn database_host(&self) -> Option> { + None + } + + /// A freshly resolved configuration owned by this plugin in the invoking project. + fn run_configuration(&self, _client: &str, _plugin: &str, _config: &str) -> Result { + Err(HostError::NotFound("run configuration unavailable".into())) + } + + /// Window-owned reports shared with the MCP tools. + fn open_editors(&self, _client: &str) -> Result, HostError> { + Ok(Vec::new()) + } + /// Shared Find in Files service, preferring the invoking window's document store. + fn search_text( + &self, + _client: Option<&str>, + _root: &Path, + _query: ide_plugin::TextQuery, + _limit: u32, + _cancel: &CancellationToken, + ) -> Result { + Err(HostError::Failed("search unavailable".into())) + } + /// Problems view reports for this window's project. + fn problems(&self, _client: &str, _path: Option<&Path>) -> Result, HostError> { + Ok(Vec::new()) + } + /// Roots of the open projects. fn projects(&self) -> Vec; /// Unsaved text of the open document at `path`. fn buffer_text(&self, path: &Path) -> Option; + /// A window's unsaved text, with the normal project overlay as fallback. + fn buffer_text_in(&self, _client: Option<&str>, path: &Path) -> Option { + self.buffer_text(path) + } + /// An editor owned by this window. + fn document_in(&self, _client: &str, path: &Path) -> Option<(u64, String)> { + self.document(path) + } /// Whether the editor holds changes for `path` that are not on disk yet; /// writing such a file would lose them. fn buffer_dirty(&self, _path: &Path) -> bool { @@ -157,6 +196,34 @@ pub(super) fn net_runtime() -> &'static tokio::runtime::Runtime { }) } +#[derive(Debug)] +struct RedirectDenied(String); +impl std::fmt::Display for RedirectDenied { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} is not granted for this redirect", self.0) + } +} +impl std::error::Error for RedirectDenied {} + +fn network_error(error: reqwest::Error) -> HostError { + let mut source: Option<&(dyn std::error::Error + 'static)> = Some(&error); + while let Some(e) = source { + if let Some(denied) = e.downcast_ref::() { + return HostError::Denied(denied.0.clone()); + } + source = e.source(); + } + HostError::Failed(error.to_string()) +} + +fn redirect_capability(from: &reqwest::Url, to: &reqwest::Url) -> Result { + if from.scheme() == "https" && to.scheme() == "http" { + ide_plugin::net_host(to.as_str()).map(Capability::NetHttp) + } else { + ide_plugin::net_capability(to.as_str()) + } +} + /// Replaces `{{secret:}}` in `value` with the plugin's secrets. fn fill_secrets(value: &str, lookup: impl Fn(&str) -> Option) -> Result { let mut out = String::with_capacity(value.len()); @@ -182,15 +249,16 @@ struct TaskEntry { } /// A question a plugin's call waits on: the user's (a prompt) or a window's. -struct PromptEntry { - plugin: String, - client: String, - answer: Waiter, +pub(super) struct PromptEntry { + pub(super) plugin: String, + pub(super) client: String, + pub(super) answer: Waiter, } -enum Waiter { +pub(super) enum Waiter { Prompt(std::sync::mpsc::SyncSender>>), Commit(std::sync::mpsc::SyncSender), + Command(std::sync::mpsc::SyncSender), } /// A process a plugin started and has not released (API 1.3 `process.start`). @@ -405,6 +473,13 @@ fn append_process_env(command: &mut StdCommand, env: &[(String, String)]) -> Res Ok(()) } +/// Approved project-specific programs. +pub(super) type ProjectPins = Arc>>; + +type ContentRequests = BTreeMap<(String, String, String), (String, Instant)>; + +type SettingSnapshots = BTreeMap<(Option, String), BTreeMap>; + /// State shared by the host and every plugin's [`PluginApi`]. pub(super) struct Shared { pub sink: Arc, @@ -416,16 +491,28 @@ pub(super) struct Shared { /// (plugin, handle id) → the processes it started and has not released. processes: Mutex>>, next_process: AtomicU64, + connections: super::ipc::Connections, + pub loopbacks: RwLock>, process_shutdowns: Mutex>>, - prompts: Mutex>, + pub(super) prompts: Mutex>, /// Plugins being stopped: their calls may not open new prompts (taken before `prompts`). - prompts_blocked: Mutex>, - next_prompt: AtomicU64, + pub(super) prompts_blocked: Mutex>, + pub(super) next_prompt: AtomicU64, + pub(super) run_configurations: super::run_detectors::DynamicConfigurations, + pub(super) run_targets: Mutex, + pub(super) operations: Mutex>, + pub(super) content_tabs: Mutex>, + pub(super) content_requests: Mutex, + /// (plugin, item id) → item, for windows opened later. + output: Mutex>, + decorations: Mutex>>, status: Mutex>, + setting_snapshots: Mutex, + window_status: Mutex>, /// (scope, plugin, key) → JSON value. contexts: Mutex>, - closed_clients: Mutex>, + pub(super) closed_clients: Mutex>, /// plugin → kinds subscribed at run time (kept across idle reclaim). pub subscriptions: Mutex, /// plugin → files it shows problems for, withdrawn when it stops. @@ -433,6 +520,8 @@ pub(super) struct Shared { pub secrets: RwLock>, /// (plugin, `process:` capability) → the approved program. pub pins: Arc>>, + pub project_pins: ProjectPins, + pub event_editors: RwLock>, /// Validated UI declarations (settings schemas, declared views). pub ui: Arc, /// The UI language the windows report (BCP 47). @@ -463,16 +552,30 @@ impl Shared { workspace: RwLock::new(None), secrets: RwLock::new(Arc::new(FileSecretStore { dir: data_dir.clone() })), pins, + project_pins: Arc::default(), + event_editors: RwLock::default(), data_dir, tasks: Mutex::default(), next_task: AtomicU64::new(1), processes: Mutex::default(), next_process: AtomicU64::new(1), + connections: super::ipc::Connections::default(), + loopbacks: RwLock::new(Arc::new(super::auth::Loopbacks::default())), process_shutdowns: Mutex::default(), prompts: Mutex::default(), prompts_blocked: Mutex::default(), next_prompt: AtomicU64::new(1), + run_configurations: Arc::default(), + run_targets: Mutex::default(), + operations: Mutex::default(), + content_tabs: Mutex::default(), + content_requests: Mutex::default(), + status: Mutex::default(), + output: Mutex::default(), + decorations: Mutex::default(), + window_status: Mutex::default(), + setting_snapshots: Mutex::default(), contexts: Mutex::default(), closed_clients: Mutex::default(), subscriptions: Mutex::default(), @@ -518,6 +621,45 @@ impl Shared { self.status.lock().iter().map(|((p, id), item)| (p.clone(), id.clone(), item.clone())).collect() } + pub fn changed_settings(self: &Arc, pkg: &Package, client: Option<&str>) -> Vec { + let root = client.and_then(|c| self.project_of(c)); + let api = PluginApi { pkg: pkg.clone(), shared: self.clone() }; + let mut current = BTreeMap::new(); + for item in self.ui.items(super::ui_points::SETTINGS, Some(pkg.id())) { + if let Some(properties) = item.decl.get("properties").and_then(|p| p.as_array()) { + for property in properties { + if let Some(key) = property.get("key").and_then(|k| k.as_str()) + && let Some(value) = api.setting(root.as_deref(), key) + { + current.insert(key.into(), value); + } + } + } + } + let previous = + self.setting_snapshots.lock().insert((client.map(str::to_owned), pkg.id().into()), current.clone()); + previous + .map(|old| current.into_iter().filter(|(k, v)| old.get(k) != Some(v)).map(|(k, _)| k).collect()) + .unwrap_or_default() + } + + pub fn status_snapshot(&self, client: &str) -> Vec { + let mut items: Vec<_> = self + .status_items() + .into_iter() + .map(|(plugin_id, id, item)| super::PluginStatusItem { plugin_id, id, item, window: false }) + .collect(); + items.extend(self.window_status.lock().iter().filter(|((w, ..), _)| w == client).map( + |((_, plugin_id, id), item)| super::PluginStatusItem { + plugin_id: plugin_id.clone(), + id: id.clone(), + item: item.clone(), + window: true, + }, + )); + items + } + pub fn context_keys(&self, client: Option<&str>) -> Vec { let contexts = self.contexts.lock(); let mut keys: BTreeMap<(&str, &str), &str> = BTreeMap::new(); @@ -542,6 +684,13 @@ impl Shared { self.prompts.lock().retain(|_, entry| entry.client != client); drop(closed); self.contexts.lock().retain(|(scope, ..), _| scope.as_deref() != Some(client)); + self.loopbacks.read().forget_window(client); + self.window_status.lock().retain(|(window, ..), _| window != client); + self.operations.lock().retain(|_, op| op.client != client); + self.content_tabs.lock().retain(|(_, window, _), _| window != client); + self.content_requests.lock().retain(|(_, window, _), _| window != client); + self.event_editors.write().remove(client); + self.setting_snapshots.lock().retain(|(window, _), _| window.as_deref() != Some(client)); let mine: Vec<_> = { let mut processes = self.processes.lock(); let keys: Vec<_> = processes @@ -597,7 +746,28 @@ impl Shared { /// Withdraws a plugin's run-time state (it stopped or failed). Its /// subscriptions go only with `subscriptions`, as they survive restarts. pub fn clear_plugin(&self, plugin: &str, subscriptions: bool) { + self.withdraw_run_targets(Some(plugin), None); + self.run_configurations.write().retain(|(p, _), _| p != plugin); + self.operations.lock().retain(|_, op| op.plugin != plugin); + self.content_tabs.lock().retain(|(p, _, _), _| p != plugin); + self.content_requests.lock().retain(|(p, _, _), _| p != plugin); + self.kill_processes(plugin); + self.connections.clear(plugin); + self.setting_snapshots.lock().retain(|(_, p), _| p != plugin); + let channels: Vec<_> = { + let mut output = self.output.lock(); + let keys: Vec<_> = output.keys().filter(|(p, _)| p == plugin).cloned().collect(); + for key in &keys { + output.remove(key); + } + keys + }; + for (_, channel) in channels { + self.sink.emit(None, PluginEvent::OutputChanged { plugin_id: plugin.into(), channel, text: None }); + } + + self.loopbacks.read().clear(plugin); let removed: Vec = { let mut status = self.status.lock(); let ids: Vec = status.keys().filter(|(p, _)| p == plugin).map(|(_, id)| id.clone()).collect(); @@ -609,6 +779,21 @@ impl Shared { for id in removed { self.sink.emit(None, PluginEvent::StatusItemChanged { plugin_id: plugin.to_owned(), id, item: None }); } + let removed: Vec<_> = { + let mut items = self.window_status.lock(); + let keys: Vec<_> = items.keys().filter(|(_, p, _)| p == plugin).cloned().collect(); + for key in &keys { + items.remove(key); + } + keys + }; + for (window, _, id) in removed { + self.sink + .emit(Some(&window), PluginEvent::WindowStatusItemChanged { plugin_id: plugin.into(), id, item: None }); + } + if self.decorations.lock().remove(plugin).is_some() { + self.sink.emit(None, PluginEvent::DecorationsChanged { plugin_id: plugin.into(), sets: Vec::new() }); + } self.withdraw_prompts(plugin); let removed: Vec<(Option, String)> = { let mut contexts = self.contexts.lock(); @@ -699,6 +884,16 @@ impl Shared { } /// A window answered a [`CommitRequest`]. + pub fn answer_command(&self, client: &str, id: u64, answer: super::CommandAnswer) -> bool { + let mut prompts = self.prompts.lock(); + if !matches!(prompts.get(&id), Some(PromptEntry { client: window, answer: Waiter::Command(_), .. }) if window == client) + { + return false; + } + let Some(PromptEntry { answer: Waiter::Command(tx), .. }) = prompts.remove(&id) else { return false }; + tx.try_send(answer).is_ok() + } + pub fn answer_commit(&self, id: u64, answer: CommitAnswer) -> bool { let mut prompts = self.prompts.lock(); if !matches!(prompts.get(&id), Some(PromptEntry { answer: Waiter::Commit(_), .. })) { @@ -802,7 +997,17 @@ impl PluginApi { /// The program approved for the (already checked) `process:` grant, /// verified right before it starts. + #[cfg(test)] fn approved_program(&self, program: &str) -> Result { + self.approved_program_in_project(program, None) + } + + pub(super) fn approved_program_in_project(&self, program: &str, root: Option<&Path>) -> Result { + if let Some(pin) = root.and_then(|root| { + self.shared.project_pins.read().get(&(self.id().into(), format!("process:{program}"), root.into())).cloned() + }) { + return pin.verify().map(Path::to_path_buf).map_err(|e| HostError::Denied(e.to_string())); + } let cap = format!("process:{program}"); // Granted without a pinned file: the program wasn't found when it was // allowed, and installing it later approves nothing by itself. @@ -828,7 +1033,7 @@ impl PluginApi { cancel: &CancellationToken, on_line: &dyn Fn(super::PluginOutputLine), ) -> Result, HostError> { - let binary = self.approved_program(program)?; + let binary = self.approved_program_in_project(program, cwd)?; let cwd = cwd.map(Path::to_path_buf).unwrap_or_else(|| self.shared.data_dir.join(self.id())); let _ = std::fs::create_dir_all(&cwd); let mut cmd = std::process::Command::new(&binary); @@ -1051,6 +1256,500 @@ fn failed(e: impl std::fmt::Display) -> HostError { } impl HostApi for PluginApi { + fn database_upsert(&self, root: &Path, source: ide_plugin::PluginDataSource) -> Result<(), HostError> { + let (host, db) = self.database_host(root)?; + let id = source.id.clone(); + let source = super::database::settings(source)?; + self.remember_database_project(root, &id)?; + host.runtime.block_on(db.upsert_plugin_source(self.pkg.id(), &id, source)).map_err(super::database::error)?; + let internal = ide_db::plugin::source_id(self.pkg.id(), &id); + for project in host.projects().into_iter().filter(|p| p.root == root) { + host.runtime.block_on(project.db.disconnect_source(&internal)); + } + super::database::changed(&host, root); + Ok(()) + } + fn database_remove(&self, root: &Path, id: &str) -> Result<(), HostError> { + let (host, db) = self.database_host(root)?; + host.runtime.block_on(db.remove_plugin_source(self.pkg.id(), id)).map_err(super::database::error)?; + let internal = ide_db::plugin::source_id(self.pkg.id(), id); + for project in host.projects().into_iter().filter(|p| p.root == root) { + host.runtime.block_on(project.db.forget_plugin_source(&internal)); + } + super::database::changed(&host, root); + Ok(()) + } + fn database_sources(&self, root: &Path) -> Result, HostError> { + let (_, db) = self.database_host(root)?; + db.plugin_sources(self.pkg.id()).map_err(super::database::error) + } + fn database_credential(&self, root: &Path, id: &str, password: String) -> Result<(), HostError> { + let (host, db) = self.database_host(root)?; + let source = db.plugin_source(self.pkg.id(), id).map_err(super::database::error)?; + self.remember_database_project(root, id)?; + for project in host.projects().into_iter().filter(|p| p.root == root) { + host.runtime.block_on(project.db.set_password(&source.id, Some(password.clone()), true)).map_err(failed)?; + } + Ok(()) + } + fn database_connect( + &self, + root: &Path, + client: &str, + id: &str, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + self.connect_database(root, client, id, cancel, timeout) + } + fn mcp_stdio_command(&self, client: &str) -> Option> { + let host = self.shared.workspace()?.database_host()?; + let root = host.project(client).ok()?.root.clone(); + host.mcp_executable.read().as_ref().map(|exe| ide_mcp::stdio_command(exe, &root)) + } + + fn register_run_target(&self, root: &Path, target: ide_plugin::PluginRunTarget) -> Result<(), HostError> { + self.register_target(root, target) + } + fn unregister_run_target(&self, root: &Path, id: &str) { + self.unregister_target(root, id); + } + + fn start_run( + &self, + client: &str, + config: &str, + args: Vec, + debug: bool, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result { + let _ = timeout; + if cancel.is_cancelled() { + return Err(HostError::Cancelled); + } + let mut run = + self.shared.workspace().ok_or(HostError::Cancelled)?.run_configuration(client, self.id(), config)?; + let binary = self.approved_program_in_project(&run.command, self.shared.project_of(client).as_deref())?; + if debug && run.debug_adapter.is_none() { + return Err(HostError::Invalid("this configuration has no debugger".into())); + } + let program = run.command.clone(); + if debug && run.extras.target.as_ref().is_some_and(|t| !t.is_empty()) { + return Err(HostError::Invalid("debugging this target is not supported".into())); + } + if debug { + run.command = binary.to_string_lossy().into_owned(); + } + run.args.extend(args); + let id = self.new_operation( + client, + super::execution::OperationKind::Run { config: config.into(), configuration: run.id.clone(), program }, + )?; + run.extras.params.insert(super::execution::RUN_OPERATION.into(), id.clone()); + let request = super::WindowRequest::Run { operation: id.clone(), config: Box::new(run), debug }; + self.shared.sink.emit( + Some(client), + PluginEvent::WindowRequest { plugin_id: self.id().into(), request_id: String::new(), request }, + ); + Ok(id) + } + fn open_terminal_session( + &self, + client: &str, + key: String, + title: String, + cmd: ProcessCommand, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result { + super::execution::label(&key, 128)?; + super::execution::label(&title, 256)?; + let root = + self.shared.project_of(client).ok_or_else(|| HostError::Invalid("no project in this window".into()))?; + if !self.shared.workspace().is_some_and(|w| w.trusted(&root)) { + return Err(HostError::Denied("project is not trusted".into())); + } + let binary = self.approved_program_in_project(&cmd.program, Some(&root))?; + let mut command = StdCommand::new(&binary); + append_process_env(&mut command, &cmd.env)?; + if cmd.args.iter().any(|a| a.contains('\0')) { + return Err(HostError::Invalid("NUL in terminal argument".into())); + } + let existing = self + .shared + .operations + .lock() + .iter() + .find(|(_, op)| { + op.plugin == self.id() + && op.client == client + && matches!(&op.kind, super::execution::OperationKind::Terminal { key: k, .. } if k == &key) + }) + .map(|(id, _)| id.clone()); + let id = match existing { + Some(id) => id, + None => self.new_operation( + client, + super::execution::OperationKind::Terminal { key: key.clone(), command: cmd.clone() }, + )?, + }; + let request = super::WindowRequest::Terminal { + operation: id.clone(), + key, + title, + program: binary.into(), + args: cmd.args, + env: cmd.env.into_iter().collect(), + cwd: root.into(), + }; + if let Err(error) = self.window_request(client, request, cancel, timeout) { + self.shared.operations.lock().remove(&id); + return Err(error); + } + Ok(id) + } + fn write_terminal( + &self, + client: &str, + key: &str, + text: String, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + let root = self.shared.project_of(client).ok_or(HostError::Cancelled)?; + if !self.shared.workspace().is_some_and(|w| w.trusted(&root)) { + return Err(HostError::Denied("project is not trusted".into())); + } + let _ = timeout; + if cancel.is_cancelled() { + return Err(HostError::Cancelled); + } + let terminal = self + .shared + .operations + .lock() + .iter() + .find(|(_, op)| { + op.plugin == self.id() + && op.client == client + && op.root.as_ref() == Some(&root) + && matches!(&op.kind, super::execution::OperationKind::Terminal { key: k, .. } if k == key) + }) + .and_then(|(_, op)| op.terminal.clone()) + .ok_or_else(|| HostError::NotFound("no running terminal with this key".into()))?; + let (host, id) = terminal; + host.upgrade().ok_or(HostError::Cancelled)?.terminals.write(id, &text).map_err(failed) + } + + fn open_content_tab( + &self, + client: &str, + tab: ide_plugin::ContentTab, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + super::execution::validate_tab(&tab)?; + let mut tabs = self.shared.content_tabs.lock(); + let key = (self.id().into(), client.into(), tab.key.clone()); + if !tabs.contains_key(&key) && tabs.keys().filter(|(p, w, _)| p == self.id() && w == client).count() >= 32 { + return Err(HostError::Invalid("at most 32 content tabs".into())); + } + let previous = tabs.insert(key.clone(), tab.clone()); + drop(tabs); + if let Err(error) = self.window_request(client, super::WindowRequest::ContentTab { tab }, cancel, timeout) { + let mut tabs = self.shared.content_tabs.lock(); + if let Some(previous) = previous { + tabs.insert(key, previous); + } else { + tabs.remove(&key); + } + return Err(error); + } + Ok(()) + } + fn reply_content_tab( + &self, + client: &str, + request: &str, + reply: ide_plugin::ContentTabReply, + ) -> Result<(), HostError> { + super::execution::validate_reply(&reply)?; + if self.shared.content_requests.lock().remove(&(self.id().into(), client.into(), request.into())).is_none() { + return Err(HostError::NotFound("unknown content action request".into())); + } + self.shared.sink.emit( + Some(client), + PluginEvent::ContentTabReply { plugin_id: self.id().into(), request_id: request.into(), reply }, + ); + Ok(()) + } + + fn owns_command(&self, id: &str) -> bool { + let commands = self.shared.ui.items(super::COMMANDS, None); + let mut matches = commands.iter().filter(|c| id == format!("plugin.{}.{}", c.plugin_id, c.key)).peekable(); + matches.peek().is_some() && matches.all(|c| c.plugin_id == self.id()) + } + + fn open_editors(&self, client: &str) -> Result, HostError> { + self.shared.workspace().ok_or_else(|| HostError::NotFound("window project".into()))?.open_editors(client) + } + fn editor_open(&self, client: &str, path: &Path, selection: Option) -> Result<(), HostError> { + self.shared.sink.emit(Some(client), PluginEvent::EditorOpen { path: path.into(), selection }); + Ok(()) + } + fn editor_ranges( + &self, + client: &str, + path: &Path, + ranges: Vec, + reveal: bool, + ) -> Result<(), HostError> { + let workspace = self.shared.workspace().ok_or_else(|| HostError::NotFound("window project".into()))?; + if !workspace.open_editors(client)?.iter().any(|e| Path::new(&e.path) == path) { + return Err(HostError::Invalid("editor does not belong to the invoking window".into())); + } + let (_, text) = + workspace.document_in(client, path).ok_or_else(|| HostError::NotFound("editor is closed".into()))?; + let document = ide_text::Document::new(&text); + for r in &ranges { + if !valid_range(&document, *r) { + return Err(HostError::Invalid("range outside document".into())); + } + } + self.shared.sink.emit(Some(client), PluginEvent::EditorRanges { path: path.into(), ranges, reveal }); + Ok(()) + } + fn search_text( + &self, + client: Option<&str>, + root: &Path, + query: ide_plugin::TextQuery, + limit: u32, + cancel: &CancellationToken, + deadline: Instant, + ) -> Result { + let workspace = self.shared.workspace().ok_or_else(|| HostError::NotFound("project closed".into()))?; + if query.pattern.len() > 16_384 || query.include.len() + query.exclude.len() > 256 { + return Err(HostError::Invalid("search query too large".into())); + } + let token = cancel.child_token(); + let timed = token.clone(); + let (done, wait) = std::sync::mpsc::channel(); + let timer = std::thread::spawn(move || { + if wait.recv_timeout(deadline.saturating_duration_since(Instant::now())).is_err() { + timed.cancel(); + } + }); + let result = workspace.search_text(client, root, query, limit.min(10_000), &token); + let _ = done.send(()); + let _ = timer.join(); + if cancel.is_cancelled() { + Err(HostError::Cancelled) + } else if Instant::now() >= deadline { + Err(HostError::Failed("text search timed out".into())) + } else { + result + } + } + fn problems(&self, client: &str, path: Option<&Path>) -> Result, HostError> { + self.shared.workspace().ok_or_else(|| HostError::NotFound("window project".into()))?.problems(client, path) + } + fn decorations(&self, path: Option<&Path>, set: &str, items: Vec) -> Result<(), HostError> { + self.set_decorations(path, set, items, None, false) + } + fn file_decorations( + &self, + path: Option<&Path>, + set: &str, + item: Option, + ) -> Result<(), HostError> { + self.set_decorations(path, set, Vec::new(), item, true) + } + fn vcs_log( + &self, + root: &Path, + path: Option<&str>, + limit: u32, + cancel: &CancellationToken, + ) -> Result, HostError> { + let repo = self.read_repo(root)?; + let path = path + .map(|p| ide_plugin::resolve_new_in_root(root, p).map(ide_core::IpcPath::from).map_err(HostError::Invalid)) + .transpose()?; + let Some(repo) = repo else { + return Ok(Vec::new()); + }; + if limit == 0 { + return Ok(Vec::new()); + } + let query = ide_vcs::LogQuery { path, limit: limit.min(1000), ..Default::default() }; + repo.log_cancellable(&query, cancel) + .map(|cs| { + cs.into_iter() + .map(|c| ide_plugin::CommitInfo { + id: c.id, + short_id: c.short_id, + author: c.author, + author_email: c.email, + date: (c.time_ms / 1000.0) as i64, + subject: c.summary, + body: c + .message + .split_once('\n') + .map_or(String::new(), |(_, body)| body.trim_start_matches('\n').into()), + parents: c.parents, + }) + .collect() + }) + .map_err(failed) + } + fn vcs_blame( + &self, + client: Option<&str>, + root: &Path, + path: &str, + ) -> Result, HostError> { + let repo = self.read_repo(root)?; + let path = ide_plugin::resolve_new_in_root(root, path).map_err(HostError::Invalid)?; + let Some(repo) = repo else { + return Ok(Vec::new()); + }; + let workspace = self.shared.workspace().unwrap(); + let text = workspace.buffer_text_in(client, &path); + repo.blame(&path, text.as_deref(), None, workspace.vcs_encoding(&path)) + .map(|lines| { + lines + .into_iter() + .enumerate() + .map(|(i, l)| ide_plugin::BlameLine { + line: i as u32, + commit: l.commit, + author: l.author, + date: (l.time_ms / 1000.0) as i64, + summary: l.summary, + }) + .collect() + }) + .map_err(failed) + } + fn vcs_diff( + &self, + root: &Path, + path: Option<&str>, + against: Option<&str>, + cancel: &CancellationToken, + ) -> Result, HostError> { + let repo = self.read_repo(root)?; + let path = path.map(|p| ide_plugin::resolve_new_in_root(root, p).map_err(HostError::Invalid)).transpose()?; + let Some(repo) = repo else { + return Ok(None); + }; + repo.working_tree_patch(path.as_deref(), against, 4 << 20, cancel).map(Some).map_err(failed) + } + + fn execute_command( + &self, + invocation: &ide_plugin::Invocation, + id: &str, + arg: Option, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result, HostError> { + let client = from_window(invocation.client.as_deref(), "execute commands")?; + let mut chain = invocation.command_chain.clone(); + chain.push(self.id().into()); + if chain.len() > 8 { + return Err(HostError::Invalid("command nesting limit (8)".into())); + } + for command in self.shared.ui.items(super::COMMANDS, None) { + if id == format!("plugin.{}.{}", command.plugin_id, command.key) && chain.contains(&command.plugin_id) { + return Err(HostError::Invalid("recursive plugin command".into())); + } + } + let request = self.shared.next_prompt.fetch_add(1, Ordering::Relaxed); + let (tx, rx) = std::sync::mpsc::sync_channel(1); + { + let closed = self.shared.closed_clients.lock(); + let blocked = self.shared.prompts_blocked.lock(); + if closed.contains(client) || blocked.contains(self.id()) { + return Err(HostError::Cancelled); + } + self.shared.prompts.lock().insert( + request, + PromptEntry { plugin: self.id().into(), client: client.into(), answer: Waiter::Command(tx) }, + ); + } + self.shared.sink.emit( + Some(client), + PluginEvent::ExecuteCommand { request_id: request.to_string(), id: id.into(), arg, chain }, + ); + let deadline = Instant::now() + timeout; + loop { + match rx.recv_timeout(Duration::from_millis(20)) { + Ok(super::CommandAnswer::Value(value)) => return Ok(value), + Ok(super::CommandAnswer::NotFound) => return Err(HostError::NotFound(id.into())), + Ok(super::CommandAnswer::Failed(error)) => return Err(HostError::Failed(error)), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) + if !cancel.is_cancelled() && Instant::now() < deadline => {} + Err(_) => { + self.shared.prompts.lock().remove(&request); + return Err(HostError::Cancelled); + } + } + } + } + fn output_write(&self, channel: &str, text: Option<&str>) -> Result<(), HostError> { + if channel.is_empty() || channel.len() > 128 || channel.chars().any(char::is_control) { + return Err(HostError::Invalid("output channel must be 1-128 bytes without control characters".into())); + } + let key = (self.id().to_owned(), channel.to_owned()); + let mut channels = self.shared.output.lock(); + if !channels.contains_key(&key) && channels.keys().filter(|(p, _)| p == self.id()).count() >= 32 { + return Err(HostError::Invalid("at most 32 output channels".into())); + } + let buffer = channels.entry(key).or_default(); + if let Some(text) = text { + let mut start = text.len().saturating_sub(1 << 20); + while !text.is_char_boundary(start) { + start += 1; + } + buffer.push_str(&text[start..]); + } else { + buffer.clear(); + } + let mut remove = buffer.len().saturating_sub(1 << 20); + while !buffer.is_char_boundary(remove) { + remove += 1; + } + buffer.drain(..remove); + let text = buffer.clone(); + drop(channels); + self.shared.sink.emit( + None, + PluginEvent::OutputChanged { plugin_id: self.id().into(), channel: channel.into(), text: Some(text) }, + ); + Ok(()) + } + fn output_reveal(&self, client: &str, channel: &str) -> Result<(), HostError> { + let key = (self.id().to_owned(), channel.to_owned()); + if !self.shared.output.lock().contains_key(&key) { + self.output_write(channel, None)?; + } + if self.shared.client_closed(client) { + return Err(HostError::Cancelled); + } + let text = self.shared.output.lock().get(&key).cloned().unwrap_or_default(); + self.shared.sink.emit( + Some(client), + PluginEvent::RevealOutput { plugin_id: self.id().into(), channel: channel.into(), text }, + ); + Ok(()) + } + fn require_api(&self, what: &str, minor: u64) -> Result<(), HostError> { + self.pkg.manifest.needs_api(what, minor).map_err(HostError::Invalid) + } + fn notify( &self, client: Option<&str>, @@ -1202,6 +1901,32 @@ impl HostApi for PluginApi { .emit(None, PluginEvent::StatusItemChanged { plugin_id: self.id().to_owned(), id: id.to_owned(), item }); } + fn window_status_item(&self, client: &str, id: &str, item: Option) -> Result<(), HostError> { + let closed = self.shared.closed_clients.lock(); + let blocked = self.shared.prompts_blocked.lock(); + if closed.contains(client) || blocked.contains(self.id()) { + return Err(HostError::Cancelled); + } + let key = (client.to_owned(), self.id().to_owned(), id.to_owned()); + let mut items = self.shared.window_status.lock(); + if let Some(item) = &item { + if !items.contains_key(&key) + && items.keys().filter(|(w, p, _)| w == client && p == self.id()).count() >= MAX_STATUS_ITEMS + { + return Err(HostError::Invalid("too many window status items".into())); + } + items.insert(key, item.clone()); + } else { + items.remove(&key); + } + drop(items); + self.shared.sink.emit( + Some(client), + PluginEvent::WindowStatusItemChanged { plugin_id: self.id().into(), id: id.into(), item }, + ); + Ok(()) + } + fn set_context(&self, client: Option<&str>, key: &str, value: Option) -> Result<(), HostError> { let map_key = (client.map(str::to_owned), self.id().to_owned(), key.to_owned()); { @@ -1419,25 +2144,32 @@ impl HostApi for PluginApi { } let method = reqwest::Method::from_bytes(request.method.to_ascii_uppercase().as_bytes()) .map_err(|_| HostError::Invalid(format!("method {:?}", request.method)))?; - let client = ide_http::client::shared().map_err(|e| HostError::Failed(e.message))?; - // A redirect to another host is refused after the fact (the shared - // client follows redirects). + let grants = request.grants.clone(); + let policy = reqwest::redirect::Policy::custom(move |attempt| { + if attempt.previous().len() >= 10 { + return attempt.error("too many redirects"); + } + let wanted = attempt.previous().last().map_or_else( + || ide_plugin::net_capability(attempt.url().as_str()), + |from| redirect_capability(from, attempt.url()), + ); + match wanted { + Ok(wanted) if grants.iter().any(|g| g.covers(&wanted)) => attempt.follow(), + Ok(wanted) => attempt.error(RedirectDenied(wanted.to_string())), + Err(error) => attempt.error(error), + } + }); + let client = ide_http::client::build_with_redirect(&ide_core::proxy_settings::proxy(), policy) + .map_err(|e| HostError::Failed(e.message))?; let builder = client.request(method, &request.url).headers(headers).timeout(request.timeout); let builder = match request.body { Some(body) => builder.body(body), None => builder, }; let cancel = cancel.clone(); - let host = ide_plugin::net_host(&request.url).map_err(HostError::Invalid)?; net_runtime().block_on(async move { let send = async { - let response = builder.send().await.map_err(|e| HostError::Failed(e.to_string()))?; - if ide_plugin::net_host(response.url().as_str()).ok().as_deref() != Some(host.as_str()) { - return Err(HostError::Denied(format!( - "net:{} (redirected)", - response.url().host_str().unwrap_or("?") - ))); - } + let response = builder.send().await.map_err(network_error)?; let status = response.status().as_u16(); let headers = response .headers() @@ -1465,6 +2197,55 @@ impl HostApi for PluginApi { self.shared.secrets.read().get(self.id(), name).is_some() } + fn secret_set(&self, name: &str, value: Option<&str>) -> Result<(), HostError> { + if !self.shared.ui.items(super::providers::SECRETS_POINT, Some(self.id())).iter().any(|s| s.key == name) { + return Err(HostError::NotFound(format!("undeclared secret {name:?}"))); + } + self.shared.secrets.read().set(self.id(), name, value).map_err(HostError::Failed) + } + fn begin_loopback(&self, client: &str, path: &str) -> Result<(u64, String), HostError> { + let closed = self.shared.closed_clients.lock(); + let blocked = self.shared.prompts_blocked.lock(); + if closed.contains(client) || blocked.contains(self.id()) { + return Err(HostError::Cancelled); + } + self.shared.loopbacks.read().begin(self.id(), client, path) + } + fn wait_loopback( + &self, + id: u64, + timeout: Duration, + cancel: &CancellationToken, + ) -> Result, HostError> { + self.shared.loopbacks.read().clone().wait(self.id(), id, timeout, cancel) + } + fn close_loopback(&self, id: u64) { + self.shared.loopbacks.read().close(self.id(), id); + } + fn ipc_connect(&self, name: &str, cancel: &CancellationToken, timeout: Duration) -> Result { + self.shared.connections.connect(self.id(), name, cancel, timeout) + } + fn ipc_write( + &self, + id: u64, + bytes: Vec, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + self.shared.connections.write(self.id(), id, bytes, cancel, timeout) + } + fn ipc_read( + &self, + id: u64, + max: usize, + timeout: Duration, + cancel: &CancellationToken, + ) -> Result, HostError> { + self.shared.connections.read(self.id(), id, max, timeout, cancel) + } + fn ipc_close(&self, id: u64) { + self.shared.connections.close(self.id(), id); + } fn run_process( &self, cmd: ProcessCommand, @@ -1473,7 +2254,7 @@ impl HostApi for PluginApi { cancel: &CancellationToken, timeout: Duration, ) -> Result { - let binary = self.approved_program(&cmd.program)?; + let binary = self.approved_program_in_project(&cmd.program, cwd.or(cmd.approval_root.as_deref()))?; let cwd = self.spawn_dir(cwd); let mut command = StdCommand::new(&binary); ide_core::process::prepare(&mut command); @@ -1554,7 +2335,7 @@ impl HostApi for PluginApi { } } fn start_process(&self, cmd: ProcessCommand, cwd: Option<&Path>, client: Option<&str>) -> Result { - let binary = self.approved_program(&cmd.program)?; + let binary = self.approved_program_in_project(&cmd.program, cwd.or(cmd.approval_root.as_deref()))?; let cwd = self.spawn_dir(cwd); if client.is_some_and(|c| self.shared.closed_clients.lock().contains(c)) { return Err(HostError::Cancelled); @@ -1777,7 +2558,7 @@ impl HostApi for PluginApi { args: Vec, cwd: Option<&Path>, ) -> Result<(), HostError> { - let binary = self.approved_program(program)?; + let binary = self.approved_program_in_project(program, cwd)?; let cwd = cwd.map(Path::to_path_buf).unwrap_or_else(|| self.shared.data_dir.join(self.id())); let _ = std::fs::create_dir_all(&cwd); let event = PluginEvent::OpenTerminal { @@ -1810,7 +2591,7 @@ impl HostApi for PluginApi { log: ide_plugin::ServiceLog, ) -> Result<(), HostError> { // Fails now, rather than in the tab, when the approved file is gone or changed. - self.approved_program(&log.program)?; + self.approved_program_in_project(&log.program, self.shared.project_of(client).as_deref())?; { let mut all = self.shared.log_tabs.lock(); let tabs = all.entry((self.id().to_owned(), client.to_owned())).or_default(); @@ -1849,7 +2630,7 @@ impl HostApi for PluginApi { ide_vfs::create_private_dir(&data).map_err(failed)?; let files = data.join("files"); ide_vfs::create_private_dir(&files).map_err(failed)?; - files.canonicalize().map_err(failed) + dunce::canonicalize(files).map_err(failed) } fn read_text_file(&self, real: &Path, max: usize) -> Result { @@ -1941,6 +2722,13 @@ fn not_writable(path: &Path, e: ide_core::error::IdeError) -> HostError { pub struct HostWorkspace(pub std::sync::Weak); impl HostWorkspace { + fn project_for(&self, client: Option<&str>, root: &Path) -> Option> { + let host = self.0.upgrade()?; + client + .and_then(|w| host.project(w).ok()) + .filter(|p| p.root == root) + .or_else(|| host.windows_for_root(root).first().and_then(|w| host.project(w).ok())) + } fn projects(&self) -> Vec> { self.0.upgrade().map(|h| h.projects()).unwrap_or_default() } @@ -1952,6 +2740,183 @@ impl HostWorkspace { } impl PluginWorkspace for HostWorkspace { + fn database_host(&self) -> Option> { + self.0.upgrade() + } + fn run_configuration(&self, client: &str, plugin: &str, config: &str) -> Result { + let host = self.0.upgrade().ok_or(HostError::Cancelled)?; + let prefix = format!("{}{}:", ide_run::EXTERNAL_PREFIX, plugin); + let id = if config.starts_with(&prefix) { config.to_owned() } else { format!("{prefix}{config}") }; + let project = host.project(client).map_err(failed)?; + project.require_trust().map_err(failed)?; + let plugins = host.plugin_host().ok_or(HostError::Cancelled)?; + let detected = plugins + .run_detection() + .detect(&project.root) + .into_iter() + .find(|c| c.id == id) + .ok_or_else(|| HostError::NotFound("no owned run configuration with this id".into()))?; + let mut config = ide_run::load_user_configs(&project.root) + .map_err(failed)? + .into_iter() + .find(|c| c.id == id) + .unwrap_or(detected); + ide_sdk::apply::apply_to_run_configs( + std::slice::from_mut(&mut config), + &project.root, + &project.settings.effective(), + ); + if config.debug_adapter.is_none() { + let cwd = config.cwd.as_ref().map(|p| p.as_ref()).unwrap_or(&project.root); + config.debug_adapter = + config.args.iter().map(|arg| cwd.join(arg)).filter(|path| path.is_file()).find_map(|path| { + let language = project.languages.detect(&path, None); + project.languages.get(&language)?.debugger.map(|d| d.adapter) + }); + } + Ok(config) + } + + fn buffer_text_in(&self, client: Option<&str>, path: &Path) -> Option { + client + .and_then(|w| self.0.upgrade()?.project(w).ok()?.documents.text_of(path)) + .or_else(|| self.buffer_text(path)) + } + fn document_in(&self, client: &str, path: &Path) -> Option<(u64, String)> { + let project = self.0.upgrade()?.project(client).ok()?; + let doc = project.documents.by_path(path)?; + let snapshot = project.documents.snapshot(doc).ok()?; + Some((u64::from(snapshot.version), snapshot.text)) + } + fn open_editors(&self, client: &str) -> Result, HostError> { + let host = self.0.upgrade().ok_or(HostError::Cancelled)?; + let project = host.project(client).map_err(failed)?; + let active = host.mcp_reports.focused(client).map(|e| e.path.into_path_buf()); + Ok(host + .mcp_reports + .editors(&[client.into()]) + .into_iter() + .map(|path| { + let language = project + .languages + .detect(&path, project.documents.text_of(&path).as_deref().and_then(|t| t.lines().next())); + ide_plugin::OpenEditor { + active: active.as_ref() == Some(&path), + dirty: project + .documents + .by_path(&path) + .is_some_and(|doc| project.documents.is_dirty(doc).unwrap_or(false)), + language: language.to_string(), + path: to_ipc(&path), + } + }) + .collect()) + } + fn search_text( + &self, + client: Option<&str>, + root: &Path, + query: ide_plugin::TextQuery, + limit: u32, + cancel: &CancellationToken, + ) -> Result { + let host = self.0.upgrade().ok_or(HostError::Cancelled)?; + let project = self.project_for(client, root).ok_or_else(|| HostError::NotFound("project closed".into()))?; + if limit == 0 { + return Ok(ide_plugin::TextSearch::default()); + } + let mut query = ide_search::SearchQuery { + pattern: query.pattern, + is_regex: query.regex, + case_sensitive: query.case_sensitive, + whole_word: query.whole_word, + include: query.include, + exclude: query.exclude, + scope: ide_search::SearchScope::Project, + max_matches: limit.min(10_000).saturating_add(1), + context: Default::default(), + }; + query.exclude.extend(project.search_excludes()); + let files = Mutex::new(Vec::new()); + let summary = crate::ops::search::search_project(&project, &host.syntax(), &query, cancel, &|file| { + files.lock().push(file) + }) + .map_err(failed)?; + if summary.cancelled || cancel.is_cancelled() { + return Err(HostError::Cancelled); + } + let mut out = ide_plugin::TextSearch { matches: Vec::new(), limit_hit: summary.limit_hit }; + let mut bytes = 0usize; + for file in files.into_inner() { + if cancel.is_cancelled() { + return Err(HostError::Cancelled); + } + // Restrict project.read to the project root, including symlink targets. + let Ok(path) = ide_plugin::resolve_in_root(root, &to_ipc(&file.path.0)) else { + continue; + }; + let text = project.documents.text_of(&path).or_else(|| { + let loaded = project.documents.load_file_bounded(&path, ide_plugin::MAX_READ_BYTES as usize).ok()?; + (!loaded.binary && !loaded.too_large) + .then(|| ide_text::normalize_line_endings(&loaded.text).into_owned()) + }); + let Some(text) = text else { + out.limit_hit = true; + continue; + }; + let doc = ide_text::Document::new(&text); + for (start, end) in ide_search::match_ranges(&text, &query).map_err(failed)? { + if out.matches.len() >= limit as usize { + out.limit_hit = true; + return Ok(out); + } + let range = ide_core::Range::new(doc.byte_to_pos(start), doc.byte_to_pos(end)); + let line = text[..start].rfind('\n').map_or(0, |i| i + 1); + let end_line = text[start..].find('\n').map_or(text.len(), |i| start + i); + let preview = text[line..end_line].trim_end_matches('\r'); + let mut cut = preview.len().min(4096); + while !preview.is_char_boundary(cut) { + cut -= 1; + } + let preview = &preview[..cut]; + bytes += preview.len() + path.as_os_str().len() + 64; + if bytes > 4 << 20 { + out.limit_hit = true; + return Ok(out); + } + out.matches.push(ide_plugin::TextMatch { + location: ide_plugin::Location { path: path.clone(), range }, + line: preview.into(), + }); + } + } + Ok(out) + } + fn problems(&self, client: &str, path: Option<&Path>) -> Result, HostError> { + let host = self.0.upgrade().ok_or(HostError::Cancelled)?; + let root = host.project(client).map_err(failed)?.root.clone(); + let windows = host.windows_for_root(&root); + let mut out = Vec::new(); + for file in host.mcp_reports.problems(&windows) { + if path.is_some_and(|p| p != file.path.0) { + continue; + } + if ide_plugin::resolve_new_in_root(&root, &to_ipc(&file.path.0)).is_err() { + continue; + } + let mut sources: BTreeMap> = BTreeMap::new(); + for d in file.items { + sources.entry(d.server.clone().or_else(|| d.source.clone()).unwrap_or_default()).or_default().push(d); + } + out.extend(sources.into_iter().map(|(source, diagnostics)| ide_plugin::FileProblems { + path: to_ipc(&file.path.0), + source, + diagnostics, + })); + } + Ok(out) + } + fn projects(&self) -> Vec { let mut roots: Vec = HostWorkspace::projects(self).iter().map(|p| p.root.clone()).collect(); roots.sort(); @@ -2104,6 +3069,22 @@ mod tests { PluginApi { pkg: Package::new(manifest, dir.to_path_buf()), shared } } + #[cfg(windows)] + #[test] + fn private_directory_matches_resolved_write_paths() { + let tmp = tempfile::tempdir().unwrap(); + let api = api(tmp.path(), Arc::default()); + let dir = api.plugin_dir().unwrap(); + let verbatim = std::fs::canonicalize(&dir).unwrap(); + for root in [&dir, &verbatim] { + let path = ide_plugin::resolve_new_in_root(root, "state.txt").unwrap(); + assert!(path.starts_with(&dir), "private writes must use the private-directory branch"); + api.write_text_file(&path, "text").unwrap(); + api.write_bytes_file(&path, b"bytes").unwrap(); + assert_eq!(std::fs::read(&path).unwrap(), b"bytes"); + } + } + #[test] fn storage_moves_an_older_storage_json_to_one_file_per_key() { let tmp = tempfile::tempdir().unwrap(); @@ -2180,7 +3161,7 @@ mod tests { fn repository_and_search_paths_match_ipc_events() { let tmp = tempfile::tempdir().unwrap(); assert!(std::process::Command::new("git").args(["init", "-q"]).arg(tmp.path()).status().unwrap().success()); - let root = tmp.path().canonicalize().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); std::fs::create_dir(root.join("src")).unwrap(); let file = root.join("src/Example.java"); std::fs::write(&file, "class Example {}\n").unwrap(); @@ -2223,7 +3204,7 @@ mod tests { #[test] fn find_files_uses_the_project_index_when_there_is_one() { let tmp = tempfile::tempdir().unwrap(); - let root = tmp.path().canonicalize().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); let api = api(&root, Arc::default()); *api.shared.workspace.write() = Some(Arc::new(Indexed( root.clone(), @@ -2338,7 +3319,12 @@ mod tests { #[cfg(unix)] fn shell(script: &str) -> ProcessCommand { - ProcessCommand { program: "sh".into(), args: vec!["-c".into(), script.into()], env: Vec::new() } + ProcessCommand { + program: "sh".into(), + args: vec!["-c".into(), script.into()], + env: Vec::new(), + approval_root: None, + } } #[cfg(unix)] @@ -2451,6 +3437,58 @@ mod tests { matches!(api.approved_program("lint"), Err(HostError::Denied(e)) if e.contains("changed since it was approved")) ); } + #[test] + fn redirects_check_grants_before_connecting_and_downgrades_require_http() { + assert_eq!( + redirect_capability(&"https://localhost/start".parse().unwrap(), &"http://localhost/end".parse().unwrap()) + .unwrap(), + Capability::NetHttp("localhost".into()) + ); + let source = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let target = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + target.set_nonblocking(true).unwrap(); + let source_port = source.local_addr().unwrap().port(); + let target_port = target.local_addr().unwrap().port(); + let worker = std::thread::spawn(move || { + use std::io::{Read, Write}; + let (mut stream, _) = source.accept().unwrap(); + let mut buf = [0; 4096]; + let _ = stream.read(&mut buf); + write!(stream, "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{target_port}/\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").unwrap(); + }); + let dir = tempfile::tempdir().unwrap(); + let api = api(dir.path(), Arc::default()); + let result = api.fetch( + HttpRequest { + method: "GET".into(), + url: format!("http://localhost:{source_port}/"), + headers: Vec::new(), + body: None, + timeout: Duration::from_secs(2), + grants: vec![Capability::Net("localhost".into())], + }, + &CancellationToken::new(), + ); + assert!(matches!(result, Err(HostError::Denied(cap)) if cap == "net:127.0.0.1")); + assert!(target.accept().is_err(), "ungranted target must never receive a connection"); + worker.join().unwrap(); + } + + #[test] + fn output_ring_keeps_utf8_and_is_withdrawn_on_stop() { + let dir = tempfile::tempdir().unwrap(); + let api = api(dir.path(), Arc::default()); + api.output_write("build", Some(&"😀".repeat(300_000))).unwrap(); + let buffer = api.shared.output.lock().get(&(api.id().into(), "build".into())).cloned().unwrap(); + assert_eq!(buffer.len(), 1 << 20); + api.output_write("build", Some("x")).unwrap(); + let buffer = api.shared.output.lock().get(&(api.id().into(), "build".into())).cloned().unwrap(); + assert!(buffer.len() <= 1 << 20 && buffer.ends_with('x')); + api.output_write("build", None).unwrap(); + assert!(api.shared.output.lock().get(&(api.id().into(), "build".into())).unwrap().is_empty()); + api.shared.clear_plugin(api.id(), true); + assert!(api.shared.output.lock().is_empty()); + } } /// The window a user action invoked the plugin from: browser tabs and clipboard writes @@ -2491,3 +3529,309 @@ mod credential_tests { assert_eq!(from_window(Some("main"), "open a URL").unwrap(), "main"); } } + +fn valid_range(document: &ide_text::Document, range: ide_core::Range) -> bool { + range.start <= range.end + && [range.start, range.end].into_iter().all(|pos| document.byte_to_pos(document.pos_to_byte(pos)) == pos) +} + +impl PluginApi { + fn read_repo(&self, root: &Path) -> Result, HostError> { + if !self.shared.workspace().is_some_and(|w| w.trusted(root)) { + return Err(HostError::Failed("the project is in Restricted Mode: git does not run there".into())); + } + let repo = ide_vcs::Repo::discover(root).map_err(failed)?; + if let Some(repo) = &repo { + repo.set_execution_allowed(false); + } + Ok(repo) + } + fn set_decorations( + &self, + path: Option<&Path>, + set: &str, + mut items: Vec, + file: Option, + tree: bool, + ) -> Result<(), HostError> { + if set.is_empty() || set.len() > 128 { + return Err(HostError::Invalid("set id must be 1-128 bytes".into())); + } + if items.len() > 10_000 || file.as_ref().and_then(|f| f.badge.as_ref()).is_some_and(|b| b.chars().count() > 2) { + return Err(HostError::Invalid("decoration limit exceeded".into())); + } + items.retain(|d| decoration_style(&d.style)); + if items.iter().any(|d| d.range.start > d.range.end) { + return Err(HostError::Invalid("reversed decoration range".into())); + } + let replacement = if let Some(path) = path { + let workspace = self.shared.workspace().ok_or_else(|| HostError::NotFound("no project open".into()))?; + let (root, real) = workspace + .projects() + .into_iter() + .filter_map(|root| ide_plugin::resolve_new_in_root(&root, &to_ipc(path)).ok().map(|real| (root, real))) + .max_by_key(|(root, _)| root.components().count()) + .ok_or_else(|| HostError::Invalid("path outside open projects".into()))?; + Some(super::DecorationSet { + root: root.into(), + path: real.into(), + set_id: set.into(), + tree, + items, + file: file.filter(|f| decoration_style(&f.style)), + }) + } else { + None + }; + let mut all = self.shared.decorations.lock(); + let mut sets = all.get(self.id()).cloned().unwrap_or_default(); + sets.retain(|s| !(s.tree == tree && s.set_id == set && replacement.as_ref().is_none_or(|r| r.path == s.path))); + if let Some(replacement) = replacement { + sets.push(replacement); + } + let names: BTreeSet<_> = sets.iter().filter(|s| s.tree == tree).map(|s| &s.set_id).collect(); + if names.len() > 50 || serde_json::to_vec(&sets).map_err(failed)?.len() > 4 << 20 { + return Err(HostError::Invalid("decoration sets exceed plugin limits".into())); + } + // A plugin may spread one file's annotations across sets, but the combined limit is fixed. + let mut counts = BTreeMap::new(); + for s in &sets { + *counts.entry(&s.path).or_insert(0usize) += s.items.len(); + } + if counts.values().any(|n| *n > 10_000) { + return Err(HostError::Invalid("at most 10000 decorations per file".into())); + } + all.insert(self.id().into(), sets.clone()); + // Emit while holding the state lock so replacement snapshots cannot arrive out of order. + self.shared.sink.emit(None, PluginEvent::DecorationsChanged { plugin_id: self.id().into(), sets }); + Ok(()) + } +} +fn decoration_style(style: &str) -> bool { + matches!( + style, + "highlight" + | "search" + | "added" + | "modified" + | "removed" + | "warning" + | "error" + | "info" + | "underline" + | "strikethrough" + | "muted" + ) +} +impl Shared { + pub(super) fn decoration_sets(&self) -> BTreeMap> { + self.decorations.lock().clone() + } +} + +#[cfg(test)] +mod workbench_tests { + use super::*; + struct Workspace { + root: PathBuf, + trusted: bool, + text: Option, + } + impl PluginWorkspace for Workspace { + fn projects(&self) -> Vec { + vec![self.root.clone()] + } + fn buffer_text(&self, _: &Path) -> Option { + self.text.clone() + } + fn document(&self, _: &Path) -> Option<(u64, String)> { + self.text.clone().map(|s| (1, s)) + } + fn apply_edits(&self, _: &Path, _: u64, _: &[TextEdit]) -> Result { + Err(HostError::Stale) + } + fn trusted(&self, _: &Path) -> bool { + self.trusted + } + fn project_of(&self, _: &str) -> Option { + Some(self.root.clone()) + } + fn publish_diagnostics(&self, _: &str, _: &Path, _: Vec) -> Result<(), HostError> { + Ok(()) + } + fn open_editors(&self, client: &str) -> Result, HostError> { + Ok(if client == "main" { + vec![ide_plugin::OpenEditor { + path: to_ipc(&self.root.join("a.txt")), + language: "text".into(), + active: true, + dirty: true, + }] + } else { + Vec::new() + }) + } + } + fn api(root: &Path, trusted: bool, text: Option<&str>) -> PluginApi { + let manifest = ide_extension::Manifest::parse( + "id = \"test\"\nname = \"Test\"\nversion = \"1.0.0\"\nengines.openidea = \"^1.4\"\n", + ) + .unwrap(); + let shared = Shared::new( + Arc::new(|_: Option<&str>, _: PluginEvent| {}), + root.join(".data"), + Arc::default(), + ide_extension::UiStore::new(), + ); + *shared.workspace.write() = + Some(Arc::new(Workspace { root: root.into(), trusted, text: text.map(str::to_owned) })); + PluginApi { pkg: Package::new(manifest, root.into()), shared } + } + #[test] + fn command_exemption_requires_a_declared_unambiguous_owner() { + let dir = tempfile::tempdir().unwrap(); + let api = api(dir.path(), true, None); + let own = api.shared.ui.insert(ide_extension::UiContribution { + plugin_id: "test".into(), + point: super::super::COMMANDS.into(), + key: "other.run".into(), + decl: serde_json::json!({}), + }); + assert!(api.owns_command("plugin.test.other.run")); + assert!(!api.owns_command("plugin.test.unknown")); + let other = api.shared.ui.insert(ide_extension::UiContribution { + plugin_id: "test.other".into(), + point: super::super::COMMANDS.into(), + key: "run".into(), + decl: serde_json::json!({}), + }); + assert!(!api.owns_command("plugin.test.other.run")); + drop(other); + assert!(api.owns_command("plugin.test.other.run")); + drop(own); + } + #[test] + fn selections_validate_window_and_utf16_without_requiring_a_grant() { + let dir = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(dir.path()).unwrap(); + let path = root.join("a.txt"); + let api = api(&root, true, Some("😀abc")); + let range = |col| ide_core::Range::point(ide_core::Pos::new(0, col)); + assert!(api.editor_ranges("main", &path, vec![range(2)], false).is_ok()); + assert!(api.editor_ranges("main", &path, vec![range(1)], true).is_err()); + assert!(api.editor_ranges("other", &path, vec![range(2)], false).is_err()); + } + #[test] + fn decoration_sets_validate_paths_limits_and_cleanup() { + let dir = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(dir.path()).unwrap(); + let path = root.join("a.txt"); + std::fs::write(&path, "text").unwrap(); + let api = api(&root, true, None); + let item = |style: &str| ide_plugin::Decoration { + range: ide_core::Range::default(), + style: style.into(), + hover: None, + after: Some("after".into()), + }; + api.decorations(Some(&path), "set", vec![item("highlight"), item("unknown")]).unwrap(); + assert_eq!(api.shared.decoration_sets()["test"][0].items.len(), 1); + assert!(api.decorations(Some(&root.join("../outside.txt")), "set", vec![]).is_err()); + api.decorations(None, "set", vec![]).unwrap(); + for i in 0..50 { + api.decorations(Some(&path), &format!("s{i}"), vec![]).unwrap(); + } + assert!(api.decorations(Some(&path), "overflow", vec![]).is_err()); + api.decorations(Some(&path), "s0", vec![item("info"); 10_000]).unwrap(); + assert!(api.decorations(Some(&path), "s1", vec![item("info")]).is_err()); + assert!( + api.file_decorations( + Some(&path), + "tree", + Some(ide_plugin::FileDecoration { badge: Some("abc".into()), style: "info".into(), tooltip: None }) + ) + .is_err() + ); + api.shared.clear_plugin("test", true); + assert!(api.shared.decoration_sets().is_empty()); + } + fn git(root: &Path, args: &[&str]) { + let out = StdCommand::new("git").current_dir(root).args(args).output().unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + } + #[test] + fn vcs_reads_reuse_history_and_dirty_blame_and_require_trust() { + let dir = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(dir.path()).unwrap(); + git(&root, &["init", "-q"]); + git(&root, &["config", "user.name", "Test"]); + git(&root, &["config", "user.email", "test@example.com"]); + let path = root.join("a.txt"); + std::fs::write(&path, "committed\nsecond\n").unwrap(); + git(&root, &["add", "."]); + git(&root, &["commit", "-qm", "Subject\n\nBody"]); + std::fs::write(&path, "disk\nsecond\n").unwrap(); + let api = api(&root, true, Some("unsaved\nsecond\nnew\n")); + let cancel = CancellationToken::new(); + let history = api.vcs_log(&root, Some("a.txt"), 10, &cancel).unwrap(); + assert_eq!(history.len(), 1); + assert_eq!(history[0].subject, "Subject"); + assert_eq!(history[0].body.trim(), "Body"); + assert!(history[0].date > 0); + let lines = api.vcs_blame(None, &root, "a.txt").unwrap(); + assert_eq!(lines.len(), 3); + assert!(lines[0].commit.bytes().all(|b| b == b'0')); + assert_eq!(lines[1].commit, history[0].id); + assert_eq!(lines[2].line, 2); + let patch = api.vcs_diff(&root, Some("a.txt"), None, &cancel).unwrap().unwrap(); + assert!(patch.contains("+disk")); + assert!(!patch.contains("+unsaved")); + assert!(api.vcs_diff(&root, None, Some("--output=outside"), &cancel).is_err()); + assert!(api.vcs_log(&root, Some("../outside"), 1, &cancel).is_err()); + std::fs::remove_file(&path).unwrap(); + assert_eq!(api.vcs_log(&root, Some("a.txt"), 10, &cancel).unwrap().len(), 1); + assert!(api.vcs_diff(&root, Some("a.txt"), None, &cancel).unwrap().unwrap().contains("-committed")); + let restricted = super::workbench_tests::api(&root, false, None); + assert!(restricted.vcs_log(&root, None, 1, &cancel).is_err()); + assert!(restricted.vcs_blame(None, &root, "a.txt").is_err()); + assert!(restricted.vcs_diff(&root, None, None, &cancel).is_err()); + std::fs::write(&path, "x".repeat(5 << 20)).unwrap(); + assert!(api.vcs_diff(&root, None, None, &cancel).is_err()); + } + #[cfg(unix)] + #[test] + fn vcs_reads_never_run_repository_configured_helpers() { + let dir = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(dir.path()).unwrap(); + git(&root, &["init", "-q"]); + git(&root, &["config", "user.name", "Test"]); + git(&root, &["config", "user.email", "test@example.com"]); + std::fs::write(root.join("a.txt"), "base\n").unwrap(); + git(&root, &["add", "."]); + git(&root, &["commit", "-qm", "base"]); + let marker = root.join("HELPER-RAN"); + let helper = format!("touch '{}'", marker.display()); + for key in [ + "core.fsmonitor", + "diff.external", + "diff.test.command", + "diff.test.textconv", + "filter.test.clean", + "filter.test.process", + ] { + git(&root, &["config", key, &helper]); + } + git(&root, &["config", "filter.test.required", "true"]); + assert!(!marker.exists(), "setup ran helper"); + std::fs::write(root.join(".gitattributes"), "*.txt diff=test filter=test\n").unwrap(); + std::fs::write(root.join("a.txt"), "changed\n").unwrap(); + let api = api(&root, true, Some("unsaved\n")); + let cancel = CancellationToken::new(); + api.vcs_log(&root, None, 10, &cancel).unwrap(); + assert!(!marker.exists(), "log ran helper"); + api.vcs_blame(None, &root, "a.txt").unwrap(); + assert!(!marker.exists(), "blame ran helper"); + api.vcs_diff(&root, None, None, &cancel).unwrap(); + assert!(!marker.exists()); + } +} diff --git a/crates/ide-host/src/plugins/auth.rs b/crates/ide-host/src/plugins/auth.rs new file mode 100644 index 000000000..e0a955ba4 --- /dev/null +++ b/crates/ide-host/src/plugins/auth.rs @@ -0,0 +1,205 @@ +//! One-shot OAuth redirects, always bound on the desktop's IPv4 loopback. +use ide_core::CancellationToken; +use ide_plugin::HostError; +use parking_lot::Mutex; +use std::collections::HashMap; +use std::io::{Read, Write}; +use std::net::{Ipv4Addr, TcpListener, TcpStream}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{Duration, Instant}; + +pub(super) trait Service: Send + Sync { + fn begin(&self, plugin: &str, window: &str, path: &str) -> Result<(u64, String), HostError>; + fn wait( + &self, + plugin: &str, + id: u64, + timeout: Duration, + cancel: &CancellationToken, + ) -> Result, HostError>; + fn close(&self, plugin: &str, id: u64); + fn clear(&self, plugin: &str); + fn forget_window(&self, window: &str); +} +struct Entry { + window: String, + path: String, + listener: Mutex>, + cancel: CancellationToken, +} +#[derive(Default)] +pub(super) struct Loopbacks { + next: AtomicU64, + entries: Mutex>>, +} +fn failed(e: impl std::fmt::Display) -> HostError { + HostError::Failed(e.to_string()) +} +fn respond(stream: &mut TcpStream, ok: bool) { + let body = if ok { "You can close this tab." } else { "Not found." }; + let status = if ok { "200 OK" } else { "404 Not Found" }; + let _ = write!( + stream, + "HTTP/1.1 {status}\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n{body}", + body.len() + ); +} +impl Loopbacks { + fn remove_where(&self, predicate: impl Fn(&str, &Entry) -> bool) { + self.entries.lock().retain(|(p, _), entry| { + if !predicate(p, entry) { + return true; + } + entry.cancel.cancel(); + entry.listener.lock().take(); + false + }); + } +} +impl Service for Loopbacks { + fn begin(&self, plugin: &str, window: &str, path: &str) -> Result<(u64, String), HostError> { + let path = path.trim_start_matches('/'); + if path.len() > 1024 + || path.bytes().any(|b| !(b.is_ascii_alphanumeric() || b"/-._~".contains(&b))) + || path.split('/').any(|s| matches!(s, "." | "..")) + { + return Err(HostError::Invalid("loopback path must be a URL path without query or fragment".into())); + } + let mut entries = self.entries.lock(); + if entries.keys().filter(|(p, _)| p == plugin).count() >= 2 { + return Err(HostError::Invalid("at most two open loopbacks per plugin".into())); + } + let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).map_err(failed)?; + listener.set_nonblocking(true).map_err(failed)?; + let uri = format!("http://127.0.0.1:{}/{path}", listener.local_addr().map_err(failed)?.port()); + let id = self.next.fetch_add(1, Ordering::Relaxed); + entries.insert( + (plugin.into(), id), + Arc::new(Entry { + window: window.into(), + path: format!("/{path}"), + listener: Mutex::new(Some(listener)), + cancel: CancellationToken::new(), + }), + ); + Ok((id, uri)) + } + fn wait( + &self, + plugin: &str, + id: u64, + timeout: Duration, + cancel: &CancellationToken, + ) -> Result, HostError> { + let entry = self + .entries + .lock() + .get(&(plugin.into(), id)) + .cloned() + .ok_or_else(|| HostError::NotFound("loopback".into()))?; + let listener = + entry.listener.lock().take().ok_or_else(|| HostError::Invalid("loopback already awaited".into()))?; + let deadline = Instant::now() + timeout; + let result = (|| loop { + if cancel.is_cancelled() || entry.cancel.is_cancelled() { + return Err(HostError::Cancelled); + } + match listener.accept() { + Ok((mut stream, address)) => { + if !address.ip().is_loopback() { + continue; + } + stream.set_nonblocking(true).map_err(failed)?; + let mut head = Vec::new(); + loop { + if cancel.is_cancelled() || entry.cancel.is_cancelled() { + return Err(HostError::Cancelled); + } + if Instant::now() >= deadline { + return Ok(None); + } + let mut buf = [0; 1024]; + match stream.read(&mut buf) { + Ok(0) => break, + Ok(n) => { + head.extend_from_slice(&buf[..n]); + if head.windows(4).any(|w| w == b"\r\n\r\n") || head.len() > 8192 { + break; + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + std::thread::sleep(Duration::from_millis(5)) + } + Err(e) => return Err(failed(e)), + } + } + let line = std::str::from_utf8(&head).unwrap_or_default().lines().next().unwrap_or_default(); + let target = line.strip_prefix("GET ").and_then(|s| s.split_once(' ')).map(|(target, _)| target); + let query = target.and_then(|target| { + let (path, query) = target.split_once('?').unwrap_or((target, "")); + (path == entry.path && head.len() <= 8192).then(|| query.to_owned()) + }); + stream.set_nonblocking(false).map_err(failed)?; + stream.set_write_timeout(Some(Duration::from_millis(100))).map_err(failed)?; + respond(&mut stream, query.is_some()); + if query.is_some() { + return Ok(query); + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {} + Err(e) => return Err(failed(e)), + } + if Instant::now() >= deadline { + return Ok(None); + } + std::thread::sleep(Duration::from_millis(5)); + })(); + self.close(plugin, id); + result + } + fn close(&self, plugin: &str, id: u64) { + let entry = self.entries.lock().remove(&(plugin.into(), id)); + if let Some(entry) = entry { + entry.cancel.cancel(); + entry.listener.lock().take(); + } + } + fn clear(&self, plugin: &str) { + self.remove_where(|p, _| p == plugin || p.starts_with("remote:") && p.ends_with(&format!(":{plugin}"))); + } + fn forget_window(&self, window: &str) { + self.remove_where(|_, entry| entry.window == window); + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn one_shot_raw_query_limits_timeout_and_cleanup() { + let service = Arc::new(Loopbacks::default()); + let (id, uri) = service.begin("a", "main", "oauth/callback").unwrap(); + assert!(uri.starts_with("http://127.0.0.1:")); + let (other, _) = service.begin("a", "main", "").unwrap(); + assert!(service.begin("a", "main", "third").is_err()); + assert_eq!(service.wait("a", other, Duration::ZERO, &CancellationToken::new()).unwrap(), None); + let host = uri.strip_prefix("http://").unwrap().split('/').next().unwrap().to_owned(); + let worker = std::thread::spawn(move || { + let mut stream = TcpStream::connect(host).unwrap(); + stream.write_all(b"GET /oauth/callback?code=a%2Bb&state=x HTTP/1.1\r\nHost: localhost\r\n\r\n").unwrap(); + let mut answer = String::new(); + stream.read_to_string(&mut answer).unwrap(); + assert!(answer.contains("You can close this tab")); + }); + assert_eq!( + service.wait("a", id, Duration::from_secs(2), &CancellationToken::new()).unwrap(), + Some("code=a%2Bb&state=x".into()) + ); + worker.join().unwrap(); + assert!(service.wait("a", id, Duration::ZERO, &CancellationToken::new()).is_err()); + let (id, _) = service.begin("a", "main", "callback").unwrap(); + service.forget_window("main"); + assert!(service.wait("a", id, Duration::ZERO, &CancellationToken::new()).is_err()); + } +} diff --git a/crates/ide-host/src/plugins/database.rs b/crates/ide-host/src/plugins/database.rs new file mode 100644 index 000000000..5fb19ee11 --- /dev/null +++ b/crates/ide-host/src/plugins/database.rs @@ -0,0 +1,225 @@ +//! Database ownership stays on the project host; credentials never pass back to guests. +use super::api::PluginApi; +use super::{PluginHost, WindowRequest}; +use ide_core::{CancellationToken, IdeError, IdeEvent, IdeResult}; +use ide_db::{DataSource, DbDriver, DbProperty}; +use ide_plugin::{HostError, PluginDataSource}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; + +#[derive(serde::Serialize, serde::Deserialize)] +struct OwnedProject { + root: PathBuf, + sources: std::collections::BTreeSet, +} + +pub(super) fn settings(source: PluginDataSource) -> Result { + let bad = || HostError::Invalid("invalid non-secret database source settings".into()); + if source.id.is_empty() + || source.id.len() > 128 + || source.name.trim().is_empty() + || source.name.len() > 256 + || source.id.chars().chain(source.name.chars()).any(char::is_control) + || source.options.len() > 128 + || [&source.host, &source.database, &source.user, &source.url] + .into_iter() + .flatten() + .any(|v| v.len() > 16_384 || v.contains('\0')) + { + return Err(bad()); + } + let driver = match source.driver.as_str() { + "sqlite" => DbDriver::Sqlite, + "postgres" => DbDriver::Postgres, + "mysql" => DbDriver::Mysql, + "sqlServer" => DbDriver::SqlServer, + "clickhouse" => DbDriver::Clickhouse, + "oracle" => DbDriver::Oracle, + _ => return Err(bad()), + }; + let secret = |key: &str| { + let key = key.to_ascii_lowercase(); + ["password", "passwd", "pwd", "token", "secret", "credential"].iter().any(|s| key.contains(s)) + }; + if source.options.iter().any(|(k, v)| { + k.is_empty() || k.len() > 256 || v.len() > 16_384 || k.contains('\0') || v.contains('\0') || secret(k) + }) { + return Err(bad()); + } + let mut out = DataSource { + name: source.name, + driver, + host: source.host.unwrap_or_else(|| "localhost".into()), + port: source.port, + database: source.database.unwrap_or_default(), + user: source.user.unwrap_or_default(), + save_password: true, + properties: source.options.into_iter().map(|(name, value)| DbProperty { name, value }).collect(), + ..Default::default() + }; + if driver == DbDriver::Sqlite { + out.file = Some(out.database.clone().into()); + } + if let Some(url) = source.url.filter(|u| !u.is_empty()) { + if url.contains(';') { + return Err(bad()); + } + let parsed = ide_db::source::parse_url(&url).map_err(|_| bad())?; + if parsed.password.is_some() || parsed.params.iter().any(|(k, _)| secret(k)) { + return Err(bad()); + } + // Case-insensitive URL credential keys must not bypass the parser's lowercase password key. + if let Some((_, query)) = url.split_once('?') { + for part in query.split('&') { + let key = part.split('=').next().unwrap_or_default(); + // Parsing a synthetic URL decodes percent-encoded parameter names as well. + let probe = ide_db::source::parse_url(&format!("postgres://host/db?{key}=x")).map_err(|_| bad())?; + if probe.password.is_some() || probe.params.iter().any(|(k, _)| secret(k)) { + return Err(bad()); + } + } + } + out.url_only = true; + out.url = url; + out.resolved().map_err(|_| bad())?; + } + Ok(out) +} + +impl PluginApi { + pub(super) fn database_host(&self, root: &Path) -> Result<(Arc, Arc), HostError> { + let workspace = self.shared.workspace().ok_or(HostError::Cancelled)?; + if !workspace.projects().iter().any(|r| r == root) { + return Err(HostError::NotFound("project is not open".into())); + } + if !workspace.trusted(root) { + return Err(HostError::Denied("project is not trusted".into())); + } + ide_plugin::resolve_new_in_root(root, ".openidea/data-sources.toml").map_err(HostError::Invalid)?; + let host = workspace + .database_host() + .ok_or_else(|| HostError::Invalid("Database subsystem is unavailable on this host".into()))?; + let db = host.projects().into_iter().find(|p| p.root == root).ok_or(HostError::Cancelled)?.db.clone(); + Ok((host, db)) + } + pub(super) fn remember_database_project(&self, root: &Path, id: &str) -> Result<(), HostError> { + let file = self.shared.data_dir.join(".database-projects").join(format!("{}.json", self.pkg.id())); + let mut projects: Vec = match std::fs::read(&file) { + Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| HostError::Failed(e.to_string()))?, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => vec![], + Err(e) => return Err(HostError::Failed(e.to_string())), + }; + if !projects.iter().any(|p| p.root == root) { + projects.push(OwnedProject { root: root.into(), sources: Default::default() }); + } + let project = projects.iter_mut().find(|p| p.root == root).unwrap(); + if project.sources.insert(id.into()) { + std::fs::create_dir_all(file.parent().unwrap()).map_err(|e| HostError::Failed(e.to_string()))?; + ide_vfs::write_atomic(&file, &serde_json::to_vec(&projects).unwrap()).map_err(error)?; + } + Ok(()) + } + pub(super) fn connect_database( + &self, + root: &Path, + client: &str, + id: &str, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + let (_, db) = self.database_host(root)?; + if self.shared.project_of(client).as_deref() != Some(root) { + return Err(HostError::Invalid("requires this project's invoking window".into())); + } + let source = db.plugin_source(self.pkg.id(), id).map_err(error)?; + self.window_request(client, WindowRequest::DatabaseConnect { id: source.id }, cancel, timeout).map(|_| ()) + } +} + +pub(super) fn changed(host: &crate::Host, root: &Path) { + for project in host.projects().into_iter().filter(|p| p.root == root) { + project.sink.emit(IdeEvent::DbSourcesChanged); + } +} +impl PluginHost { + pub(super) fn remove_database_sources(&self, plugin: &str) -> IdeResult<()> { + let file = self.shared.data_dir.join(".database-projects").join(format!("{plugin}.json")); + let projects: Vec = match std::fs::read(&file) { + Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| IdeError::invalid(e.to_string()))?, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(e.into()), + }; + let host = self + .shared + .workspace() + .and_then(|w| w.database_host()) + .ok_or_else(|| IdeError::unavailable("Database subsystem is unavailable on this host"))?; + for project in projects { + let root = project.root; + let removed = ide_db::plugin::remove_all(&root, plugin)?; + let store = ide_db::secrets::SecretStore::new(host.data_dir.join("db-passwords.json"), &root); + let ids: std::collections::BTreeSet<_> = project + .sources + .iter() + .map(|id| ide_db::plugin::source_id(plugin, id)) + .chain(removed.into_iter().map(|source| source.id)) + .collect(); + for id in ids { + store.delete(&id); + store.delete(&ide_db::secrets::ssh_key(&id)); + for project in host.projects().into_iter().filter(|p| p.root == root) { + host.runtime.block_on(project.db.forget_plugin_source(&id)); + } + } + changed(&host, &root); + } + std::fs::remove_file(file)?; + Ok(()) + } +} + +pub(super) fn error(e: IdeError) -> HostError { + use ide_core::error::ErrorKind; + match e.kind { + ErrorKind::NotFound => HostError::NotFound(e.message), + ErrorKind::InvalidInput => HostError::Invalid(e.message), + ErrorKind::PermissionDenied => HostError::Denied(e.message), + _ => HostError::Failed(e.message), + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn source() -> PluginDataSource { + PluginDataSource { id: "local".into(), name: "Local".into(), driver: "postgres".into(), ..Default::default() } + } + #[test] + fn rejects_secrets_without_echoing_them_and_documents_every_driver_id() { + for driver in ["sqlite", "postgres", "mysql", "sqlServer", "clickhouse", "oracle"] { + assert!(settings(PluginDataSource { driver: driver.into(), ..source() }).is_ok(), "{driver}"); + } + for url in [ + "postgres://user:top-secret@host/db", + "postgres://host/db?password=top-secret", + "postgres://host/db?PASSWORD=top-secret", + "postgres://host/db?to%6ben=top-secret", + ] { + let error = settings(PluginDataSource { url: Some(url.into()), ..source() }).unwrap_err(); + assert!(!error.to_string().contains("top-secret")); + } + for key in ["password", "accessToken", "client_secret", "PWD"] { + assert!( + settings(PluginDataSource { options: vec![(key.into(), "top-secret".into())], ..source() }).is_err() + ); + } + let source = settings(PluginDataSource { + url: Some("postgres://reader@host:5433/db?sslmode=require".into()), + options: vec![("application_name".into(), "Example".into())], + ..source() + }) + .unwrap(); + assert_eq!(source.resolved().unwrap().host, "host"); + } +} diff --git a/crates/ide-host/src/plugins/diagnostics.rs b/crates/ide-host/src/plugins/diagnostics.rs index 14470f56a..86e44ccd8 100644 --- a/crates/ide-host/src/plugins/diagnostics.rs +++ b/crates/ide-host/src/plugins/diagnostics.rs @@ -79,7 +79,9 @@ impl Diagnostics { doc.cancel.cancel(); doc.cancel = CancellationToken::new(); let Some(host) = editor_plugin_host() else { return }; - if host.providers.plugins_for(ProviderKind::Diagnostics, &doc.snapshot.language).is_empty() { + if host.providers.plugins_for(ProviderKind::Diagnostics, &doc.snapshot.language).is_empty() + && !host.providers.any(ProviderKind::Injections) + { return; } // Register only once per project incarnation; no project is retained by the host. @@ -113,6 +115,7 @@ impl Diagnostics { snapshot.text = current.text; drop(documents); + let injections = host.providers.injection_generations(&snapshot.language); let generations = host.providers.diagnostic_generations(&snapshot.language); let results = if snapshot.text.len() <= super::navigation::MAX_DOCUMENT_BYTES { host.provide_project_diagnostics(Some(&root), &snapshot, &cancel) @@ -124,6 +127,11 @@ impl Diagnostics { if state.closed || cancel.is_cancelled() { return; } + if injections != host.providers.injection_generations(&snapshot.language) + || injections.keys().any(|id| lifecycle_generations.get(id) != state.generations.get(id)) + { + return; + } let matching: HashSet<_> = host .diagnostic_plugins(Some(&root), &snapshot.language) .into_iter() @@ -224,6 +232,13 @@ pub(super) type ProjectState = (Weak>, SharedSink); impl PluginHost { pub(super) fn clear_provider_diagnostics(&self, plugin: &str) { // Clone before taking a project's state lock (edits register in the other order). + let injector = self.get(plugin).is_ok_and(|p| { + p.manifest().declarations("providers").iter().any(|d| { + d.get("kinds") + .and_then(|v| v.as_array()) + .is_some_and(|v| v.iter().any(|k| k.as_str() == Some("injections"))) + }) + }); let projects = self.diagnostic_projects.lock().clone(); for (state, sink) in projects { if let Some(state) = state.upgrade() { @@ -231,7 +246,15 @@ impl PluginHost { // A completed answer can still be waiting for another provider. *state.generations.entry(plugin.to_owned()).or_default() += 1; for doc in state.documents.values_mut() { - if doc.sources.remove(plugin) { + if injector { + for source in doc.sources.drain() { + sink.emit(IdeEvent::Diagnostics { + path: doc.snapshot.path.clone().into(), + source: format!("plugin-provider:{source}"), + items: vec![], + }); + } + } else if doc.sources.remove(plugin) { sink.emit(IdeEvent::Diagnostics { path: doc.snapshot.path.clone().into(), source: format!("plugin-provider:{plugin}"), diff --git a/crates/ide-host/src/plugins/endpoints.rs b/crates/ide-host/src/plugins/endpoints.rs index 204768328..358ad4918 100644 --- a/crates/ide-host/src/plugins/endpoints.rs +++ b/crates/ide-host/src/plugins/endpoints.rs @@ -281,7 +281,7 @@ mod tests { #[test] fn validates_entire_rooted_batch_with_utf16_positions() { let tmp = tempfile::tempdir().unwrap(); - let root = tmp.path().canonicalize().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); let file = root.join("routes.rb"); std::fs::write(&file, "😀x\n").unwrap(); let good = endpoint(file.clone()); diff --git a/crates/ide-host/src/plugins/execution.rs b/crates/ide-host/src/plugins/execution.rs new file mode 100644 index 000000000..95328befe --- /dev/null +++ b/crates/ide-host/src/plugins/execution.rs @@ -0,0 +1,534 @@ +//! Window-owned plugin operations. Instance reclamation does not end them. +use super::api::{PluginApi, PromptEntry, Waiter}; +use super::{CommandAnswer, EventQueue, PluginEvent, PluginHost, WindowRequest}; +use ide_core::{CancellationToken, IdeError, IdeResult}; +use ide_plugin::{ContentTab, ContentTabReply, HostError, WorkbenchEvent}; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::time::{Duration, Instant}; + +pub(super) struct Operation { + pub plugin: String, + pub client: String, + pub kind: OperationKind, + pub root: Option, + pub started: bool, + pub closed: bool, + pub run: Option, + pub terminal: Option<(std::sync::Weak, ide_core::TerminalId)>, +} + +pub(super) enum OperationKind { + Run { config: String, configuration: String, program: String }, + Terminal { key: String, command: ide_plugin::ProcessCommand }, +} + +pub(super) fn label(value: &str, max: usize) -> Result<(), HostError> { + if value.is_empty() || value.encode_utf16().count() > max || value.chars().any(char::is_control) { + return Err(HostError::Invalid(format!("expected 1-{max} characters without controls"))); + } + Ok(()) +} +fn body(value: &str) -> Result<(), HostError> { + if value.encode_utf16().count() > 1 << 20 || value.contains('\0') { + return Err(HostError::Invalid("content too large or contains NUL".into())); + } + Ok(()) +} +pub(super) fn validate_tab(tab: &ContentTab) -> Result<(), HostError> { + label(&tab.key, 128)?; + label(&tab.title, 256)?; + body(&tab.content)?; + if let Some(name) = &tab.file_name { + label(name, 256)?; + if name.contains(['/', '\\']) { + return Err(HostError::Invalid("file-name cannot contain separators".into())); + } + } + if let Some(language) = &tab.language { + label(language, 64)?; + } + if tab.actions.len() > 16 || tab.actions.iter().filter(|a| a.save).count() > 1 { + return Err(HostError::Invalid("at most 16 actions and one Save action".into())); + } + let mut ids = std::collections::BTreeSet::new(); + for action in &tab.actions { + label(&action.id, 64)?; + label(&action.label, 128)?; + if !ids.insert(&action.id) { + return Err(HostError::Invalid("duplicate action id".into())); + } + if let Some(icon) = &action.icon { + label(icon, 128)?; + } + } + Ok(()) +} +pub(super) fn validate_reply(reply: &ContentTabReply) -> Result<(), HostError> { + if let Some(content) = &reply.content { + body(content)?; + } + if reply.status.as_ref().is_some_and(|s| s.len() > 512) || reply.error.as_ref().is_some_and(|s| s.len() > 1024) { + return Err(HostError::Invalid("content status or error too long".into())); + } + if let Some(diff) = &reply.diff { + body(&diff.left)?; + body(&diff.right)?; + for value in [&diff.left_label, &diff.right_label].into_iter().flatten() { + label(value, 128)?; + } + if let Some(title) = &diff.title { + label(title, 256)?; + } + } + Ok(()) +} + +impl PluginApi { + pub(super) fn new_operation(&self, client: &str, kind: OperationKind) -> Result { + let closed = self.shared.closed_clients.lock(); + let blocked = self.shared.prompts_blocked.lock(); + if closed.contains(client) || blocked.contains(self.pkg.id()) { + return Err(HostError::Cancelled); + } + let mut operations = self.shared.operations.lock(); + if operations.values().filter(|o| o.plugin == self.pkg.id()).count() >= 32 { + return Err(HostError::Invalid("at most 32 active operations".into())); + } + let id = format!("plugin-op-{}", self.shared.next_prompt.fetch_add(1, Ordering::Relaxed)); + operations.insert( + id.clone(), + Operation { + plugin: self.pkg.id().into(), + client: client.into(), + kind, + root: self.shared.project_of(client), + started: false, + closed: false, + terminal: None, + run: None, + }, + ); + Ok(id) + } + pub(super) fn window_request( + &self, + client: &str, + request: WindowRequest, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result, HostError> { + let id = self.shared.next_prompt.fetch_add(1, Ordering::Relaxed); + let (tx, rx) = std::sync::mpsc::sync_channel(1); + { + let closed = self.shared.closed_clients.lock(); + let blocked = self.shared.prompts_blocked.lock(); + if closed.contains(client) || blocked.contains(self.pkg.id()) { + return Err(HostError::Cancelled); + } + self.shared.prompts.lock().insert( + id, + PromptEntry { plugin: self.pkg.id().into(), client: client.into(), answer: Waiter::Command(tx) }, + ); + } + self.shared.sink.emit( + Some(client), + PluginEvent::WindowRequest { plugin_id: self.pkg.id().into(), request_id: id.to_string(), request }, + ); + let deadline = Instant::now() + timeout; + loop { + match rx.recv_timeout(Duration::from_millis(20)) { + Ok(CommandAnswer::Value(value)) => return Ok(value), + Ok(CommandAnswer::NotFound) => return Err(HostError::NotFound("window service unavailable".into())), + Ok(CommandAnswer::Failed(error)) => return Err(HostError::Failed(error)), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) + if !cancel.is_cancelled() && Instant::now() < deadline => {} + Err(_) => { + self.shared.prompts.lock().remove(&id); + return Err(HostError::Cancelled); + } + } + } + } +} + +impl PluginHost { + /// The launcher alone may finish its operation; duplicate and foreign reports are ignored. + pub fn finish_operation(&self, client: &str, id: &str, status: Option, stopped: bool) -> bool { + let operation = { + let mut operations = self.shared.operations.lock(); + if !operations.get(id).is_some_and(|op| op.client == client) { + return false; + } + operations.remove(id).unwrap() + }; + let event = match operation.kind { + OperationKind::Run { config, .. } => { + WorkbenchEvent::RunExited(ide_plugin::RunExited { id: id.into(), config, status, killed: stopped }) + } + OperationKind::Terminal { key, .. } => WorkbenchEvent::TerminalExited(ide_plugin::TerminalExited { + id: id.into(), + key, + status, + closed: stopped, + }), + }; + self.post_owned_event(&operation.plugin, client, event); + true + } + /// Content actions bypass subscriptions, just like completions of owned operations. + pub fn content_tab_action( + &self, + client: &str, + plugin: &str, + action: ide_plugin::ContentTabAction, + ) -> IdeResult<()> { + let tabs = self.shared.content_tabs.lock(); + let tab = tabs + .get(&(plugin.into(), client.into(), action.key.clone())) + .ok_or_else(|| IdeError::not_found("unknown content tab"))?; + if !tab.actions.iter().any(|a| a.id == action.action) { + return Err(IdeError::invalid("unknown content action")); + } + body(&action.content).map_err(|e| IdeError::invalid(e.to_string()))?; + label(&action.request_id, 128).map_err(|e| IdeError::invalid(e.to_string()))?; + drop(tabs); + let mut pending = self.shared.content_requests.lock(); + pending.retain(|_, (_, created)| created.elapsed() < Duration::from_secs(15)); + if pending.keys().filter(|(p, w, _)| p == plugin && w == client).count() >= 32 { + return Err(IdeError::invalid("too many pending content actions")); + } + pending.insert((plugin.into(), client.into(), action.request_id.clone()), (action.key.clone(), Instant::now())); + drop(pending); + self.post_owned_event(plugin, client, WorkbenchEvent::ContentTabAction(action)); + Ok(()) + } + fn post_owned_event(&self, plugin: &str, client: &str, event: WorkbenchEvent) { + if self.shared.client_closed(client) { + return; + } + self.queues + .lock() + .entry((plugin.into(), Some(client.into()))) + .or_insert_with(|| EventQueue { since: Instant::now(), events: Vec::new(), files_source: None }) + .events + .push(event); + } +} + +/// Dynamic configurations opt in with marker files, just like static detectors. +pub(super) fn configurations_declaration( + pkg: &ide_extension::Package, + value: toml::Value, +) -> Result { + #[derive(serde::Deserialize, serde::Serialize)] + #[serde(deny_unknown_fields)] + struct Decl { + files: Vec, + } + pkg.manifest.needs_api("contributes.runConfigurations", 4)?; + let decl: Decl = ide_extension::decode("runConfigurations", value)?; + if pkg.manifest.wasm.is_none() || decl.files.is_empty() || decl.files.len() > 16 { + return Err("runConfigurations needs code and 1-16 marker files".into()); + } + for file in &decl.files { + ide_extension::check_relative(std::path::Path::new(file), "files")?; + } + Ok(ide_extension::UiDecl::new( + serde_json::to_string(&decl.files).unwrap(), + "Run configurations", + serde_json::to_value(decl).unwrap(), + )) +} + +impl PluginHost { + /// Refreshes configuration exports on the same listing path as static detectors. + pub fn refresh_run_configurations(&self, root: &std::path::Path) { + if !self.shared.workspace().is_some_and(|w| w.trusted(root)) { + return; + } + let owners: std::collections::BTreeSet<_> = self + .ui + .items("runConfigurations", None) + .into_iter() + .filter(|d| { + d.decl["files"] + .as_array() + .is_some_and(|files| files.iter().filter_map(|f| f.as_str()).any(|f| root.join(f).is_file())) + }) + .map(|d| d.plugin_id) + .collect(); + self.shared.run_configurations.write().retain(|(p, r), _| r != root || owners.contains(p)); + for plugin in owners { + let project = root.to_path_buf(); + let invocation = ide_plugin::Invocation { workspace_root: Some(project.clone()), ..Default::default() }; + let result = self.code_call(&plugin, invocation, "run-configurations", |rt, ctx| { + rt.run_configurations( + ctx.workspace_root.unwrap().to_string_lossy().into_owned(), + Duration::from_secs(2), + ) + }); + let Ok(items) = result else { continue }; + if let Err(error) = validate_configurations(root, &items) { + tracing::warn!(%plugin, %error, "invalid run configurations"); + continue; + } + self.shared.run_configurations.write().insert((plugin, project), items); + } + } + /// Parses a bounded batch under the editor-provider soft/hard budgets. + pub fn parse_test_results( + &self, + client: &str, + config: &str, + chunk: String, + ) -> IdeResult>> { + if chunk.len() > 64 << 10 { + return Err(IdeError::invalid("test output batch too large")); + } + let root = self.shared.project_of(client).ok_or_else(|| IdeError::not_found("no project"))?; + if !self.shared.workspace().is_some_and(|w| w.trusted(&root)) { + return Err(IdeError::invalid("project is not trusted")); + } + let Some(rest) = config.strip_prefix(ide_run::EXTERNAL_PREFIX) else { return Ok(None) }; + let Some((owner, id)) = rest.split_once(':') else { return Ok(None) }; + let configuration = self + .shared + .workspace() + .ok_or_else(IdeError::cancelled)? + .run_configuration(client, owner, id) + .map_err(|e| IdeError::invalid(e.to_string()))?; + if !configuration.is_test || configuration.extras.params.get("framework").map(String::as_str) != Some("plugin") + { + return Ok(None); + } + let plugin = self.get(owner)?; + let (actor, activated) = self.ensure_active(&plugin, None)?; + if activated { + self.changed(); + } + let invocation = ide_plugin::Invocation { workspace_root: Some(root.clone()), ..Default::default() }; + let (id, hard) = (id.to_owned(), self.limits.edit_hard); + let started = Instant::now(); + let result = + actor.call(Some(self.limits.edit_soft), move |rt| rt.parse_test_results(id, chunk, invocation, hard)); + let mut events = self.finish_provider_call(&plugin, &actor, started, result); + if let Some(events) = &mut events { + if serde_json::to_vec(&*events).is_ok_and(|b| b.len() > 4 << 20) { + return Ok(None); + } + for event in events { + let location = match event { + ide_plugin::PluginTestEvent::SuiteStarted(v) | ide_plugin::PluginTestEvent::TestStarted(v) => { + v.location.as_mut() + } + ide_plugin::PluginTestEvent::Failed(v) => v.location.as_mut(), + _ => None, + }; + if let Some(location) = location { + let Ok(path) = ide_plugin::resolve_new_in_root(&root, &location.path.to_string_lossy()) else { + return Ok(None); + }; + if location.range.end < location.range.start { + return Ok(None); + } + location.path = path; + } + } + } + Ok(events) + } +} + +fn validate_configurations( + root: &std::path::Path, + items: &[ide_plugin::PluginRunConfiguration], +) -> Result<(), HostError> { + let mut ids = std::collections::BTreeSet::new(); + if items.len() > 1000 || serde_json::to_vec(items).is_ok_and(|b| b.len() > 4 << 20) { + return Err(HostError::Invalid("too many run configurations".into())); + } + for c in items { + ide_extension::validate_item_id(&c.id, "run configuration id").map_err(HostError::Invalid)?; + label(&c.name, 128)?; + if !ids.insert(&c.id) + || c.program.is_empty() + || c.program.len() > 256 + || c.program.contains(char::is_whitespace) + || c.args.len() > 256 + || c.args.iter().any(|a| a.len() > 4096 || a.contains('\0')) + { + return Err(HostError::Invalid("invalid run configuration".into())); + } + if let Some(cwd) = &c.cwd { + ide_plugin::resolve_new_in_root(root, cwd).map_err(HostError::Invalid)?; + } + } + Ok(()) +} + +impl PluginHost { + /// Resolves and rechecks the pin immediately before starting a keyed terminal's PTY. + pub fn spawn_terminal_session( + self: &std::sync::Arc, + host: &std::sync::Arc, + client: &str, + operation: &str, + cols: u16, + rows: u16, + output: crate::ops::Emit, + ) -> IdeResult { + let (plugin, command) = { + let mut operations = self.shared.operations.lock(); + let op = operations + .get_mut(operation) + .filter(|op| op.client == client) + .ok_or_else(|| IdeError::not_found("unknown terminal operation"))?; + if op.started || op.closed || op.root != self.shared.project_of(client) { + return Err(IdeError::invalid("terminal operation already started or project changed")); + } + let OperationKind::Terminal { command, .. } = &op.kind else { + return Err(IdeError::invalid("not a terminal operation")); + }; + op.started = true; + (op.plugin.clone(), command.clone()) + }; + let options = super::web::WebPtyCommand { + program: command.program, + args: command.args, + env: command.env.into_iter().collect(), + cols, + rows, + }; + let weak = Arc::downgrade(self); + let (id, window) = (operation.to_owned(), client.to_owned()); + let completion = Arc::new(move |status| { + if let Some(plugins) = weak.upgrade() { + let closed = plugins.shared.operations.lock().get(&id).is_some_and(|op| op.closed); + plugins.finish_operation(&window, &id, status, closed); + } + }); + let result = self.spawn_plugin_pty(host, &plugin, client, output, Some(completion), || { + self.pty_options(&plugin, client, options) + }); + if let Ok(info) = &result { + let mut operations = self.shared.operations.lock(); + if let Some(op) = operations.get_mut(operation) { + op.terminal = Some((Arc::downgrade(host), info.id)); + if op.closed { + let _ = host.terminals.kill(info.id); + } + } + } + if result.is_err() + && let Some(op) = self.shared.operations.lock().get_mut(operation) + { + op.started = false; + } + result + } +} + +impl PluginHost { + /// Closing a tab marks the operation before killing its PTY. The real exit emits the event. + pub fn close_terminal_session(&self, client: &str, id: &str) -> IdeResult<()> { + let terminal = { + let mut operations = self.shared.operations.lock(); + let Some(op) = operations + .get_mut(id) + .filter(|op| op.client == client && matches!(op.kind, OperationKind::Terminal { .. })) + else { + return Ok(()); + }; + op.closed = true; + op.terminal.clone() + }; + if let Some((host, id)) = terminal + && let Some(host) = host.upgrade() + { + host.terminals.kill(id)?; + } + Ok(()) + } +} + +impl PluginHost { + /// Rechecks the owned configuration's program before Debug and every IDE rerun. + pub fn prepare_debug_run(&self, client: &str, mut config: ide_run::RunConfig) -> IdeResult { + let Some((plugin, id)) = config.id.strip_prefix(ide_run::EXTERNAL_PREFIX).and_then(|s| s.split_once(':')) + else { + return Err(IdeError::invalid("not a plugin configuration")); + }; + let workspace = self.shared.workspace().ok_or_else(IdeError::cancelled)?; + let owned = workspace.run_configuration(client, plugin, id).map_err(|e| IdeError::invalid(e.to_string()))?; + if owned.debug_adapter.is_none() { + return Err(IdeError::invalid("this configuration has no debugger")); + } + if config.extras.target.is_none() { + config.extras.target = owned.extras.target.clone(); + } + if config.extras.target.as_ref().is_some_and(|t| !t.is_empty()) { + return Err(IdeError::invalid("debugging this target is not supported")); + } + config.command = self + .program_for_project(plugin, &owned.command, self.shared.project_of(client).as_deref())? + .to_string_lossy() + .into_owned(); + config.debug_adapter = owned.debug_adapter; + Ok(config) + } +} + +/// Internal launch metadata; it is ignored after the original operation finishes. +pub(super) const RUN_OPERATION: &str = "plugin.operation"; +impl PluginHost { + pub(crate) fn run_operation_sink( + self: &Arc, + client: &str, + config: &mut ide_run::RunConfig, + previous: Arc, + ) -> IdeResult> { + let Some(id) = config.extras.params.get(RUN_OPERATION).cloned() else { return Ok(previous) }; + let origin = self.shared.operations.lock().get(&id).and_then(|op| { + if op.client != client { + return None; + } + let OperationKind::Run { configuration, program, .. } = &op.kind else { return None }; + (configuration == &config.id).then(|| (op.plugin.clone(), op.root.clone(), program.clone())) + }); + let Some((plugin, root, program)) = origin else { return Ok(previous) }; + let binary = self.program_for_project(&plugin, &program, root.as_deref())?; + config.command = if config.extras.target.as_ref().is_some_and(|t| !t.is_empty()) { + program + } else { + binary.to_string_lossy().into_owned() + }; + config.detected = false; + let (weak, client) = (Arc::downgrade(self), client.to_owned()); + Ok(Arc::new(move |event: ide_core::IdeEvent| { + if let ide_core::IdeEvent::RunExited { code, .. } = &event + && let Some(plugins) = weak.upgrade() + { + let stopped = plugins.shared.operations.lock().get(&id).is_some_and(|op| op.closed); + plugins.finish_operation(&client, &id, *code, stopped); + } + previous.emit(event); + })) + } + pub(crate) fn run_operation_started(&self, client: &str, operation: &str, run: ide_core::RunId) { + if let Some(op) = self + .shared + .operations + .lock() + .get_mut(operation) + .filter(|op| op.client == client && matches!(op.kind, OperationKind::Run { .. })) + { + op.run = Some(run); + } + } + pub(crate) fn run_operation_stopped(&self, run: ide_core::RunId) { + for op in self.shared.operations.lock().values_mut().filter(|op| op.run == Some(run)) { + op.closed = true; + } + } +} diff --git a/crates/ide-host/src/plugins/index_ops.rs b/crates/ide-host/src/plugins/index_ops.rs index ef11aac62..60f09fd6a 100644 --- a/crates/ide-host/src/plugins/index_ops.rs +++ b/crates/ide-host/src/plugins/index_ops.rs @@ -113,6 +113,7 @@ pub async fn plugin_navigation_index( &project.root, crate::plugins::navigation::Snapshot { path: &path, version, text: &text, symbols: &symbols, scope }, Invocation { + command_chain: Vec::new(), client: Some(client.clone()), workspace_root: Some(project.root.clone()), editor: None, diff --git a/crates/ide-host/src/plugins/injections.rs b/crates/ide-host/src/plugins/injections.rs new file mode 100644 index 000000000..79e132845 --- /dev/null +++ b/crates/ide-host/src/plugins/injections.rs @@ -0,0 +1,126 @@ +//! Embedded plugin-provider documents preserve the host's UTF-16 coordinates. +use super::{PluginHost, ProviderKind}; +use ide_core::{Pos, Range}; +use ide_plugin::{ProviderDocument, language_features::Injection}; +use std::sync::Arc; + +/// Allowed output ranges for one virtual document; the host document is unrestricted. +pub(super) struct Bounds(Option>); +impl Bounds { + pub fn unrestricted() -> Self { + Self(None) + } + pub fn contains(&self, range: Range) -> bool { + self.0.as_ref().is_none_or(|rs| rs.iter().any(|r| r.start <= range.start && range.end <= r.end)) + } + pub fn lines(&self, start: u32, end: u32) -> bool { + self.0.as_ref().is_none_or(|rs| rs.iter().any(|r| r.start.line <= start && end <= r.end.line)) + } +} + +impl PluginHost { + pub fn provide_injections(&self, doc: &ProviderDocument) -> Vec { + if !self.providers.any(ProviderKind::Injections) { + return vec![]; + } + if doc.text.len() > super::navigation::MAX_DOCUMENT_BYTES { + return Vec::new(); + } + let text = ide_text::Document::new(&doc.text); + let d = Arc::new(doc.clone()); + let mut items: Vec<_> = self + .document_calls(ProviderKind::Injections, doc, move |rt, t| rt.provide_injections(&d, t)) + .into_iter() + .flat_map(|(_, v)| v) + .filter(|i| { + !i.range.is_empty() + && super::language_features::valid_range_in(&text, i.range) + && i.language != doc.language + && !i.language.is_empty() + && i.language.len() <= 64 + && i.language.chars().all(|c| c.is_ascii_alphanumeric() || "-_.+#".contains(c)) + }) + .collect(); + items.sort_by_key(|i| i.range); + let mut end = Pos::default(); + items.retain(|i| { + if i.range.start < end { + return false; + } + end = i.range.end; + true + }); + items.truncate(1000); + items + } + + pub(super) fn feature_documents(&self, doc: &ProviderDocument, at: Option) -> Vec<(ProviderDocument, Bounds)> { + let injections = self.provide_injections(doc); + let mut languages = Vec::new(); + for i in &injections { + if at.is_some_and(|p| p < i.range.start || p >= i.range.end) || languages.contains(&i.language) { + continue; + } + languages.push(i.language.clone()); + if languages.len() == 32 { + break; + } + } + let mut docs = Vec::new(); + for language in languages { + let ranges = injections.iter().filter(|i| i.language == language).map(|i| i.range).collect(); + let text = virtual_text(doc, &injections, &language); + docs.push((ProviderDocument { language, text, ..doc.clone() }, Bounds(Some(ranges)))); + } + docs.push((doc.clone(), Bounds(None))); + docs + } +} + +pub(crate) fn virtual_text(doc: &ProviderDocument, ranges: &[Injection], language: &str) -> String { + let ranges: Vec<_> = ranges.iter().filter(|i| i.language == language).map(|i| i.range).collect(); + let mut index = 0; + let mut at = Pos::default(); + let mut text = String::with_capacity(doc.text.len()); + for c in doc.text.chars() { + while index < ranges.len() && ranges[index].end <= at { + index += 1; + } + let inside = ranges.get(index).is_some_and(|r| r.start <= at && at < r.end); + if inside || c == '\n' || c == '\r' { + text.push(c); + } else { + for _ in 0..c.len_utf16() { + text.push(' '); + } + } + if c == '\n' { + at.line += 1; + at.col = 0; + } else { + at.col += c.len_utf16() as u32; + } + } + text +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn virtual_documents_preserve_utf16_columns_and_newlines() { + let d = ProviderDocument { + path: "/p/a".into(), + language: "host".into(), + version: 7, + text: "😀 sql\n outside\n sql".into(), + }; + let ranges = vec![ + Injection { range: Range::new(Pos::new(0, 3), Pos::new(0, 6)), language: "sql".into() }, + Injection { range: Range::new(Pos::new(2, 1), Pos::new(2, 4)), language: "sql".into() }, + ]; + let text = virtual_text(&d, &ranges, "sql"); + assert_eq!(text, " sql\n \n sql"); + assert_eq!(text.encode_utf16().count(), d.text.encode_utf16().count()); + } +} diff --git a/crates/ide-host/src/plugins/ipc.rs b/crates/ide-host/src/plugins/ipc.rs new file mode 100644 index 000000000..50612562d --- /dev/null +++ b/crates/ide-host/src/plugins/ipc.rs @@ -0,0 +1,174 @@ +//! Plugin-owned local byte streams, closed on resource drop and plugin shutdown. +use super::api::net_runtime; +use ide_core::CancellationToken; +use ide_plugin::HostError; +use parking_lot::Mutex; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Duration; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +trait Stream: AsyncRead + AsyncWrite + Unpin + Send {} +impl Stream for T {} +struct Connection { + stream: tokio::sync::Mutex>, + closed: CancellationToken, +} +impl Drop for Connection { + fn drop(&mut self) { + self.closed.cancel(); + } +} + +#[derive(Default)] +pub(super) struct Connections { + next: AtomicU64, + entries: Mutex>>, +} +fn failed(e: impl std::fmt::Display) -> HostError { + HostError::Failed(e.to_string()) +} +impl Connections { + pub fn connect( + &self, + plugin: &str, + name: &str, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result { + let check = |entries: &HashMap<(String, u64), Arc>| { + if entries.keys().filter(|(p, _)| p == plugin).count() >= 8 { + Err(HostError::Invalid("at most eight IPC connections per plugin".into())) + } else { + Ok(()) + } + }; + check(&self.entries.lock())?; + let stream: Box = net_runtime().block_on(async { + let connect = async { + #[cfg(unix)] + { Ok::, HostError>(Box::new(tokio::net::UnixStream::connect(name).await.map_err(failed)?)) } + #[cfg(windows)] + { Ok::, HostError>(Box::new(tokio::net::windows::named_pipe::ClientOptions::new().open(name).map_err(failed)?)) } + #[cfg(not(any(unix, windows)))] + { let _ = name; Err(HostError::Failed("local IPC unavailable".into())) } + }; + tokio::select! { + r = tokio::time::timeout(timeout, connect) => r.map_err(|_| HostError::Failed("IPC connection timed out".into()))?, + () = cancel.cancelled() => Err(HostError::Cancelled), + } + })?; + let mut entries = self.entries.lock(); + check(&entries)?; + if cancel.is_cancelled() { + return Err(HostError::Cancelled); + } + let id = self.next.fetch_add(1, Ordering::Relaxed); + entries.insert( + (plugin.into(), id), + Arc::new(Connection { stream: tokio::sync::Mutex::new(stream), closed: CancellationToken::new() }), + ); + Ok(id) + } + fn get(&self, plugin: &str, id: u64) -> Result, HostError> { + self.entries + .lock() + .get(&(plugin.into(), id)) + .cloned() + .ok_or_else(|| HostError::NotFound("IPC connection".into())) + } + pub fn write( + &self, + plugin: &str, + id: u64, + bytes: Vec, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + let entry = self.get(plugin, id)?; + net_runtime().block_on(async { + tokio::select! { + r = tokio::time::timeout(timeout, async { entry.stream.lock().await.write_all(&bytes).await.map_err(failed) }) => r.map_err(|_| HostError::Failed("IPC write timed out".into()))?, + () = cancel.cancelled() => Err(HostError::Cancelled), + () = entry.closed.cancelled() => Err(HostError::Cancelled), + } + }) + } + pub fn read( + &self, + plugin: &str, + id: u64, + max: usize, + timeout: Duration, + cancel: &CancellationToken, + ) -> Result, HostError> { + let entry = self.get(plugin, id)?; + net_runtime().block_on(async { + let read = async { + let mut bytes = vec![0; max.min(1 << 20)]; + let count = entry.stream.lock().await.read(&mut bytes).await.map_err(failed)?; + bytes.truncate(count); + Ok(bytes) + }; + tokio::select! { + biased; + () = cancel.cancelled() => Err(HostError::Cancelled), + () = entry.closed.cancelled() => Err(HostError::Cancelled), + r = read => r, + () = tokio::time::sleep(timeout) => Ok(Vec::new()), + } + }) + } + pub fn close(&self, plugin: &str, id: u64) { + if let Some(entry) = self.entries.lock().remove(&(plugin.into(), id)) { + entry.closed.cancel(); + } + } + pub fn clear(&self, plugin: &str) { + self.entries.lock().retain(|(p, _), entry| { + if p != plugin { + return true; + } + entry.closed.cancel(); + false + }); + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use std::io::{Read, Write}; + use std::os::unix::net::UnixListener; + #[test] + fn echo_limit_and_shutdown() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("echo.sock"); + let listener = UnixListener::bind(&path).unwrap(); + let worker = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut bytes = [0; 4]; + stream.read_exact(&mut bytes).unwrap(); + stream.write_all(&bytes).unwrap(); + // Accept the remaining seven connections before shutdown. + for _ in 1..8 { + listener.accept().unwrap(); + } + }); + let pool = Connections::default(); + let token = CancellationToken::new(); + let timeout = Duration::from_secs(2); + let id = pool.connect("a", path.to_str().unwrap(), &token, timeout).unwrap(); + pool.write("a", id, b"echo".to_vec(), &token, timeout).unwrap(); + assert_eq!(pool.read("a", id, 4, timeout, &token).unwrap(), b"echo"); + for _ in 1..8 { + pool.connect("a", path.to_str().unwrap(), &token, timeout).unwrap(); + } + assert!(matches!(pool.connect("a", path.to_str().unwrap(), &token, timeout), Err(HostError::Invalid(_)))); + assert!(pool.read("b", id, 4, timeout, &token).is_err()); + pool.clear("a"); + assert!(pool.read("a", id, 4, timeout, &token).is_err()); + worker.join().unwrap(); + } +} diff --git a/crates/ide-host/src/plugins/language_features.rs b/crates/ide-host/src/plugins/language_features.rs new file mode 100644 index 000000000..b7852690a --- /dev/null +++ b/crates/ide-host/src/plugins/language_features.rs @@ -0,0 +1,462 @@ +//! API 1.4 language features. Results are bounded and checked against the snapshot. +use super::{PluginHost, ProviderKind}; +use ide_core::{IdeResult, Pos, Range}; +use ide_plugin::{ProviderDocument, language_features::*}; +use std::sync::Arc; + +pub(crate) fn valid_range(doc: &ProviderDocument, range: Range) -> bool { + valid_range_in(&ide_text::Document::new(&doc.text), range) +} +pub(super) fn valid_range_in(text: &ide_text::Document, range: Range) -> bool { + if range.start > range.end { + return false; + } + [range.start, range.end].iter().all(|p| text.offset_to_pos(text.pos_to_offset(*p)) == *p) +} + +impl PluginHost { + pub fn language_format( + &self, + id: &str, + doc: &ProviderDocument, + range: Option, + options: FormatOptions, + ) -> IdeResult> { + let candidates = if let Some(r) = range { + self.feature_documents(doc, Some(r.start)) + } else { + vec![(doc.clone(), super::injections::Bounds::unrestricted())] + }; + let (target, bounds) = candidates + .into_iter() + .find(|(d, b)| { + range.is_none_or(|r| b.contains(r)) + && self + .provider_plugins(ProviderKind::Formatting, &d.language) + .iter() + .any(|p| p.id() == id && p.dev_root.as_ref().is_none_or(|r| d.path.starts_with(r))) + }) + .ok_or_else(|| ide_core::IdeError::not_found("The selected plugin formatter is unavailable"))?; + let text = ide_text::Document::new(&target.text); + let d = Arc::new(target); + let edits = self.code_call(id, ide_plugin::Invocation::default(), "formatting", move |rt, _| { + rt.language_format(&d, range, options.clone(), std::time::Duration::from_secs(5)) + })?; + if edits.iter().any(|e| !valid_range_in(&text, e.range) || !bounds.contains(e.range)) { + return Err(ide_core::IdeError::invalid("Plugin formatter returned an invalid range")); + } + let mut ranges: Vec<_> = edits.iter().map(|e| e.range).collect(); + ranges.sort(); + if ranges.windows(2).any(|p| p[0].end > p[1].start) { + return Err(ide_core::IdeError::invalid("Plugin formatter returned overlapping edits")); + } + Ok(edits) + } + pub fn language_signature_help(&self, doc: &ProviderDocument, at: Pos) -> Option { + for (d, _bounds) in self.feature_documents(doc, Some(at)) { + let snapshot = d.clone(); + let d = Arc::new(d); + let results = self.document_calls(ProviderKind::SignatureHelp, &snapshot, move |rt, t| { + rt.language_signature_help(&d, at, t) + }); + for (_, v) in results { + if let Some(v) = v.filter(|h| !h.signatures.is_empty()) { + return Some(v); + } + } + } + None + } + pub fn language_document_highlights(&self, doc: &ProviderDocument, at: Pos) -> Vec { + let mut out = Vec::new(); + for (d, bounds) in self.feature_documents(doc, Some(at)) { + let snapshot = d.clone(); + let text = ide_text::Document::new(&snapshot.text); + let d = Arc::new(d); + let results = self.document_calls(ProviderKind::DocumentHighlights, &snapshot, move |rt, t| { + rt.language_document_highlights(&d, at, t) + }); + for (p, items) in results { + for i in items { + let _ = &p; + if valid_range_in(&text, i.range) && bounds.contains(i.range) { + out.push(i); + } + } + } + } + out + } + pub fn language_document_links(&self, doc: &ProviderDocument) -> Vec { + let mut out = Vec::new(); + for (d, bounds) in self.feature_documents(doc, None) { + let snapshot = d.clone(); + let text = ide_text::Document::new(&snapshot.text); + let d = Arc::new(d); + let results = self + .document_calls(ProviderKind::DocumentLinks, &snapshot, move |rt, t| rt.language_document_links(&d, t)); + for (p, items) in results { + for i in items { + let _ = &p; + if valid_range_in(&text, i.range) && bounds.contains(i.range) { + out.push(i); + } + } + } + } + out + } + pub fn language_code_lenses(&self, doc: &ProviderDocument) -> Vec<(String, CodeLens)> { + let mut out = Vec::new(); + for (d, bounds) in self.feature_documents(doc, None) { + let snapshot = d.clone(); + let text = ide_text::Document::new(&snapshot.text); + let d = Arc::new(d); + let results = + self.document_calls(ProviderKind::CodeLenses, &snapshot, move |rt, t| rt.language_code_lenses(&d, t)); + for (p, items) in results { + let commands: std::collections::HashSet<_> = + self.ui.items(super::COMMANDS, Some(p.id())).into_iter().map(|c| c.key).collect(); + for i in items { + if valid_range_in(&text, i.range) && bounds.contains(i.range) && commands.contains(&i.command) { + out.push((p.id().to_owned(), i)); + } + } + } + } + out + } + pub fn language_selection_ranges(&self, doc: &ProviderDocument, at: Pos) -> Vec { + let mut out = Vec::new(); + for (d, bounds) in self.feature_documents(doc, Some(at)) { + let snapshot = d.clone(); + let text = ide_text::Document::new(&snapshot.text); + let d = Arc::new(d); + let results = self.document_calls(ProviderKind::SelectionRanges, &snapshot, move |rt, t| { + rt.language_selection_ranges(&d, at, t) + }); + for (p, items) in results { + for i in items { + let _ = &p; + if valid_range_in(&text, i) && i.start <= at && at <= i.end && bounds.contains(i) { + out.push(i); + } + } + } + } + out.sort_by(|a, b| b.start.cmp(&a.start).then(a.end.cmp(&b.end))); + out.dedup(); + let mut end = None; + out.retain(|r| { + let ok = end.is_none_or(|prev: Range| r.start <= prev.start && prev.end <= r.end); + if ok { + end = Some(*r); + } + ok + }); + out + } + pub fn language_folding(&self, doc: &ProviderDocument) -> Vec { + let mut out = Vec::new(); + for (d, bounds) in self.feature_documents(doc, None) { + let lines = d.text.split('\n').count() as u32; + let snapshot = d.clone(); + let d = Arc::new(d); + let results = + self.document_calls(ProviderKind::Folding, &snapshot, move |rt, t| rt.language_folding(&d, t)); + for (p, items) in results { + for i in items { + let _ = &p; + if i.start_line < i.end_line && i.end_line < lines && bounds.lines(i.start_line, i.end_line) { + out.push(i); + } + } + } + } + out + } + pub fn language_colors(&self, doc: &ProviderDocument) -> Vec<(String, ColorInfo)> { + let mut out = Vec::new(); + for (d, bounds) in self.feature_documents(doc, None) { + let snapshot = d.clone(); + let text = ide_text::Document::new(&snapshot.text); + let d = Arc::new(d); + let results = self.document_calls(ProviderKind::Colors, &snapshot, move |rt, t| rt.language_colors(&d, t)); + for (p, items) in results { + for i in items { + if valid_range_in(&text, i.range) + && bounds.contains(i.range) + && [i.red, i.green, i.blue, i.alpha].iter().all(|c| c.is_finite() && (0.0..=1.0).contains(c)) + { + out.push((p.id().to_owned(), i)); + } + } + } + } + out + } + pub fn language_color_presentations(&self, id: &str, doc: &ProviderDocument, color: ColorInfo) -> Vec { + for (d, bounds) in self.feature_documents(doc, Some(color.range.start)) { + if !bounds.contains(color.range) + || !self.provider_plugins(ProviderKind::Colors, &d.language).iter().any(|p| p.id() == id) + { + continue; + } + let snapshot = d.clone(); + let d = Arc::new(d); + let color = color.clone(); + let result = self.document_calls_for(ProviderKind::Colors, &snapshot, Some(id), move |rt, t| { + rt.language_color_presentations(&d, color.clone(), t) + }); + if let Some((_, items)) = result.into_iter().find(|(p, _)| p.id() == id) { + return items.into_iter().take(100).collect(); + } + } + Vec::new() + } + pub fn language_document_symbols(&self, doc: &ProviderDocument) -> Vec { + let mut out = Vec::new(); + for (d, bounds) in self.feature_documents(doc, None) { + let snapshot = d.clone(); + let text = ide_text::Document::new(&snapshot.text); + let d = Arc::new(d); + let results = self.document_calls(ProviderKind::DocumentSymbols, &snapshot, move |rt, t| { + rt.language_document_symbols(&d, t) + }); + for (_, items) in results { + let mut remap = std::collections::HashMap::new(); + for (index, mut i) in items.into_iter().enumerate() { + if !valid_range_in(&text, i.range) + || !valid_range_in(&text, i.selection_range) + || i.selection_range.start < i.range.start + || i.selection_range.end > i.range.end + || !bounds.contains(i.range) + { + continue; + } + i.parent = i.parent.and_then(|p| remap.get(&(p as usize)).copied()); + remap.insert(index, out.len() as u32); + out.push(i); + } + } + } + out + } + pub fn language_implementation(&self, doc: &ProviderDocument, at: Pos) -> Vec { + let mut out = Vec::new(); + for (d, _bounds) in self.feature_documents(doc, Some(at)) { + let snapshot = d.clone(); + let d = Arc::new(d); + let results = self.document_calls(ProviderKind::Implementation, &snapshot, move |rt, t| { + rt.language_implementation(&d, at, t) + }); + for (p, items) in results { + for i in items { + let _ = &p; + out.push(i); + } + } + } + out + } + pub fn language_type_definition(&self, doc: &ProviderDocument, at: Pos) -> Vec { + let mut out = Vec::new(); + for (d, _bounds) in self.feature_documents(doc, Some(at)) { + let snapshot = d.clone(); + let d = Arc::new(d); + let results = self.document_calls(ProviderKind::TypeDefinition, &snapshot, move |rt, t| { + rt.language_type_definition(&d, at, t) + }); + for (p, items) in results { + for i in items { + let _ = &p; + out.push(i); + } + } + } + out + } + pub fn language_gutter_actions(&self, doc: &ProviderDocument) -> Vec<(String, GutterAction)> { + let mut out = Vec::new(); + for (d, bounds) in self.feature_documents(doc, None) { + let lines = d.text.split('\n').count() as u32; + let snapshot = d.clone(); + let d = Arc::new(d); + let results = self + .document_calls(ProviderKind::GutterActions, &snapshot, move |rt, t| rt.language_gutter_actions(&d, t)); + for (p, items) in results { + let commands: std::collections::HashSet<_> = + self.ui.items(super::COMMANDS, Some(p.id())).into_iter().map(|c| c.key).collect(); + for i in items { + if i.line < lines && bounds.lines(i.line, i.line) && commands.contains(&i.command) { + out.push((p.id().to_owned(), i)); + } + } + } + } + out + } +} + +impl PluginHost { + pub(crate) fn run_language_command( + &self, + plugin: &str, + command: &str, + argument: Option<&str>, + invocation: ide_plugin::Invocation, + ) -> IdeResult<()> { + if !self.ui.items(super::COMMANDS, Some(plugin)).iter().any(|c| c.key == command) { + return Err(ide_core::IdeError::not_found("The plugin did not declare this command")); + } + self.run_command(plugin, command, argument, invocation).map(|_| ()) + } +} + +pub(crate) fn signature_help(h: SignatureHelp) -> ide_lsp::types::SignatureHelp { + let signatures: Vec<_> = h + .signatures + .into_iter() + .take(100) + .map(|s| { + let len = s.label.encode_utf16().count() as u32; + let parameters = s + .parameters + .into_iter() + .take(100) + .map(|p| match p.label { + ParameterLabel::Offsets((a, b)) => (a.min(len), b.max(a).min(len)), + ParameterLabel::Text(t) => s + .label + .find(&t) + .map(|a| { + let start = s.label[..a].encode_utf16().count() as u32; + (start, start + t.encode_utf16().count() as u32) + }) + .unwrap_or((0, 0)), + }) + .collect(); + ide_lsp::types::Signature { + label: s.label, + documentation: s.documentation.map(|value| ide_lsp::types::Markup { kind: "markdown".into(), value }), + parameters, + } + }) + .collect(); + let active_signature = h.active_signature.min(signatures.len().saturating_sub(1) as u32); + let active_parameter = + h.active_parameter + .min(signatures.get(active_signature as usize).map(|s| s.parameters.len().saturating_sub(1)).unwrap_or(0) + as u32); + ide_lsp::types::SignatureHelp { signatures, active_signature, active_parameter } +} + +pub(crate) fn outline(items: Vec) -> Vec { + // A parent must precede its child, ruling out cycles and unbounded recursion. + let mut depths = vec![0usize; items.len()]; + let mut parents = vec![None; items.len()]; + for (idx, item) in items.iter().enumerate() { + if let Some(parent) = item.parent.map(|p| p as usize).filter(|p| { + *p < idx + && depths[*p] < 63 + && items[*p].range.start <= item.range.start + && item.range.end <= items[*p].range.end + }) { + parents[idx] = Some(parent); + depths[idx] = depths[parent] + 1; + } + } + let mut nodes: Vec<_> = items + .into_iter() + .map(|i| { + Some(ide_core::OutlineItem { + name: i.name, + detail: Some(i.detail).filter(|s| !s.is_empty()), + kind: serde_json::from_value(serde_json::Value::String(symbol_kind_name(&i.kind))) + .unwrap_or(ide_core::SymbolKind::Unknown), + range: i.range, + selection_range: i.selection_range, + children: vec![], + visibility: None, + }) + }) + .collect(); + let mut roots = Vec::new(); + for idx in (0..nodes.len()).rev() { + let mut node = nodes[idx].take().expect("once"); + node.children.reverse(); + if let Some(parent) = parents[idx] { + nodes[parent].as_mut().expect("parent precedes child").children.push(node); + } else { + roots.push(node); + } + } + roots.reverse(); + roots +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub struct FormatterChoice { + pub id: String, + pub name: String, + pub languages: Vec, +} +impl PluginHost { + pub fn formatter_choices(&self) -> Vec { + let mut out = std::collections::BTreeMap::::new(); + for (id, languages) in self.providers.formatter_declarations() { + let Ok(p) = self.get(&id) else { + continue; + }; + if !matches!(p.meta.lock().status, super::PluginStatus::Active | super::PluginStatus::Inactive) { + continue; + } + out.entry(id.clone()) + .or_insert_with(|| FormatterChoice { id, name: p.pkg.manifest.name.clone(), languages: vec![] }) + .languages + .extend(languages); + } + out.into_values() + .map(|mut c| { + c.languages.sort(); + c.languages.dedup(); + c + }) + .collect() + } +} + +fn symbol_kind_name(kind: &str) -> String { + let mut parts = kind.split('-'); + let mut out = parts.next().unwrap_or_default().to_owned(); + for part in parts { + let mut chars = part.chars(); + if let Some(c) = chars.next() { + out.extend(c.to_uppercase()); + out.extend(chars); + } + } + out +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn outline_uses_parent_order_containment_and_string_kinds() { + let range = Range::new(Pos::new(0, 0), Pos::new(0, 10)); + let item = |name: &str, parent| DocumentSymbol { + name: name.into(), + detail: String::new(), + kind: "enum-member".into(), + range, + selection_range: range, + parent, + }; + let out = + outline(vec![item("root", None), item("child", Some(0)), item("future", Some(3)), item("other", Some(2))]); + assert_eq!(out[0].children[0].kind, ide_core::SymbolKind::EnumMember); + assert_eq!(out[1].name, "future"); + assert_eq!(out[1].children[0].name, "other"); + } +} diff --git a/crates/ide-host/src/plugins/mcp.rs b/crates/ide-host/src/plugins/mcp.rs new file mode 100644 index 000000000..419f368d5 --- /dev/null +++ b/crates/ide-host/src/plugins/mcp.rs @@ -0,0 +1,176 @@ +//! Validated MCP declarations and dispatch through the ordinary plugin actor. +use super::{PluginHost, PluginStatus}; +use ide_core::{IdeError, IdeResult}; +use ide_extension::{Package, UiDecl, decode}; +use ide_plugin::Invocation; +use serde::Deserialize; + +pub(super) const POINT: &str = "mcpTools"; +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct Declaration { + name: String, + description: String, + input_schema: std::path::PathBuf, + read_only: bool, + destructive: bool, +} +fn tool_name(plugin: &str, name: &str) -> String { + let plugin: String = plugin + .chars() + .map(|c| if c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' { c } else { '_' }) + .collect(); + format!("{plugin}__{name}") +} +pub(super) fn declaration(pkg: &Package, value: toml::Value) -> Result { + pkg.manifest.needs_api("contributes.mcpTools", 4)?; + if pkg.manifest.wasm.is_none() { + return Err("mcpTools requires wasm".into()); + } + let d: Declaration = decode(POINT, value)?; + if d.name.is_empty() + || d.name.len() > 40 + || !d.name.bytes().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_') + || tool_name(pkg.id(), &d.name).len() > 64 + || d.description.trim().is_empty() + || d.description.len() > 4096 + { + return Err( + "mcpTools: name must match [a-z0-9_]{1,40}, namespaced name at most 64 bytes, description 1–4096 bytes" + .into(), + ); + } + let text = pkg.read_text(&d.input_schema)?; + if text.len() > 256 << 10 { + return Err("mcpTools: schema exceeds 256 KiB".into()); + } + let schema: serde_json::Value = serde_json::from_str(&text).map_err(|e| format!("mcpTools: {e}"))?; + if schema.get("type").and_then(serde_json::Value::as_str) != Some("object") { + return Err("mcpTools: inputSchema must describe an object".into()); + } + // No HTTP or filesystem resolver features: schemas must be self-contained. + jsonschema::validator_for(&schema).map_err(|e| format!("mcpTools: invalid JSON Schema: {e}"))?; + let mut out = UiDecl::new( + d.name.clone(), + d.description.clone(), + serde_json::json!({ + "name": d.name, "description": d.description, "inputSchema": schema, "readOnly": d.read_only, "destructive": d.destructive, + }), + ); + out.files.push(d.input_schema); + Ok(out) +} + +impl PluginHost { + /// Live declarations only, refreshed on every list/call. Ambiguous normalized names fail closed. + pub fn mcp_tools(&self, root: &std::path::Path) -> Vec<(String, String, ide_mcp::Tool)> { + let mut tools = vec![]; + for item in self.ui.items(POINT, None) { + let Ok(plugin) = self.get(&item.plugin_id) else { continue }; + if !matches!(plugin.status(), PluginStatus::Active | PluginStatus::Inactive) + || plugin.dev_root.as_deref().is_some_and(|r| r != root) + { + continue; + } + let d = &item.decl; + let Some(name) = d["name"].as_str() else { continue }; + tools.push(( + plugin.id().to_owned(), + name.to_owned(), + ide_mcp::Tool { + name: tool_name(plugin.id(), name), + description: format!( + "{} (plugin: {} [{}])", + d["description"].as_str().unwrap_or_default(), + plugin.manifest().name, + plugin.id() + ), + input_schema: d["inputSchema"].clone(), + output_schema: None, + read_only: d["readOnly"].as_bool().unwrap_or(false), + destructive: d["destructive"].as_bool().unwrap_or(true), + }, + )); + } + let mut counts = std::collections::HashMap::new(); + for (_, _, tool) in &tools { + *counts.entry(tool.name.clone()).or_insert(0) += 1; + } + tools.retain(|(_, _, tool)| counts[&tool.name] == 1); + tools + } + pub fn call_mcp_tool( + &self, + plugin: &str, + tool: &str, + arguments: &str, + invocation: Invocation, + cancel: &ide_core::CancellationToken, + ) -> IdeResult { + if arguments.len() > 1 << 20 { + return Err(IdeError::invalid("MCP arguments exceed 1 MiB")); + } + let root = + invocation.workspace_root.as_ref().ok_or_else(|| IdeError::invalid("MCP requires a project window"))?; + let (_, _, declared) = self + .mcp_tools(std::path::Path::new(root)) + .into_iter() + .find(|(p, n, _)| p == plugin && n == tool) + .ok_or_else(|| IdeError::not_found("plugin MCP tool is no longer available"))?; + let args: serde_json::Value = serde_json::from_str(arguments).map_err(|e| IdeError::invalid(e.to_string()))?; + let validator = + jsonschema::validator_for(&declared.input_schema).map_err(|e| IdeError::invalid(e.to_string()))?; + if !validator.is_valid(&args) { + return Err(IdeError::invalid("MCP arguments do not match inputSchema")); + } + let (tool, arguments) = (tool.to_owned(), arguments.to_owned()); + self.code_call_cancellable(plugin, invocation, "calling the MCP tool", Some(cancel), move |rt, inv| { + rt.mcp_call(&tool, &arguments, inv) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn validates_names_schema_files_versions_and_schema_keywords_at_install() { + let tmp = tempfile::tempdir().unwrap(); + let pkg = |api: &str| { + Package::new( + ide_extension::Manifest::parse(&format!( + "id='test.tools'\nname='Tools'\nversion='1.0.0'\nengines.openidea='{api}'\nwasm='plugin.wasm'" + )) + .unwrap(), + tmp.path().into(), + ) + }; + let decl = |name: &str, path: &str| { + toml::from_str::(&format!( + "name='{name}'\ndescription='Tool'\ninputSchema='{path}'\nreadOnly=true\ndestructive=false" + )) + .unwrap() + }; + std::fs::write( + tmp.path().join("args.json"), + r##"{"type":"object","$defs":{"text":{"type":"string"}},"properties":{"text":{"$ref":"#/$defs/text"}}}"##, + ) + .unwrap(); + assert_eq!(declaration(&pkg("^1.4"), decl("echo", "args.json")).unwrap().files.len(), 1); + assert!(declaration(&pkg("^1.3"), decl("echo", "args.json")).is_err()); + for name in ["", "Bad", "bad-name", &"x".repeat(41)] { + assert!(declaration(&pkg("^1.4"), decl(name, "args.json")).is_err()); + } + assert!(declaration(&pkg("^1.4"), decl("echo", "../args.json")).is_err()); + for schema in [ + r#"{"type":"nonsense"}"#, + r#"{"type":"object","required":42}"#, + r#"{"type":"object","properties":{"x":{"$ref":"file:///private/secret"}}}"#, + r#"{"type":"object","properties":{"x":{"$ref":"https://example.invalid/schema"}}}"#, + ] { + std::fs::write(tmp.path().join("args.json"), schema).unwrap(); + assert!(declaration(&pkg("^1.4"), decl("echo", "args.json")).is_err(), "{schema}"); + } + assert_eq!(tool_name("a.b-c", "echo"), "a_b_c__echo"); + } +} diff --git a/crates/ide-host/src/plugins/mod.rs b/crates/ide-host/src/plugins/mod.rs index be00ee3c1..1b339bbce 100644 --- a/crates/ide-host/src/plugins/mod.rs +++ b/crates/ide-host/src/plugins/mod.rs @@ -16,10 +16,13 @@ //! projects) and folders linked with `openidea-plugin link` (`links.json`). mod api; +mod auth; pub mod commit; +mod database; pub(crate) mod diagnostics; pub mod endpoints; mod event_paths; +mod execution; mod files_changed; mod hover; mod icon_extensions; @@ -27,17 +30,23 @@ mod icon_patterns; mod icon_settings; mod icons; pub mod index_ops; +pub(crate) mod injections; mod inlay_hints; mod install; pub mod intellij; +mod ipc; +pub mod language_features; mod market; +mod mcp; pub mod navigation; mod points; mod providers; pub mod remote; pub mod remote_host; mod run_detectors; +pub mod run_targets; mod semantic_tokens; +mod snippets; mod theme; mod types; pub mod ui_points; @@ -410,6 +419,11 @@ struct PendingUpdate { } impl PluginHost { + /// Current decoration snapshots for windows opening after a plugin published them. + pub fn decoration_sets(&self) -> std::collections::BTreeMap> { + self.shared.decoration_sets() + } + /// Creates the host; call [`load_all`](Self::load_all) to load installed plugins. pub fn new(plugins_dir: PathBuf, sink: Arc, limits: Limits) -> IdeResult { std::fs::create_dir_all(&plugins_dir)?; @@ -653,6 +667,7 @@ impl PluginHost { /// Unloads the project plugins of `root` (the project became untrusted or closed). pub fn restrict_project(&self, root: &Path) { + self.shared.withdraw_run_targets(None, Some(root)); let _guard = self.lifecycle.lock(); let ids: Vec<_> = self .plugins @@ -1080,7 +1095,9 @@ impl PluginHost { ledger.set(&subject, &cap, granted); if granted && let Capability::Process(program) = &cap - && let Some(bin) = self.resolve_binary(program) + && let Some(bin) = ledger + .program_choice(&subject, &cap) + .map_or_else(|| self.resolve_binary(program), |path| ledger::approve_program(path).ok()) { ledger.set_binary(&subject, &cap, bin); } @@ -1097,14 +1114,74 @@ impl PluginHost { /// The program a `process:` grant allows, checked against the file /// approved for it. Errors ask the user to confirm again. pub fn program_for(&self, id: &str, program: &str) -> IdeResult { + self.program_for_project(id, program, None) + } + + /// Resolves the approved executable for the invoking project, verifying its pinned content. + pub fn program_for_project(&self, id: &str, program: &str, root: Option<&Path>) -> IdeResult { let plugin = self.get(id)?; - let cap = Capability::Process(program.to_owned()); self.ledger .lock() - .check_binary(&plugin.subject(), &cap) + .check_binary_in_project(&plugin.subject(), &Capability::Process(program.into()), root) .map_err(|e| IdeError::new(ide_core::error::ErrorKind::PermissionDenied, e)) } + /// Records a user-approved executable choice; removing it revalidates the fallback or revokes the grant. + pub fn choose_program( + &self, + id: &str, + capability: &str, + root: Option<&Path>, + path: Option<&Path>, + ) -> IdeResult { + let _guard = self.lifecycle.lock(); + let plugin = self.get(id)?; + self.check_not_uninstalling(&plugin)?; + let cap: Capability = capability.parse().map_err(IdeError::invalid)?; + let Capability::Process(program) = &cap else { + return Err(IdeError::invalid("expected a process capability")); + }; + if !plugin.manifest().declares(&cap) && !plugin.meta.lock().declared_extra.contains(&cap) { + return Err(IdeError::invalid("undeclared process capability")); + } + let binary = if let Some(path) = path { + Some(ledger::approve_program(path).map_err(IdeError::invalid)?) + } else if root.is_none() { + self.resolve_binary(program) + } else { + None + }; + { + let mut ledger = self.ledger.lock(); + let subject = plugin.subject(); + if ledger.get(&subject).is_none() { + return Err(IdeError::invalid("confirm this plugin's permissions first")); + } + if let Some(root) = root { + ledger.set_project_binary(&subject, &cap, root, binary); + if path.is_some() { + ledger.set(&subject, &cap, true); + } else if ledger.check_binary(&subject, &cap).is_err() { + ledger.set(&subject, &cap, false); + } + } else if let Some(binary) = binary { + ledger.set_program_choice(&subject, &cap, path); + ledger.set(&subject, &cap, true); + ledger.set_binary(&subject, &cap, binary); + } else { + ledger.set_program_choice(&subject, &cap, None); + ledger.set(&subject, &cap, false); + } + self.save_ledger(&ledger); + } + self.stop(&plugin); + if !self.state.lock().disabled.contains(id) { + self.start(&plugin); + } + self.changed(); + self.info_of(id) + } + fn check_uninstall_pending(&self, id: &str) -> IdeResult<()> { if install::uninstall_pending(&self.plugins_dir, id)? { return Err(IdeError::unavailable(std::io::Error::from(std::io::ErrorKind::ResourceBusy).to_string())); @@ -1151,6 +1228,7 @@ impl PluginHost { } self.stop(&plugin); let cleanup = || -> std::io::Result<()> { + self.remove_database_sources(id).map_err(|e| std::io::Error::other(e.message))?; install::check_cleanup_parent(&self.plugins_dir, &self.shared.data_dir.join(id))?; install::check_cleanup_parent(&self.plugins_dir, plugin.dir())?; install::check_not_symlink(&self.shared.data_dir.join(id))?; @@ -1342,6 +1420,17 @@ impl PluginHost { invocation: Invocation, what: &str, f: impl Fn(&mut Runtime, Invocation) -> Result + Clone + Send + 'static, + ) -> IdeResult { + self.code_call_cancellable(id, invocation, what, None, f) + } + + fn code_call_cancellable( + &self, + id: &str, + invocation: Invocation, + what: &str, + cancel: Option<&ide_core::CancellationToken>, + f: impl Fn(&mut Runtime, Invocation) -> Result + Clone + Send + 'static, ) -> IdeResult { let plugin = self.get(id)?; let client = invocation.client.clone(); @@ -1383,7 +1472,14 @@ impl PluginHost { } let started = Instant::now(); let (call, inv) = (f.clone(), invocation.clone()); - let result = actor.call(None, move |rt| call(rt, inv)); + let result = match cancel { + Some(cancel) => actor.call_cancellable(cancel, Duration::from_millis(50), move |rt| call(rt, inv)), + None => actor.call(None, move |rt| call(rt, inv)), + }; + if matches!(result, Err(ActorError::Cancelled)) { + self.reap(&plugin, &actor, client.as_deref()); + return Err(IdeError::cancelled()); + } if !matches!(result, Err(ActorError::Stopped | ActorError::SoftTimeout | ActorError::Cancelled)) { let crashed = matches!(result, Err(ActorError::Call(CallError::Trap(_)))); // Cancelling or a missing grant is not the plugin failing. @@ -1457,7 +1553,12 @@ impl PluginHost { /// Plugins' `runDetectors`, for [`ide_run::set_external_detection`]. pub fn run_detection(&self) -> Arc { - Arc::new(run_detectors::PluginRunDetection { store: self.run_detectors.clone(), pins: self.pins.clone() }) + Arc::new(run_detectors::PluginRunDetection { + store: self.run_detectors.clone(), + dynamic: self.shared.run_configurations.clone(), + pins: self.pins.clone(), + project_pins: self.shared.project_pins.clone(), + }) } /// The syntax of plugin languages, for [`ide_language::set_external_syntax`]. @@ -1513,6 +1614,10 @@ impl PluginHost { } /// Plugin context keys in effect for window `client`: global keys overlaid with its own. + pub fn status_snapshot(&self, client: &str) -> Vec { + self.shared.status_snapshot(client) + } + pub fn context_keys(&self, client: Option<&str>) -> Vec { self.shared.context_keys(client) } @@ -1543,6 +1648,10 @@ impl PluginHost { /// A window answered a plugin's `CommitRequested`; false when the call no /// longer waits. + pub fn answer_command(&self, client: &str, request: &str, answer: CommandAnswer) -> bool { + request.parse().is_ok_and(|id| self.shared.answer_command(client, id, answer)) + } + pub fn answer_commit(&self, request_id: &str, answer: CommitAnswer) -> bool { request_id.parse().is_ok_and(|id| self.shared.answer_commit(id, answer)) } @@ -1632,6 +1741,30 @@ impl PluginHost { /// Queues workbench events from window `client` for the plugins that want /// them; [`flush_events`](Self::flush_events) delivers them in batches. + pub fn post_window_events(&self, client: &str, events: Vec, editor: Option) { + let mut editors = self.shared.event_editors.write(); + if let Some(editor) = editor { + editors.insert(client.into(), editor.context()); + } else { + editors.remove(client); + } + drop(editors); + self.post_events( + Some(client), + events + .into_iter() + .filter(|e| { + !matches!( + e, + WorkbenchEvent::RunExited(_) + | WorkbenchEvent::TerminalExited(_) + | WorkbenchEvent::ContentTabAction(_) + ) + }) + .collect(), + ); + } + pub fn post_events(&self, client: Option<&str>, events: Vec) { self.post_events_from(client, events, None); } @@ -1661,7 +1794,13 @@ impl PluginHost { .as_ref() .is_none_or(|root| client.and_then(|c| self.shared.project_of(c)).as_ref() == Some(root)) }) - .cloned() + .filter_map(|e| match e { + WorkbenchEvent::SettingsChanged(_) => { + let keys = self.shared.changed_settings(&p.pkg, client); + (!keys.is_empty()).then_some(WorkbenchEvent::SettingsChanged(keys)) + } + _ => Some(e.clone()), + }) .collect(); let wanted = Self::filter_path_events(&self.shared, &p, client, &wanted, &subs); if wanted.is_empty() { @@ -1682,7 +1821,15 @@ impl PluginHost { let excess = queue.events.len().saturating_sub(MAX_QUEUED_EVENTS); let mut remaining = excess; queue.events.retain(|event| { - if remaining > 0 && !matches!(event, WorkbenchEvent::FilesChanged(_)) { + if remaining > 0 + && !matches!( + event, + WorkbenchEvent::FilesChanged(_) + | WorkbenchEvent::RunExited(_) + | WorkbenchEvent::TerminalExited(_) + | WorkbenchEvent::ContentTabAction(_) + ) + { remaining -= 1; false } else { @@ -1809,9 +1956,10 @@ impl PluginHost { let invocation = Invocation { workspace_root: client.as_deref().and_then(|c| self.shared.project_of(c)), client: client.clone(), - editor: None, + editor: client.as_deref().and_then(|c| self.shared.event_editors.read().get(c).cloned()), selected_paths: Vec::new(), global_settings: false, + command_chain: Vec::new(), }; let kinds: BTreeSet = queue.events.iter().map(WorkbenchEvent::kind).collect(); let filesystem = kinds.contains(&EventKind::FilesChanged); @@ -1823,6 +1971,9 @@ impl PluginHost { if batches.running.as_ref() == Some(&kinds) && !kinds.contains(&EventKind::Scheduled) && !kinds.contains(&EventKind::FilesChanged) + && !kinds.iter().any(|k| { + matches!(k, EventKind::RunExited | EventKind::TerminalExited | EventKind::ContentTabAction) + }) { actor.cancel_soft(); } @@ -2599,8 +2750,21 @@ impl PluginHost { /// pick up a change on their next lookup. fn refresh_pins(&self, ledger: &Ledger) { let mut pins = HashMap::new(); + let mut project_pins = HashMap::new(); for plugin in self.plugins.read().values() { let subject = plugin.subject(); + if let Some(grant) = ledger.get(&subject) { + for (root, binaries) in &grant.project_binaries { + for (cap, bin) in binaries { + if grant.granted.contains(cap) { + project_pins.insert( + (plugin.id().to_owned(), cap.clone(), root.clone()), + ProgramPin { path: bin.path.clone(), hash: bin.hash.clone() }, + ); + } + } + } + } let declared = plugin.manifest().capabilities.all().cloned().chain(plugin.meta.lock().declared_extra.clone()); for cap in declared.filter(|c| matches!(c, Capability::Process(_))) { @@ -2612,6 +2776,7 @@ impl PluginHost { } } } + *self.shared.project_pins.write() = project_pins; let mut current = self.pins.write(); if *current != pins { *current = pins; @@ -2665,12 +2830,26 @@ impl PluginHost { fn record_consent(&self, request: &Request<'_>, consent: &Consent) { let accepted: Vec = request.optional.iter().filter(|c| consent.accepts(c)).cloned().collect(); + let choices: BTreeMap<_, _> = { + let ledger = self.ledger.lock(); + request + .required + .iter() + .chain(&accepted) + .filter_map(|cap| { + ledger.program_choice(&request.subject, cap).map(|p| (cap.to_string(), p.to_path_buf())) + }) + .collect() + }; let binaries: BTreeMap = request .required .iter() .chain(&accepted) .filter_map(|c| match c { - Capability::Process(program) => self.resolve_binary(program).map(|b| (c.to_string(), b)), + Capability::Process(program) => choices + .get(&c.to_string()) + .map_or_else(|| self.resolve_binary(program), |path| ledger::approve_program(path).ok()) + .map(|b| (c.to_string(), b)), _ => None, }) .collect(); diff --git a/crates/ide-host/src/plugins/navigation.rs b/crates/ide-host/src/plugins/navigation.rs index 070c1f7d6..9e8449c28 100644 --- a/crates/ide-host/src/plugins/navigation.rs +++ b/crates/ide-host/src/plugins/navigation.rs @@ -197,6 +197,7 @@ impl PluginHost { /// navigation_at. Call off the UI thread, in small cancellable batches. pub fn index_navigation(&self, root: &Path, snapshot: Snapshot<'_>, invocation: Invocation) -> IdeResult<()> { let Snapshot { path, version, text, symbols, scope } = snapshot; + let path = dunce::simplified(path); if !(self.project_policy.read())(root) { return Err(IdeError::unavailable("project is not trusted")); } diff --git a/crates/ide-host/src/plugins/points.rs b/crates/ide-host/src/plugins/points.rs index 13ed6d29f..4575de514 100644 --- a/crates/ide-host/src/plugins/points.rs +++ b/crates/ide-host/src/plugins/points.rs @@ -20,15 +20,23 @@ pub struct CommandDecl { pub id: String, pub title: String, #[serde(default, skip_serializing_if = "Option::is_none")] + pub icon: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub category: Option, /// Enablement, evaluated by the client against context keys (`editorLang == rust`). #[serde(default, skip_serializing_if = "Option::is_none")] pub when: Option, } -fn command(_pkg: &Package, decl: toml::Value) -> Result { +fn command(pkg: &Package, decl: toml::Value) -> Result { let c: CommandDecl = decode(COMMANDS, decl)?; validate_item_id(&c.id, "command id")?; + if let Some(icon) = &c.icon { + pkg.manifest.needs_api("contributes.commands.icon", 4)?; + if icon.is_empty() || icon.len() > 256 { + return Err("command icon: expected 1–256 bytes".into()); + } + } if c.title.trim().is_empty() { return Err(format!("command {:?} has an empty title", c.id)); } @@ -40,7 +48,14 @@ fn command(_pkg: &Package, decl: toml::Value) -> Result { } let c = CommandDecl { category: c.category.filter(|x| !x.trim().is_empty()), ..c }; let json = serde_json::to_value(&c).map_err(|e| e.to_string())?; - Ok(UiDecl::new(c.id.clone(), c.title, json)) + let mut out = UiDecl::new(c.id.clone(), c.title, json); + if let Some(icon) = c.icon.filter(|s| s.ends_with(".svg")) { + let path = std::path::PathBuf::from(icon); + let svg = super::icons::read_icon(pkg, &path, super::icons::MAX_SVG)?; + out.files.push(path); + out.assets.push((format!("commandIcon/{}", c.id), svg)); + } + Ok(out) } /// Registers every first-party extension point. @@ -60,6 +75,13 @@ pub fn register_builtin_points( registry.register(Arc::new(super::providers::ProvidersPoint::new(providers.clone())))?; registry.register(Arc::new(UiPoint::new(super::providers::SECRETS_POINT, ui.clone(), super::providers::secret)))?; registry.register(Arc::new(UiPoint::new(super::endpoints::POINT, ui.clone(), super::endpoints::declaration)))?; + registry.register(Arc::new(UiPoint::new( + "runConfigurations", + ui.clone(), + super::execution::configurations_declaration, + )))?; + registry.register(Arc::new(UiPoint::new("snippets", ui.clone(), super::snippets::declaration)))?; + registry.register(Arc::new(UiPoint::new(super::mcp::POINT, ui.clone(), super::mcp::declaration)))?; use super::ui_points as p; registry.register(Arc::new(UiPoint::new(p::MENUS, ui.clone(), p::menu)))?; registry.register(Arc::new(UiPoint::new(p::KEYMAPS, ui.clone(), p::keymap)))?; diff --git a/crates/ide-host/src/plugins/providers.rs b/crates/ide-host/src/plugins/providers.rs index ad1d590f9..10a8ebf27 100644 --- a/crates/ide-host/src/plugins/providers.rs +++ b/crates/ide-host/src/plugins/providers.rs @@ -42,6 +42,19 @@ pub enum ProviderKind { Hover, InlayHints, Diagnostics, + Formatting, + SignatureHelp, + DocumentHighlights, + DocumentLinks, + CodeLenses, + SelectionRanges, + Folding, + Colors, + DocumentSymbols, + Implementation, + TypeDefinition, + GutterActions, + Injections, } #[derive(Clone, Debug, Deserialize)] @@ -94,9 +107,30 @@ impl Providers { } pub(super) fn diagnostic_generations(&self, language: &str) -> HashMap { - self.matching(ProviderKind::Diagnostics, language) + if self.any(ProviderKind::Injections) { + let mut out = HashMap::new(); + for (seq, plugin, kinds, _) in self.items.read().iter() { + if kinds.contains(&ProviderKind::Diagnostics) { + out.entry(plugin.clone()).or_insert(*seq); + } + } + out + } else { + self.matching(ProviderKind::Diagnostics, language) + } } + pub(super) fn injection_generations(&self, language: &str) -> HashMap { + self.matching(ProviderKind::Injections, language) + } + pub(super) fn formatter_declarations(&self) -> Vec<(String, Vec)> { + self.items + .read() + .iter() + .filter(|(_, _, k, _)| k.contains(&ProviderKind::Formatting)) + .map(|(_, p, _, l)| (p.clone(), l.clone())) + .collect() + } /// Whether any plugin provides `kind` (for any language). pub fn any(&self, kind: ProviderKind) -> bool { self.items.read().iter().any(|(_, _, kinds, _)| kinds.contains(&kind)) @@ -142,6 +176,26 @@ impl ExtensionPoint for ProvidersPoint { if d.kinds.contains(&ProviderKind::InlayHints) { pkg.manifest.needs_api("contributes.providers: inlay-hints", 3)?; } + if d.kinds.iter().any(|k| { + matches!( + k, + ProviderKind::Formatting + | ProviderKind::SignatureHelp + | ProviderKind::DocumentHighlights + | ProviderKind::DocumentLinks + | ProviderKind::CodeLenses + | ProviderKind::SelectionRanges + | ProviderKind::Folding + | ProviderKind::Colors + | ProviderKind::DocumentSymbols + | ProviderKind::Implementation + | ProviderKind::TypeDefinition + | ProviderKind::GutterActions + | ProviderKind::Injections + ) + }) { + pkg.manifest.needs_api("contributes.providers: language features and injections", 4)?; + } for l in &d.languages { if l != "*" && (l.is_empty() @@ -211,7 +265,7 @@ pub fn set_editor_providers(host: Option<&Arc>) { /// The plugin host whose providers take part, when any declares `kind`. pub(crate) fn editor_providers(kind: ProviderKind) -> Option> { let host = editor_plugin_host()?; - host.providers.any(kind).then_some(host) + (host.providers.any(kind) || host.providers.any(ProviderKind::Injections)).then_some(host) } pub(crate) fn editor_plugin_host() -> Option> { @@ -236,15 +290,46 @@ impl PluginHost { /// declares `kind` for `language`, in plugin order, leaving out those that /// failed or were late. Each plugin runs on its own thread, so the calls /// run at the same time: the wait is the slowest plugin's, not the sum. - fn edit_calls( + pub(super) fn edit_calls( &self, kind: ProviderKind, language: &str, f: impl FnOnce(&mut Runtime, Duration) -> Result + Clone + Send + 'static, + ) -> Vec<(Arc, R)> { + self.edit_calls_plugins(self.provider_plugins(kind, language), f) + } + + pub(super) fn document_calls( + &self, + kind: ProviderKind, + doc: &ProviderDocument, + f: impl FnOnce(&mut Runtime, Duration) -> Result + Clone + Send + 'static, + ) -> Vec<(Arc, R)> { + self.document_calls_for(kind, doc, None, f) + } + pub(super) fn document_calls_for( + &self, + kind: ProviderKind, + doc: &ProviderDocument, + owner: Option<&str>, + f: impl FnOnce(&mut Runtime, Duration) -> Result + Clone + Send + 'static, + ) -> Vec<(Arc, R)> { + let plugins = self + .provider_plugins(kind, &doc.language) + .into_iter() + .filter(|p| { + p.dev_root.as_ref().is_none_or(|r| doc.path.starts_with(r)) && owner.is_none_or(|id| p.id() == id) + }) + .collect(); + self.edit_calls_plugins(plugins, f) + } + fn edit_calls_plugins( + &self, + plugins: Vec>, + f: impl FnOnce(&mut Runtime, Duration) -> Result + Clone + Send + 'static, ) -> Vec<(Arc, R)> { let (soft, hard) = (self.limits.edit_soft, self.limits.edit_hard); - let queued: Vec<_> = self - .provider_plugins(kind, language) + let queued: Vec<_> = plugins .into_iter() .filter_map(|plugin| { let (actor, activated) = self.ensure_active(&plugin, None).ok()?; @@ -303,13 +388,24 @@ impl PluginHost { at: ide_core::Pos, trigger: Option<&str>, ) -> Vec<(String, CompletionItem)> { - let (d, t) = (Arc::new(doc.clone()), trigger.map(str::to_owned)); - self.edit_calls(ProviderKind::Completion, &doc.language, move |rt, timeout| { - rt.provide_completion(&d, at, t.as_deref(), timeout) - }) - .into_iter() - .flat_map(|(p, items)| items.into_iter().map(move |i| (p.id().to_owned(), i))) - .collect() + self.feature_documents(doc, Some(at)) + .into_iter() + .flat_map(|(doc, bounds)| { + let (d, t) = (Arc::new(doc.clone()), trigger.map(str::to_owned)); + self.document_calls(ProviderKind::Completion, &doc, move |rt, timeout| { + rt.provide_completion(&d, at, t.as_deref(), timeout) + }) + .into_iter() + .flat_map(|(p, items)| items.into_iter().map(move |i| (p.id().to_owned(), i))) + .filter_map(move |(p, mut i)| { + if i.replace.is_some_and(|r| !bounds.contains(r)) { + return None; + } + i.additional_edits.retain(|e| bounds.contains(e.range)); + Some((p, i)) + }) + }) + .collect() } pub fn provide_code_actions( @@ -391,10 +487,20 @@ impl PluginHost { project: Option<&std::path::Path>, language: &str, ) -> Vec> { - self.provider_plugins(ProviderKind::Diagnostics, language) - .into_iter() - .filter(|p| p.dev_root.is_none() || p.dev_root.as_deref() == project) - .collect() + let plugins = if self.providers.any(ProviderKind::Injections) { + self.providers + .diagnostic_generations(language) + .keys() + .filter_map(|id| self.get(id).ok()) + .filter(|p| { + let m = p.meta.lock(); + matches!(m.status, PluginStatus::Active | PluginStatus::Inactive) && m.pending.is_empty() + }) + .collect() + } else { + self.provider_plugins(ProviderKind::Diagnostics, language) + }; + plugins.into_iter().filter(|p| p.dev_root.is_none() || p.dev_root.as_deref() == project).collect() } pub(super) fn provide_project_diagnostics( @@ -402,11 +508,35 @@ impl PluginHost { project: Option<&std::path::Path>, doc: &ProviderDocument, cancel: &CancellationToken, + ) -> Vec<(String, Vec)> { + let mut out = std::collections::BTreeMap::>::new(); + for (d, bounds) in self.feature_documents(doc, None) { + if cancel.is_cancelled() { + break; + } + for (id, items) in self.provide_document_diagnostics(project, &d, cancel) { + out.entry(id).or_default().extend(items.into_iter().filter(|i| bounds.contains(i.range))); + } + } + out.into_iter() + .map(|(id, mut items)| { + items.truncate(5000); + (id, items) + }) + .collect() + } + + fn provide_document_diagnostics( + &self, + project: Option<&std::path::Path>, + doc: &ProviderDocument, + cancel: &CancellationToken, ) -> Vec<(String, Vec)> { std::thread::scope(|scope| { let calls: Vec<_> = self - .diagnostic_plugins(project, &doc.language) + .provider_plugins(ProviderKind::Diagnostics, &doc.language) .into_iter() + .filter(|p| p.dev_root.is_none() || p.dev_root.as_deref() == project) .map(|p| { scope.spawn(move || { if cancel.is_cancelled() { @@ -432,10 +562,15 @@ impl PluginHost { } pub fn provide_hover(&self, doc: &ProviderDocument, at: ide_core::Pos) -> Vec { - let d = Arc::new(doc.clone()); - self.edit_calls(ProviderKind::Hover, &doc.language, move |rt, timeout| rt.provide_hover(&d, at, timeout)) + self.feature_documents(doc, Some(at)) .into_iter() - .filter_map(|(_, hover)| hover) + .flat_map(|(doc, bounds)| { + let d = Arc::new(doc.clone()); + self.document_calls(ProviderKind::Hover, &doc, move |rt, timeout| rt.provide_hover(&d, at, timeout)) + .into_iter() + .filter_map(|(_, h)| h) + .filter(move |h| h.range.is_none_or(|r| bounds.contains(r))) + }) .collect() } diff --git a/crates/ide-host/src/plugins/remote_host.rs b/crates/ide-host/src/plugins/remote_host.rs index 74ee904dd..de1f3775c 100644 --- a/crates/ide-host/src/plugins/remote_host.rs +++ b/crates/ide-host/src/plugins/remote_host.rs @@ -22,9 +22,7 @@ //! keychain and are asked for with a `plugin_secret` request //! ([`SecretRequest`]) when a request header needs one (rule 4). -use super::{ - Consent, HostWorkspace, PluginEditor, PluginEvent, PluginHost, PluginSource, PluginStatusItem, SecretStore, -}; +use super::{Consent, HostWorkspace, PluginEditor, PluginEvent, PluginHost, PluginSource, SecretStore}; use crate::rpc::Dispatcher; use base64::Engine as _; use ide_core::{IdeError, IdeResult}; @@ -45,6 +43,32 @@ pub const SYNC_PACKAGES: &str = "plugin_sync_packages"; pub const EVENT_NOTIFICATION: &str = "plugin"; /// Server → client request ([`SecretRequest`] → `Option`). pub const SECRET_REQUEST: &str = "plugin_secret"; +/// Mutations of declared secrets, kept on the desktop. +pub const SECRET_WRITE: &str = "plugin_secret_write"; +/// OAuth listeners bound on the desktop, never the project server. +pub const AUTH_REQUEST: &str = "plugin_auth"; + +#[derive(Clone, Serialize, Deserialize)] +pub struct SecretWrite { + pub plugin: String, + pub name: String, + pub value: Option, +} + +/// A remote plugin's operation on a desktop OAuth listener. +#[derive(Clone, Serialize, Deserialize)] +pub struct AuthRequest { + pub plugin: String, + pub operation: AuthOperation, +} +#[derive(Clone, Serialize, Deserialize)] +pub enum AuthOperation { + Begin { path: String }, + Wait { id: u64, timeout_ms: u32 }, + Close { id: u64 }, + Clear, +} + /// Under the server's data directory. pub const PLUGINS_DIR: &str = "remote-plugins"; const INCOMING_DIR: &str = "remote-plugins.incoming"; @@ -61,6 +85,13 @@ pub struct RemotePackage { pub digest: String, /// Capabilities the user granted on the desktop. pub granted: Vec, + /// Disabled packages remain installed so their persistent project data survives. + #[serde(default = "enabled_by_default")] + pub enabled: bool, +} + +fn enabled_by_default() -> bool { + true } /// One file of a package (`path` relative to the package, `data` base64). @@ -107,8 +138,18 @@ pub fn package_digest(dir: &Path, files: &[PathBuf]) -> IdeResult { pub fn forwarded(event: &PluginEvent) -> bool { matches!( event, - PluginEvent::Notification { .. } + PluginEvent::RunTargetsChanged + | PluginEvent::WindowRequest { .. } + | PluginEvent::ContentTabReply { .. } + | PluginEvent::DecorationsChanged { .. } + | PluginEvent::EditorOpen { .. } + | PluginEvent::EditorRanges { .. } + | PluginEvent::Notification { .. } | PluginEvent::StatusItemChanged { .. } + | PluginEvent::WindowStatusItemChanged { .. } + | PluginEvent::ExecuteCommand { .. } + | PluginEvent::OutputChanged { .. } + | PluginEvent::RevealOutput { .. } | PluginEvent::ContextChanged { .. } | PluginEvent::TaskStarted { .. } | PluginEvent::TaskProgress { .. } @@ -144,12 +185,16 @@ impl PluginHost { plugins .iter() .filter(|p| p.dev_root.is_none() && !p.invalid && p.manifest().wasm.is_some()) - .filter(|p| matches!(p.status(), super::PluginStatus::Active | super::PluginStatus::Inactive)) .filter_map(|p| { let files = self.package_files(p).ok()?; let digest = package_digest(p.dir(), &files).ok()?; let granted = p.meta.lock().granted.iter().map(ToString::to_string).collect(); - Some(RemotePackage { id: p.id().to_owned(), digest, granted }) + Some(RemotePackage { + id: p.id().to_owned(), + digest, + granted, + enabled: matches!(p.status(), super::PluginStatus::Active | super::PluginStatus::Inactive), + }) }) .collect() } @@ -218,8 +263,19 @@ impl SecretStore for ClientSecrets { } } - fn set(&self, _: &str, _: &str, _: Option<&str>) -> Result<(), String> { - Err("secrets are stored on the desktop".into()) + fn set(&self, plugin: &str, name: &str, value: Option<&str>) -> Result<(), String> { + let peer = self.peer.get().ok_or("desktop disconnected")?.clone(); + let request = SecretWrite { plugin: plugin.into(), name: name.into(), value: value.map(str::to_owned) }; + let ask = async { + tokio::time::timeout(SECRET_TIMEOUT, peer.request::<_, Result<(), String>>(SECRET_WRITE, request, None)) + .await + }; + let answer = if tokio::runtime::Handle::try_current().is_ok() { + tokio::task::block_in_place(|| self.runtime.block_on(ask)) + } else { + self.runtime.block_on(ask) + }; + answer.map_err(|_| "desktop timed out".to_owned())?.map_err(|e| e.to_string())? } } @@ -236,8 +292,10 @@ impl RemotePlugins { } }; let host = Arc::new(PluginHost::new(data_dir.join(PLUGINS_DIR), Arc::new(sink), Limits::default())?); + projects.set_plugin_host(&host); host.set_workspace(Some(Arc::new(HostWorkspace(Arc::downgrade(projects))))); - host.set_secret_store(Arc::new(ClientSecrets { peer, runtime: projects.runtime.clone() })); + host.set_secret_store(Arc::new(ClientSecrets { peer: peer.clone(), runtime: projects.runtime.clone() })); + *host.shared.loopbacks.write() = Arc::new(ClientLoopbacks { peer, runtime: projects.runtime.clone() }); let trust = Arc::downgrade(projects); host.set_project_policy(Arc::new(move |root| { trust.upgrade().is_some_and(|h| h.trust.trusted(root).unwrap_or(false)) @@ -285,6 +343,14 @@ impl RemotePlugins { report.failed.push(format!("{}: invalid plugin id", pkg.id)); continue; } + if !pkg.enabled { + if self.host.info_of(&pkg.id).is_ok() + && let Err(e) = self.host.set_enabled(&pkg.id, false) + { + report.failed.push(format!("{}: {e}", pkg.id)); + } + continue; + } // The desktop asked the user, change of signer included. let consent = Consent { optional: pkg.granted.clone(), signer_change: true }; if self.current_digest(&pkg.id).as_deref() == Some(pkg.digest.as_str()) { @@ -320,6 +386,9 @@ impl RemotePlugins { if have != want || info.status == super::PluginStatus::NeedsConsent { self.host.grant(&pkg.id, consent)?; } + if info.status == super::PluginStatus::Disabled { + self.host.set_enabled(&pkg.id, true)?; + } Ok(()) } @@ -380,6 +449,7 @@ impl RemotePlugins { let paths: Option> = c.arg("paths")?; let settings_scope: Option = c.arg("settingsScope")?; let invocation = Invocation { + command_chain: c.arg::>>("commandChain")?.unwrap_or_default(), client: Some(c.window.clone()), workspace_root: c.host.project(&c.window).ok().map(|p| p.root.clone()), editor: editor.map(PluginEditor::context), @@ -398,10 +468,59 @@ impl RemotePlugins { } }); let h = host.clone(); + d.add("plugin_parse_test_results", move |c| { + let h = h.clone(); + async move { + let host = alive(&h)?; + let config = c.arg::("config")?; + let chunk = c.arg("chunk")?; + blocking(move || host.parse_test_results(&c.window, &config, chunk)).await + } + }); + let h = host.clone(); + d.add("plugin_terminal_session_close", move |c| { + let h = h.clone(); + async move { alive(&h)?.close_terminal_session(&c.window, &c.arg::("operation")?) } + }); + let h = host.clone(); + d.add("plugin_terminal_session_spawn", move |c| { + let h = h.clone(); + async move { + let plugins = alive(&h)?; + let operation: String = c.arg("operation")?; + let cols = c.arg("cols")?; + let rows = c.arg("rows")?; + let output = c.channel("onOutput")?; + blocking(move || plugins.spawn_terminal_session(&c.host, &c.window, &operation, cols, rows, output)) + .await + } + }); + let h = host.clone(); + d.add("plugin_prepare_debug_run", move |c| { + let h = h.clone(); + async move { + let plugins = alive(&h)?; + let config = c.arg("config")?; + blocking(move || plugins.prepare_debug_run(&c.window, config)).await + } + }); + let h = host.clone(); + d.add("plugin_finish_operation", move |c| { + let h = h.clone(); + async move { + Ok(alive(&h)?.finish_operation(&c.window, &c.arg::("id")?, c.arg("status")?, c.arg("stopped")?)) + } + }); + let h = host.clone(); + d.add("plugin_content_tab_action", move |c| { + let h = h.clone(); + async move { alive(&h)?.content_tab_action(&c.window, &c.arg::("pluginId")?, c.arg("action")?) } + }); + let h = host.clone(); d.add("plugin_post_events", move |c| { let h = h.clone(); async move { - alive(&h)?.post_events(Some(&c.window), c.arg("events")?); + alive(&h)?.post_window_events(&c.window, c.arg("events")?, c.arg("editor")?); Ok(()) } }); @@ -411,15 +530,9 @@ impl RemotePlugins { async move { Ok(alive(&h)?.event_kinds().into_iter().collect::>()) } }); let h = host.clone(); - d.add("plugin_status_items", move |_| { + d.add("plugin_status_items", move |c| { let h = h.clone(); - async move { - Ok(alive(&h)? - .status_items() - .into_iter() - .map(|(plugin_id, id, item)| PluginStatusItem { plugin_id, id, item }) - .collect::>()) - } + async move { Ok(alive(&h)?.status_snapshot(&c.window)) } }); let h = host.clone(); d.add("plugin_context_keys", move |c| { @@ -437,6 +550,43 @@ impl RemotePlugins { async move { Ok(alive(&h)?.answer_prompt(&c.arg::("promptId")?, c.arg("answer")?)) } }); let h = host.clone(); + d.add("plugin_choose_program", move |c| { + let h = h.clone(); + async move { + let host = alive(&h)?; + let id: String = c.arg("pluginId")?; + let cap: String = c.arg("capability")?; + let scope: SettingsScope = c.arg("scope")?; + let path: Option = c.arg("path")?; + let root = match scope { + SettingsScope::Global => None, + SettingsScope::Project => Some(c.host.project(&c.window)?.root.clone()), + }; + blocking(move || host.choose_program(&id, &cap, root.as_deref(), path.as_deref())).await + } + }); + let h = host.clone(); + d.add("plugin_program", move |c| { + let h = h.clone(); + async move { + let host = alive(&h)?; + let id: String = c.arg("pluginId")?; + let program: String = c.arg("program")?; + let scope: SettingsScope = c.arg("scope")?; + let root = match scope { + SettingsScope::Global => None, + SettingsScope::Project => Some(c.host.project(&c.window)?.root.clone()), + }; + blocking(move || host.program_for_project(&id, &program, root.as_deref()).map(ide_core::IpcPath::from)) + .await + } + }); + let h = host.clone(); + d.add("plugin_answer_command", move |c| { + let h = h.clone(); + async move { Ok(alive(&h)?.answer_command(&c.window, &c.arg::("requestId")?, c.arg("answer")?)) } + }); + let h = host.clone(); d.add("plugin_answer_commit", move |c| { let h = h.clone(); async move { Ok(alive(&h)?.answer_commit(&c.arg::("requestId")?, c.arg("answer")?)) } @@ -456,6 +606,22 @@ impl RemotePlugins { .await } }); + d.add("mcp_set_open_editors", |c| async move { + let paths: Vec = c.arg("paths")?; + c.host.project(&c.window)?; + c.host.mcp_reports.set_open_editors(&c.window, paths.into_iter().map(PathBuf::from).collect()); + Ok(()) + }); + d.add("mcp_set_focused_editor", |c| async move { + c.host.project(&c.window)?; + c.host.mcp_reports.set_focused_editor(&c.window, c.arg("editor")?, c.arg("focused")?); + Ok(()) + }); + let h = host.clone(); + d.add("plugin_decoration_sets", move |_c| { + let h = h.clone(); + async move { Ok(alive(&h)?.decoration_sets()) } + }); let h = host.clone(); d.add("plugin_set_active_file", move |c| { let h = h.clone(); @@ -577,6 +743,21 @@ impl RemotePlugins { let h = h.clone(); async move { super::index_ops::plugin_endpoints(&c.host, alive(&h)?, &c.window, c.arg("pluginId")?).await } }); + let h = host.clone(); + d.add("plugin_run_targets", move |c| { + let h = h.clone(); + async move { super::run_targets::list(&c.host, alive(&h)?.as_ref(), &c.window) } + }); + let h = host.clone(); + d.add("plugin_answer_run_target", move |c| { + let h = h.clone(); + async move { + let plugins = alive(&h)?; + let id: String = c.arg("id")?; + let use_target = c.arg("useTarget")?; + blocking(move || super::run_targets::answer(&c.host, &plugins, &c.window, &id, use_target)).await + } + }); d.add("plugin_line_markers", |c| async move { crate::ops::lsp::plugin_line_markers(&c.host, &c.window, c.arg("doc")?).await }); @@ -621,6 +802,123 @@ fn safe_relative(path: &str) -> IdeResult { Ok(p.to_path_buf()) } +struct ClientLoopbacks { + peer: Arc>, + runtime: tokio::runtime::Handle, +} +impl ClientLoopbacks { + fn request( + &self, + plugin: &str, + operation: AuthOperation, + cancel: Option, + ) -> Result { + let peer = self.peer.get().ok_or(ide_plugin::HostError::Cancelled)?.clone(); + let timeout = match &operation { + AuthOperation::Wait { timeout_ms, .. } => Duration::from_millis(u64::from(*timeout_ms)) + SECRET_TIMEOUT, + _ => SECRET_TIMEOUT, + }; + let request = AuthRequest { plugin: plugin.into(), operation }; + let ask = async { + tokio::time::timeout( + timeout, + peer.request::<_, Result>(AUTH_REQUEST, request, cancel), + ) + .await + .map_err(|_| ide_plugin::HostError::Failed("desktop OAuth request timed out".into()))? + .map_err(|e| ide_plugin::HostError::Failed(e.to_string()))? + }; + if tokio::runtime::Handle::try_current().is_ok() { + tokio::task::block_in_place(|| self.runtime.block_on(ask)) + } else { + self.runtime.block_on(ask) + } + } +} +impl super::auth::Service for ClientLoopbacks { + fn begin(&self, plugin: &str, _window: &str, path: &str) -> Result<(u64, String), ide_plugin::HostError> { + serde_json::from_value(self.request(plugin, AuthOperation::Begin { path: path.into() }, None)?) + .map_err(|e| ide_plugin::HostError::Failed(e.to_string())) + } + fn wait( + &self, + plugin: &str, + id: u64, + timeout: Duration, + cancel: &ide_core::CancellationToken, + ) -> Result, ide_plugin::HostError> { + let result = self.request( + plugin, + AuthOperation::Wait { id, timeout_ms: timeout.as_millis().min(u128::from(u32::MAX)) as u32 }, + Some(cancel.clone()), + ); + if cancel.is_cancelled() { + self.close(plugin, id); + return Err(ide_plugin::HostError::Cancelled); + } + serde_json::from_value(result?).map_err(|e| ide_plugin::HostError::Failed(e.to_string())) + } + fn close(&self, plugin: &str, id: u64) { + let _ = self.request(plugin, AuthOperation::Close { id }, None); + } + fn clear(&self, plugin: &str) { + let _ = self.request(plugin, AuthOperation::Clear, None); + } + fn forget_window(&self, _: &str) {} +} + +impl PluginHost { + /// A remote instance may mutate only declared secrets with a current grant. + pub fn remote_secret_write(&self, request: SecretWrite) -> Result<(), String> { + let plugin = self.get(&request.plugin).map_err(|e| e.message)?; + if !plugin.meta.lock().granted.iter().any(|g| g.covers(&ide_extension::Capability::Secrets)) { + return Err("secrets capability not granted".into()); + } + if !self.ui.items(super::providers::SECRETS_POINT, Some(plugin.id())).iter().any(|s| s.key == request.name) { + return Err("undeclared secret".into()); + } + if request.value.as_ref().is_some_and(|v| v.len() > 64 << 10) { + return Err("secret value too long".into()); + } + self.shared.secrets.read().set(plugin.id(), &request.name, request.value.as_deref()) + } + /// Serves a connected remote instance using this connection's actual window. + pub fn remote_auth(&self, window: &str, request: AuthRequest) -> Result { + if matches!(&request.operation, AuthOperation::Begin { .. } | AuthOperation::Wait { .. }) { + self.get(&request.plugin).map_err(|e| ide_plugin::HostError::NotFound(e.message))?; + } + let owner = format!("remote:{window}:{}", request.plugin); + let service = self.shared.loopbacks.read().clone(); + let result = match request.operation { + AuthOperation::Begin { path } => { + if self.shared.client_closed(window) { + return Err(ide_plugin::HostError::Cancelled); + } + serde_json::to_value(service.begin(&owner, window, &path)?) + } + AuthOperation::Wait { id, timeout_ms } => serde_json::to_value(service.wait( + &owner, + id, + Duration::from_millis(u64::from(timeout_ms)), + &ide_core::CancellationToken::new(), + )?), + AuthOperation::Close { id } => { + service.close(&owner, id); + Ok(serde_json::Value::Null) + } + AuthOperation::Clear => { + service.clear(&owner); + Ok(serde_json::Value::Null) + } + }; + result.map_err(|e| ide_plugin::HostError::Failed(e.to_string())) + } + /// A remote transport closed; abandon its browser listeners. + pub fn forget_remote_loopbacks(&self, window: &str) { + self.shared.loopbacks.read().forget_window(window); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ide-host/src/plugins/run_detectors.rs b/crates/ide-host/src/plugins/run_detectors.rs index c48b001e6..31b89a876 100644 --- a/crates/ide-host/src/plugins/run_detectors.rs +++ b/crates/ide-host/src/plugins/run_detectors.rs @@ -42,6 +42,9 @@ pub struct Detector { pub args: Vec, #[serde(default)] pub test: bool, + /// Optional output parser, including the API 1.4 plugin export. + #[serde(default)] + pub framework: Option, } /// Detectors of enabled plugins. @@ -85,6 +88,12 @@ impl ExtensionPoint for RunDetectorsPoint { fn prepare(&self, pkg: &Package, decl: toml::Value) -> Result { let d: Detector = decode(POINT_ID, decl)?; + if d.framework.is_some() { + pkg.manifest.needs_api("runDetectors.framework", 4)?; + } + if d.framework.as_ref().is_some_and(|v| v.is_empty() || v.len() > 64) { + return Err("invalid test framework".into()); + } validate_item_id(&d.id, "run detector id")?; if d.name.trim().is_empty() || d.name.len() > 128 { return Err(format!("run detector {:?}: `name` must be 1–128 bytes", d.id)); @@ -110,48 +119,106 @@ impl ExtensionPoint for RunDetectorsPoint { } type Pins = Arc>>; +pub(super) type DynamicConfigurations = + Arc>>>; /// [`ExternalDetection`] over the detectors and the approved programs. pub struct PluginRunDetection { pub(crate) store: Arc, pub(crate) pins: Pins, + pub(crate) dynamic: DynamicConfigurations, + pub(crate) project_pins: super::api::ProjectPins, } -fn config_id(plugin: &str, detector: &str) -> String { +pub(super) fn config_id(plugin: &str, detector: &str) -> String { format!("{EXTERNAL_PREFIX}{plugin}:{detector}") } impl PluginRunDetection { - fn pin(&self, plugin: &str, d: &Detector) -> Option { + fn pin(&self, plugin: &str, d: &Detector, root: Option<&Path>) -> Option { let cap = Capability::Process(d.command.trim().to_owned()); - self.pins.read().get(&(plugin.to_owned(), cap.to_string())).cloned() + root.and_then(|root| self.project_pins.read().get(&(plugin.into(), cap.to_string(), root.into())).cloned()) + .or_else(|| self.pins.read().get(&(plugin.to_owned(), cap.to_string())).cloned()) } } impl ExternalDetection for PluginRunDetection { fn detect(&self, root: &Path) -> Vec { let items = self.store.items.read().clone(); - items + let mut configs: Vec<_> = items .into_iter() - .filter(|(_, plugin, d)| self.pin(plugin, d).is_some() && d.files.iter().any(|f| root.join(f).exists())) + .filter(|(_, plugin, d)| { + self.pin(plugin, d, Some(root)).is_some() && d.files.iter().any(|f| root.join(f).exists()) + }) .map(|(_, plugin, d)| RunConfig { id: config_id(&plugin, &d.id), name: d.name.clone(), kind: "plugin".into(), command: d.command.trim().to_owned(), args: d.args.clone(), - cwd: None, + cwd: Some(root.to_path_buf().into()), env: Default::default(), detected: true, is_test: d.test, debug_adapter: None, allow_parallel: false, - extras: Default::default(), + extras: ide_run::RunExtras { + params: d.framework.clone().map(|f| [("framework".into(), f)].into()).unwrap_or_default(), + ..Default::default() + }, }) - .collect() + .collect(); + for ((plugin, project), entries) in self.dynamic.read().iter().filter(|((_, project), _)| project == root) { + for c in entries { + let cap = Capability::Process(c.program.clone()); + if !self.project_pins.read().contains_key(&(plugin.clone(), cap.to_string(), project.clone())) + && !self.pins.read().contains_key(&(plugin.clone(), cap.to_string())) + { + continue; + } + let mut config = RunConfig { + id: String::new(), + name: c.name.clone(), + kind: "plugin".into(), + command: c.program.clone(), + args: c.args.clone(), + cwd: Some(root.to_path_buf().into()), + env: Default::default(), + detected: true, + is_test: c.test, + debug_adapter: None, + allow_parallel: false, + extras: Default::default(), + }; + config.id = config_id(plugin, &c.id); + config.cwd = Some(c.cwd.as_ref().map(|p| root.join(p)).unwrap_or_else(|| root.to_path_buf()).into()); + if let Some(framework) = &c.framework { + config.extras.params.insert("framework".into(), framework.clone()); + } + configs.push(config); + } + } + configs } fn launch(&self, config: &RunConfig) -> Result { + let dynamic = self.dynamic.read(); + if let Some(((plugin, root), c)) = + dynamic.iter().flat_map(|(k, entries)| entries.iter().map(move |c| (k, c))).find(|((plugin, root), c)| { + config_id(plugin, &c.id) == config.id && config.cwd.as_ref().is_some_and(|cwd| cwd.starts_with(root)) + }) + { + let cap = Capability::Process(c.program.clone()).to_string(); + let pin = self + .project_pins + .read() + .get(&(plugin.clone(), cap.clone(), root.clone())) + .cloned() + .or_else(|| self.pins.read().get(&(plugin.clone(), cap)).cloned()) + .ok_or_else(|| "run program is no longer granted".to_owned())?; + return pin.verify().map(Path::to_path_buf); + } + drop(dynamic); let items = self.store.items.read(); let (plugin, d) = items .iter() @@ -159,7 +226,7 @@ impl ExternalDetection for PluginRunDetection { .map(|(_, p, d)| (p.clone(), d.clone())) .ok_or_else(|| "the plugin that detected this configuration is no longer enabled".to_owned())?; drop(items); - let pin = self.pin(&plugin, &d).ok_or_else(|| { + let pin = self.pin(&plugin, &d, config.cwd.as_ref().map(|p| p.as_ref())).ok_or_else(|| { format!("allow process:{} for plugin {plugin} in Settings › Plugins to run this configuration", d.command) })?; pin.verify().map(Path::to_path_buf) diff --git a/crates/ide-host/src/plugins/run_targets.rs b/crates/ide-host/src/plugins/run_targets.rs new file mode 100644 index 000000000..8b2b01325 --- /dev/null +++ b/crates/ide-host/src/plugins/run_targets.rs @@ -0,0 +1,150 @@ +//! Project-owned registrations, retained across idle instance reclamation. +use super::api::{PluginApi, Shared}; +use super::{PluginEvent, PluginHost}; +use ide_core::{IdeError, IdeResult}; +use ide_plugin::{HostError, PluginRunTarget}; +use std::path::{Path, PathBuf}; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] +#[serde(rename_all = "camelCase")] +pub struct RegisteredRunTarget { + /// Opaque selector id, namespaced by the host. + pub id: String, + pub plugin_id: String, + pub plugin_name: String, + pub target: PluginRunTarget, + pub prompt: bool, +} + +pub(super) type Targets = std::collections::BTreeMap<(PathBuf, String), RegisteredRunTarget>; +fn selector(plugin: &str, id: &str) -> String { + format!("plugin-target:{}:{plugin}{id}", plugin.len()) +} +impl Shared { + fn target_answer_file(&self, root: &Path, id: &str) -> PathBuf { + let key = format!("{}\0{id}", root.display()); + self.data_dir.join(".run-target-answers").join(blake3::hash(key.as_bytes()).to_hex().as_str()) + } + pub(super) fn withdraw_run_targets(&self, plugin: Option<&str>, root: Option<&Path>) { + let mut targets = self.run_targets.lock(); + let before = targets.len(); + targets.retain(|(r, _), t| !(plugin.is_none_or(|p| p == t.plugin_id) && root.is_none_or(|path| path == r))); + if before != targets.len() { + drop(targets); + self.sink.emit(None, PluginEvent::RunTargetsChanged); + } + } +} +impl PluginApi { + pub(super) fn register_target(&self, root: &Path, target: PluginRunTarget) -> Result<(), HostError> { + ide_remote::plugin_target::validate(&target).map_err(|e| HostError::Invalid(e.to_string()))?; + let workspace = self.shared.workspace().ok_or(HostError::Cancelled)?; + if !workspace.projects().iter().any(|r| r == root) { + return Err(HostError::NotFound("project is not open".into())); + } + if !workspace.trusted(root) { + return Err(HostError::Denied("project is not trusted".into())); + } + self.approved_program_in_project(&target.program, Some(root))?; + let blocked = self.shared.prompts_blocked.lock(); + if blocked.contains(self.pkg.id()) { + return Err(HostError::Cancelled); + } + let id = selector(self.pkg.id(), &target.id); + let mut targets = self.shared.run_targets.lock(); + if !targets.contains_key(&(root.into(), id.clone())) + && targets.iter().filter(|((r, _), t)| r == root && t.plugin_id == self.pkg.id()).count() >= 32 + { + return Err(HostError::Invalid("at most 32 targets per plugin and project".into())); + } + let prompt = !self.shared.target_answer_file(root, &id).exists(); + targets.insert( + (root.into(), id.clone()), + RegisteredRunTarget { + id, + plugin_id: self.pkg.id().into(), + plugin_name: self.pkg.manifest.name.clone(), + target, + prompt, + }, + ); + drop(targets); + drop(blocked); + self.shared.sink.emit(None, PluginEvent::RunTargetsChanged); + Ok(()) + } + pub(super) fn unregister_target(&self, root: &Path, id: &str) { + if self.shared.run_targets.lock().remove(&(root.into(), selector(self.pkg.id(), id))).is_some() { + self.shared.sink.emit(None, PluginEvent::RunTargetsChanged); + } + } +} +impl PluginHost { + /// Drops registrations when their project closes or loses trust. + pub fn withdraw_project_run_targets(&self, root: &Path) { + self.shared.withdraw_run_targets(None, Some(root)); + } + pub fn run_targets(&self, root: &Path) -> Vec { + self.shared.run_targets.lock().iter().filter(|((r, _), _)| r == root).map(|(_, t)| t.clone()).collect() + } + pub fn prepare_run_target( + &self, + root: &Path, + id: &str, + config: &ide_run::RunConfig, + ) -> IdeResult { + let target = self + .shared + .run_targets + .lock() + .get(&(root.into(), id.into())) + .cloned() + .ok_or_else(|| IdeError::not_found("plugin run target is no longer available"))?; + let program = self.program_for_project(&target.plugin_id, &target.target.program, Some(root))?; + ide_remote::plugin_target::prepare(root, &target.target, &program, config) + } +} + +pub fn list(host: &crate::Host, plugins: &PluginHost, window: &str) -> IdeResult> { + Ok(plugins.run_targets(&host.project(window)?.root)) +} + +/// The first answer wins across the project's windows. Only Use changes settings. +pub fn answer( + host: &crate::Host, + plugins: &PluginHost, + window: &str, + id: &str, + use_target: bool, +) -> IdeResult { + host.require_trust(window)?; + let project = host.project(window)?; + let mut targets = plugins.shared.run_targets.lock(); + let target = targets + .get_mut(&(project.root.clone(), id.into())) + .ok_or_else(|| IdeError::not_found("plugin run target is no longer available"))?; + let file = plugins.shared.target_answer_file(&project.root, id); + if file.exists() { + target.prompt = false; + return Ok(project.settings.effective()); + } + if use_target { + plugins.program_for_project(&target.plugin_id, &target.target.program, Some(&project.root))?; + let scope = ide_workspace::SettingsScope::Project; + for key in ["run.defaultTarget", "languages.php.runTarget", "languages.node.runTarget"] { + project.settings.set(scope, key, serde_json::json!(id))?; + for window in host.windows_for_root(&project.root) { + if let Ok(open) = host.project(&window) { + open.settings.reload(); + open.sink.emit(ide_core::IdeEvent::SettingsChanged { key: key.into() }); + } + } + } + } + std::fs::create_dir_all(file.parent().unwrap())?; + std::fs::write(file, if use_target { "use" } else { "not-now" })?; + target.prompt = false; + drop(targets); + plugins.shared.sink.emit(None, PluginEvent::RunTargetsChanged); + Ok(project.settings.effective()) +} diff --git a/crates/ide-host/src/plugins/snippets.rs b/crates/ide-host/src/plugins/snippets.rs new file mode 100644 index 000000000..dac8228fb --- /dev/null +++ b/crates/ide-host/src/plugins/snippets.rs @@ -0,0 +1,106 @@ +//! Named JSON snippets, retained as read-only UI declarations. +use ide_extension::{Package, UiDecl, decode}; +use serde::Deserialize; +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Decl { + language: String, + path: std::path::PathBuf, +} +#[derive(Deserialize)] +#[serde(untagged)] +enum Lines { + One(String), + Many(Vec), +} +impl Lines { + fn values(self) -> Vec { + match self { + Self::One(s) => vec![s], + Self::Many(v) => v, + } + } +} +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Snippet { + prefix: Lines, + body: Lines, + description: Option, +} + +pub(super) fn declaration(pkg: &Package, decl: toml::Value) -> Result { + pkg.manifest.needs_api("contributes.snippets", 4)?; + let d: Decl = decode("snippets", decl)?; + if d.language.is_empty() + || d.language.len() > 64 + || !d.language.chars().all(|c| c.is_ascii_alphanumeric() || "-_.+#".contains(c)) + { + return Err("snippets: invalid language id".into()); + } + let text = pkg.read_text(&d.path)?; + if text.len() > 1024 * 1024 { + return Err("snippets: file exceeds 1 MiB".into()); + } + let snippets: std::collections::BTreeMap = + serde_json::from_str(&text).map_err(|e| format!("snippets: {e}"))?; + if snippets.len() > 1000 { + return Err("snippets: at most 1000 entries per file".into()); + } + let mut entries = vec![]; + for (name, s) in snippets { + let prefixes = s.prefix.values(); + let body = s.body.values().join("\n"); + if name.is_empty() + || name.len() > 256 + || prefixes.is_empty() + || prefixes.len() > 32 + || prefixes.iter().any(|p| p.is_empty() || p.len() > 256) + || body.len() > 65536 + || s.description.as_ref().is_some_and(|s| s.len() > 4096) + { + return Err(format!("snippets: invalid entry {name:?}")); + } + entries.push(serde_json::json!({"name":name,"prefixes":prefixes,"body":body,"description":s.description.unwrap_or_default()})); + } + let key = d.path.to_string_lossy().into_owned(); + let mut out = UiDecl::new( + key.clone(), + d.language.clone(), + serde_json::json!({"source":key,"language":d.language,"entries":entries}), + ); + out.files.push(d.path); + Ok(out) +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn validates_every_snippet_at_install_and_requires_api4() { + let dir = tempfile::tempdir().unwrap(); + let manifest = |api| { + ide_extension::Manifest::parse(&format!( + "id='test'\nname='Test'\nversion='1.0.0'\nengines.openidea='{api}'" + )) + .unwrap() + }; + let decl: toml::Value = toml::from_str("language='sql'\npath='snippets.json'").unwrap(); + std::fs::write( + dir.path().join("snippets.json"), + r#"{"Select":{"prefix":["sel","select"],"body":["SELECT ${1:column}","FROM $1$0"],"description":"Query"}}"#, + ) + .unwrap(); + let pkg = Package::new(manifest("^1.4"), dir.path().into()); + let out = declaration(&pkg, decl.clone()).unwrap(); + assert_eq!(out.json["entries"][0]["body"], "SELECT ${1:column}\nFROM $1$0"); + assert!(declaration(&Package::new(manifest("^1.3"), dir.path().into()), decl.clone()).is_err()); + for text in [ + r#"{"x":{"prefix":[],"body":"x"}}"#, + r#"{"x":{"prefix":"x","body":42}}"#, + r#"{"x":{"prefix":"x","body":"x","extra":1}}"#, + ] { + std::fs::write(dir.path().join("snippets.json"), text).unwrap(); + assert!(declaration(&pkg, decl.clone()).is_err()); + } + } +} diff --git a/crates/ide-host/src/plugins/types.rs b/crates/ide-host/src/plugins/types.rs index df58404f3..a17a148fe 100644 --- a/crates/ide-host/src/plugins/types.rs +++ b/crates/ide-host/src/plugins/types.rs @@ -8,6 +8,7 @@ pub use ide_plugin::{ }; use serde::{Deserialize, Serialize}; use specta::Type; +use std::collections::BTreeMap; /// A line a view's `process` body printed. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] @@ -164,8 +165,33 @@ pub struct PluginUpdate { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] #[serde(tag = "type", rename_all = "camelCase")] pub enum PluginEvent { + /// Project target registrations or remembered prompt answers changed. + RunTargetsChanged, + /// A window-owned operation; an empty request id queues an asynchronous run. + #[serde(rename_all = "camelCase")] + WindowRequest { plugin_id: String, request_id: String, request: WindowRequest }, + /// Reply to a content action in this window. + #[serde(rename_all = "camelCase")] + ContentTabReply { plugin_id: String, request_id: String, reply: ide_plugin::ContentTabReply }, + /// Plugins, their status or their contributions changed: re-fetch `plugin_list`. CommandsChanged, + /// Complete replacement of one plugin's project-scoped decorations. + #[serde(rename_all = "camelCase")] + DecorationsChanged { plugin_id: String, sets: Vec }, + /// Open a file in the invoking window. + EditorOpen { path: ide_core::IpcPath, selection: Option }, + /// Change selection or scrolling only in this window's already open editor. + EditorRanges { path: ide_core::IpcPath, ranges: Vec, reveal: bool }, + /// A bounded output channel snapshot, or withdrawal when the plugin stops. + #[serde(rename_all = "camelCase")] + OutputChanged { plugin_id: String, channel: String, text: Option }, + /// Reveal a read-only Output tab in the receiving window. + #[serde(rename_all = "camelCase")] + RevealOutput { plugin_id: String, channel: String, text: String }, + /// Execute an action through the receiving window and answer over IPC. + #[serde(rename_all = "camelCase")] + ExecuteCommand { request_id: String, id: String, arg: Option, chain: Vec }, /// Cached document associations were disposed (reload, disable or failure). DocumentIndexesChanged, /// The routes `plugin_id` reports for the project `root` may have changed: @@ -187,6 +213,9 @@ pub enum PluginEvent { /// A plugin's status bar item was set (`item`) or removed. #[serde(rename_all = "camelCase")] StatusItemChanged { plugin_id: String, id: String, item: Option }, + /// An override in the receiving window; removal exposes the global item. + #[serde(rename_all = "camelCase")] + WindowStatusItemChanged { plugin_id: String, id: String, item: Option }, /// A plugin's context key was set (`value`: JSON text) or removed, in the /// emitted window (every window when the client is `None`). #[serde(rename_all = "camelCase")] @@ -340,6 +369,8 @@ pub struct PackageFile { #[derive(Clone, Debug, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct PluginStatusItem { + /// Whether this item overrides the global item in the requesting window. + pub window: bool, pub plugin_id: String, pub id: String, pub item: StatusItem, @@ -388,3 +419,46 @@ pub struct PluginCommitCheckResult { /// The check could not run (the plugin failed, timed out or lacks `vcs.read`). pub error: Option, } + +/// The invoking window's response to a command request. +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +#[serde(tag = "kind", content = "value", rename_all = "camelCase")] +pub enum CommandAnswer { + Value(Option), + NotFound, + Failed(String), +} + +/// A plugin-owned editor or project-tree decoration set for one file. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct DecorationSet { + pub root: ide_core::IpcPath, + pub path: ide_core::IpcPath, + pub set_id: String, + pub tree: bool, + pub items: Vec, + pub file: Option, +} + +/// A request that the invoking window acknowledges through plugin_answer_command. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(tag = "kind", rename_all = "camelCase")] +pub enum WindowRequest { + /// Connect and introspect using the normal Database action in this window. + DatabaseConnect { id: String }, + /// Open and acknowledge validation by the shared content editor. + ContentTab { tab: ide_plugin::ContentTab }, + #[serde(rename_all = "camelCase")] + Run { operation: String, config: Box, debug: bool }, + #[serde(rename_all = "camelCase")] + Terminal { + operation: String, + key: String, + title: String, + program: ide_core::IpcPath, + args: Vec, + env: BTreeMap, + cwd: ide_core::IpcPath, + }, +} diff --git a/crates/ide-host/src/plugins/ui_points.rs b/crates/ide-host/src/plugins/ui_points.rs index 9a1edc4e0..b8ca5f94f 100644 --- a/crates/ide-host/src/plugins/ui_points.rs +++ b/crates/ide-host/src/plugins/ui_points.rs @@ -76,8 +76,18 @@ pub const COMMIT_CHECKS: &str = "commitChecks"; const VIEW_ITEM_MENU: &str = "viewItem.context"; /// The toolbar above the commit message (Commit tool window and dialog). const COMMIT_MESSAGE_MENU: &str = "commit.message"; -const MENU_IDS: [&str; 6] = - ["editor.context", "projectView.context", "editorTab.context", "tools", VIEW_ITEM_MENU, COMMIT_MESSAGE_MENU]; +const MENU_IDS: [&str; 10] = [ + "editor.context", + "projectView.context", + "editorTab.context", + "tools", + VIEW_ITEM_MENU, + COMMIT_MESSAGE_MENU, + "editor.title", + "mainToolbar", + "problems.context", + "searchResults.context", +]; const MAX_TITLE: usize = 256; const MAX_SETTINGS: usize = 200; const MAX_PATTERNS: usize = 16; @@ -135,6 +145,9 @@ pub(super) fn menu(pkg: &Package, decl: toml::Value) -> Result { return Err(format!("contributes.menus: unknown menu {:?} (one of {})", d.menu, MENU_IDS.join(", "))); } declared_command(pkg, MENUS, &d.command)?; + if ["editor.title", "mainToolbar", "problems.context", "searchResults.context"].contains(&d.menu.as_str()) { + pkg.manifest.needs_api("contributes.menus: language workbench menus", 4)?; + } if d.menu == COMMIT_MESSAGE_MENU { pkg.manifest.needs_api(&format!("contributes.menus: {COMMIT_MESSAGE_MENU}"), 3)?; } @@ -965,6 +978,20 @@ pub(super) fn locales(pkg: &Package, _decl: toml::Value) -> Result, ) -> IdeResult { - self.spawn_plugin_pty(host, plugin_id, window, output, || { + self.spawn_plugin_pty(host, plugin_id, window, output, None, || { self.web_pty_options(plugin_id, view, window, command) }) } @@ -154,19 +155,21 @@ impl PluginHost { (cols, rows): (u16, u16), output: crate::ops::Emit, ) -> IdeResult { - self.spawn_plugin_pty(host, plugin_id, window, output, || { + self.spawn_plugin_pty(host, plugin_id, window, output, None, || { self.log_tab_pty_options(plugin_id, key, window, on, (cols, rows)) }) } /// The PTY of a plugin, counted against its sessions and ended with its /// window, project or permission; `options` authorizes it. - fn spawn_plugin_pty( + #[allow(clippy::too_many_arguments)] + pub(super) fn spawn_plugin_pty( &self, host: &std::sync::Arc, plugin_id: &str, window: &str, output: crate::ops::Emit, + completion: Option) + Send + Sync>>, options: impl FnOnce() -> IdeResult, ) -> IdeResult { let _guard = self.lifecycle.lock(); @@ -181,10 +184,19 @@ impl PluginHost { return Err(IdeError::invalid("a plugin may own at most 8 PTYs")); } let cwd = options.cwd.0.clone(); + let previous = host.window_sink(window); + let sink: Arc = Arc::new(move |event: ide_core::IdeEvent| { + if let ide_core::IdeEvent::TerminalExited { code, .. } = &event + && let Some(completion) = &completion + { + completion(*code); + } + previous.emit(event); + }); let info = host.terminals.spawn_program( options, host.owner_window(window).to_owned(), - host.window_sink(window), + sink, Box::new(move |data| output(data)), )?; if !self @@ -258,7 +270,7 @@ impl PluginHost { /// Checks `command` against the plugin's current grant, the limits and /// the window's trusted project. - fn pty_options( + pub(super) fn pty_options( &self, plugin_id: &str, window: &str, @@ -309,7 +321,7 @@ impl PluginHost { if !workspace.trusted(&cwd) { return Err(IdeError::new(ide_core::error::ErrorKind::PermissionDenied, "project is not trusted")); } - let program = self.program_for(plugin_id, &command.program)?; + let program = self.program_for_project(plugin_id, &command.program, Some(&cwd))?; Ok(ide_terminal::TerminalOptions { cwd: cwd.into(), shell: Some(program.to_string_lossy().into_owned()), diff --git a/crates/ide-host/src/rpc.rs b/crates/ide-host/src/rpc.rs index fafe557ab..f8b5e75f1 100644 --- a/crates/ide-host/src/rpc.rs +++ b/crates/ide-host/src/rpc.rs @@ -248,7 +248,7 @@ impl Dispatcher { .add("term_is_busy", |c| async move { ops::term::is_busy(&c.host, c.arg("id")?) }) .add("term_default_shell", |_| async move { Ok(ide_terminal::default_shell_path()) }) .add("term_available_shells", |_| async move { Ok(ide_terminal::available_shells()) }) - .add("run_configs", |c| async move { ops::run::configs(&c.host, &c.window) }) + .add("run_configs", |c| async move { ops::doc::blocking(move || ops::run::configs(&c.host, &c.window)).await }) .add("run_save_configs", |c| async move { ops::run::save_configs( &c.host, diff --git a/crates/ide-host/src/trust.rs b/crates/ide-host/src/trust.rs index f6aa475e8..603dbc77d 100644 --- a/crates/ide-host/src/trust.rs +++ b/crates/ide-host/src/trust.rs @@ -146,6 +146,9 @@ impl crate::Host { phpstan.interrupt(); } if !trusted { + if let Some(plugins) = self.plugin_host() { + plugins.withdraw_project_run_targets(&project.root); + } project.lsp.set_execution_allowed(false); } project.repos(); @@ -396,6 +399,8 @@ pub fn projectless_command_allowed(cmd: &str) -> bool { | "plugin_set_enabled" | "plugin_grant" | "plugin_set_capability" + | "plugin_choose_program" + | "plugin_program" | "plugin_roll_back" | "plugin_reload" ) diff --git a/crates/ide-host/tests/distribution.rs b/crates/ide-host/tests/distribution.rs index 1af8a1068..1c38a8bb1 100644 --- a/crates/ide-host/tests/distribution.rs +++ b/crates/ide-host/tests/distribution.rs @@ -59,6 +59,7 @@ fn fixture() -> Fixture { fn inv() -> Invocation { Invocation { + command_chain: Vec::new(), client: Some("main".into()), workspace_root: None, editor: None, diff --git a/crates/ide-host/tests/plugin_data_tools.rs b/crates/ide-host/tests/plugin_data_tools.rs new file mode 100644 index 000000000..3721cf935 --- /dev/null +++ b/crates/ide-host/tests/plugin_data_tools.rs @@ -0,0 +1,262 @@ +//! Database and MCP calls through a real sandboxed API 1.4 component. +use ide_core::{CancellationToken, IdeEvent, event::McpUiRequest}; +use ide_host::{ + Host, HostConfig, + mcp::Registry, + ops, + plugins::{Consent, HostWorkspace, PluginEvent, PluginHost, WindowRequest}, +}; +use ide_mcp::{ToolCallError, ToolRegistry}; +use ide_plugin::{Invocation, Limits}; +use std::{ + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + time::Duration, +}; +const ID: &str = "test.data-tools"; +fn fixture() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/data-tools") +} +struct Harness { + host: Arc, + plugins: Arc, + root: PathBuf, + events: Arc>>, + requests: Arc>>, + _tmp: tempfile::TempDir, +} +impl Harness { + fn new() -> Self { + ide_ssh::secrets::testing::use_memory_store(); + let tmp = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap().join("project"); + std::fs::create_dir(&root).unwrap(); + let events = Arc::new(Mutex::new(vec![])); + let out = events.clone(); + let host = Arc::new(Host::new(HostConfig { + config_dir: tmp.path().join("config"), + data_dir: tmp.path().join("data"), + local_data_dir: tmp.path().join("local"), + languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()), + syntax: Arc::default(), + sink_for: Box::new(move |w| { + let (w, out) = (w.to_owned(), out.clone()); + Arc::new(move |e| out.lock().unwrap().push((w.clone(), e))) + }), + owner: |w| w, + runtime: tokio::runtime::Handle::current(), + project_environment: false, + })); + std::fs::write(root.join("db.sqlite"), "").unwrap(); + host.open_project("main", &root).unwrap(); + host.open_project("second", &root).unwrap(); + let requests = Arc::new(Mutex::new(vec![])); + let out = requests.clone(); + let plugins = Arc::new( + PluginHost::new( + tmp.path().join("plugins"), + Arc::new(move |w: Option<&str>, e| out.lock().unwrap().push((w.unwrap_or_default().to_owned(), e))), + Limits { timeout: Duration::from_millis(250), ..Default::default() }, + ) + .unwrap(), + ); + plugins.set_workspace(Some(Arc::new(HostWorkspace(Arc::downgrade(&host))))); + host.set_plugin_host(&plugins); + plugins.install_from_dir(&fixture(), &Consent::all()).unwrap(); + Self { host, plugins, root, events, requests, _tmp: tmp } + } + fn invocation(&self) -> Invocation { + Invocation { client: Some("main".into()), workspace_root: Some(self.root.clone()), ..Default::default() } + } + fn run(&self, id: &str, arg: Option<&str>) -> ide_core::IdeResult> { + self.plugins.run_command(ID, id, arg, self.invocation()) + } + fn source(&self) -> String { + serde_json::json!({"id":"local","name":"Example","driver":"sqlite","database":self.root.join("db.sqlite").to_string_lossy(),"options":[]}).to_string() + } +} +#[tokio::test(flavor = "multi_thread")] +async fn database_calls_enforce_grants_trust_ownership_and_uninstall_lifetime() { + let f = Harness::new(); + assert!(f.run("sources", None).is_err()); + f.host.set_project_trust(&f.root, true, false).await.unwrap(); + let source = f.source(); + f.run("upsert", Some(&source)).unwrap(); + assert_eq!(f.run("sources", None).unwrap().as_deref(), Some("[\"local\"]")); + f.run("credential", Some("never-on-disk")).unwrap(); + let source = f.host.project("main").unwrap().db.plugin_source(ID, "local").unwrap(); + let store = ide_db::secrets::SecretStore::new(f._tmp.path().join("data/db-passwords.json"), &f.root); + assert_eq!(store.get(&source.id).as_deref(), Some("never-on-disk")); + let file = f.root.join(".openidea/data-sources.toml"); + assert!(!std::fs::read_to_string(&file).unwrap().contains("never-on-disk")); + for command in ["remove", "connect"] { + assert!(f.run(command, Some("foreign")).is_err()); + } + assert!(f.events.lock().unwrap().iter().any(|(w, e)| w == "second" && matches!(e, IdeEvent::DbSourcesChanged))); + f.plugins.set_enabled(ID, false).unwrap(); + assert!(file.exists()); + f.plugins.set_enabled(ID, true).unwrap(); + assert!(f.host.project("main").unwrap().db.plugin_source(ID, "local").is_ok()); + f.host.set_mcp_executable(Some(PathBuf::from("/app/openidea"))); + let command: serde_json::Value = serde_json::from_str(&f.run("stdio", None).unwrap().unwrap()).unwrap(); + assert_eq!(command[1], "--mcp-stdio"); + f.host.set_mcp_executable(None); + assert_eq!(f.run("stdio", None).unwrap().as_deref(), Some("null")); + // Closed project cleanup uses the host-owned journal, not only current windows. + f.host.close_project("main"); + f.host.close_project("second"); + f.plugins.uninstall(ID).unwrap(); + assert!(ide_db::source::load(&f.root).unwrap().is_empty()); + assert!(store.get(&source.id).is_none()); +} +#[tokio::test(flavor = "multi_thread")] +async fn database_connect_asks_only_the_invoking_window_and_uses_normal_connection_flow() { + let f = Harness::new(); + f.host.set_project_trust(&f.root, true, false).await.unwrap(); + f.run("upsert", Some(&f.source())).unwrap(); + let (plugins, invocation) = (f.plugins.clone(), f.invocation()); + let run = tokio::task::spawn_blocking(move || plugins.run_command(ID, "connect", None, invocation)); + let (window, request, source) = tokio::time::timeout(Duration::from_secs(5), async { + loop { + let found = f.requests.lock().unwrap().iter().find_map(|(w, e)| match e { + PluginEvent::WindowRequest { request_id, request: WindowRequest::DatabaseConnect { id }, .. } => { + Some((w.clone(), request_id.clone(), id.clone())) + } + _ => None, + }); + if let Some(v) = found { + break v; + } + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + assert_eq!(window, "main"); + assert!(!f.plugins.answer_command("second", &request, ide_host::plugins::CommandAnswer::Value(None))); + tokio::time::sleep(Duration::from_millis(350)).await; // Password entry can outlive the 250 ms command budget. + ops::db::connect(&f.host, "main", &source).await.unwrap(); + f.plugins.answer_command("main", &request, ide_host::plugins::CommandAnswer::Value(None)); + run.await.unwrap().unwrap(); + assert!(!f.host.project("main").unwrap().db.connected().is_empty()); +} +#[tokio::test(flavor = "multi_thread")] +async fn mcp_listing_dispatch_confirmation_and_lifecycle_use_plugin_authority() { + let f = Harness::new(); + let registry = Arc::new(Registry::new(f.host.clone())); + assert!(!registry.tools(&f.root).unwrap().iter().any(|t| t.name == "test_data_tools__write")); + assert!(registry.call(&f.root, "test_data_tools__write", "{}", CancellationToken::new()).is_err()); + f.host.set_project_trust(&f.root, true, false).await.unwrap(); + let tools = registry.tools(&f.root).unwrap(); + let name = "test_data_tools__echo"; + assert!(tools.iter().any(|t| t.name == name && t.description.contains(ID))); + assert!(matches!( + registry.call(&f.root, name, r#"{"text":12}"#, CancellationToken::new()), + Err(ToolCallError::Failed(_)) + )); + let result = registry.call(&f.root, name, r#"{"text":"hello"}"#, CancellationToken::new()).unwrap(); + assert!(result.contains("hello")); + let denied = registry.call(&f.root, "test_data_tools__denied", "{}", CancellationToken::new()).unwrap_err(); + assert!(format!("{denied:?}").contains("process:sh")); + f.host.mcp_requests.set_ready("main", true); + let (r, root) = (registry.clone(), f.root.clone()); + let call = + tokio::task::spawn_blocking(move || r.call(&root, "test_data_tools__erase", "{}", CancellationToken::new())); + let request = tokio::time::timeout(Duration::from_secs(5), async { + loop { + if let Some(id) = f.events.lock().unwrap().iter().find_map(|(_, e)| match e { + IdeEvent::McpRequest { id, request: McpUiRequest::Confirm { .. } } => Some(*id), + _ => None, + }) { + break id; + } + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + assert!(f.host.mcp_requests.answer("second", request, true, None).is_err()); + f.host.mcp_requests.answer("main", request, false, None).unwrap(); + assert!(call.await.unwrap().is_err()); + f.plugins.set_enabled(ID, false).unwrap(); + assert!(!registry.tools(&f.root).unwrap().iter().any(|t| t.name == name)); + assert!(registry.call(&f.root, name, "{}", CancellationToken::new()).is_err()); + f.plugins.set_enabled(ID, true).unwrap(); + assert!(registry.tools(&f.root).unwrap().iter().any(|t| t.name == name)); + let (r, root) = (registry.clone(), f.root.clone()); + let spin = + tokio::task::spawn_blocking(move || r.call(&root, "test_data_tools__spin", "{}", CancellationToken::new())); + assert!(spin.await.unwrap().is_err(), "command budget interrupts wasm"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn database_imports_are_denied_without_the_optional_grant() { + let f = Harness::new(); + f.host.set_project_trust(&f.root, true, false).await.unwrap(); + f.plugins.set_capability(ID, "database.sources", false).unwrap(); + for command in ["upsert", "remove", "sources", "credential", "connect"] { + let err = f.run(command, Some(&f.source())).unwrap_err(); + assert!(err.message.contains("database.sources"), "{command}: {err}"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn official_api_examples_install_and_render_real_component_contributions() { + let f = Harness::new(); + f.host.set_project_trust(&f.root, true, false).await.unwrap(); + let official = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../plugins/official"); + for name in ["blame-annotations", "todo-finder"] { + f.plugins.install_from_dir(&official.join(name), &Consent::all()).unwrap(); + } + std::fs::write(f.root.join("notes.txt"), "TODO example\n").unwrap(); + let doc = ide_plugin::ProviderDocument { + path: f.root.join("notes.txt"), + language: "text".into(), + version: 1, + text: "TODO example\n".into(), + }; + assert!(eventually(|| { + f.plugins + .language_code_lenses(&doc) + .iter() + .any(|(owner, lens)| owner == "openidea.blame-annotations" && lens.command == "annotate") + })); + f.plugins.run_command("openidea.todo-finder", "find", None, f.invocation()).unwrap(); + let view = f.plugins.render_view("openidea.todo-finder", "todos", None, f.invocation()).unwrap(); + let ide_plugin::ViewBody::List(items) = view.body else { panic!("expected TODO list") }; + assert_eq!(items.len(), 1); + assert!(items[0].label.contains("TODO example")); + #[cfg(unix)] + { + let path = std::env::join_paths([official.join("container-runner"), official.join("test-runner")]).unwrap(); + f.plugins.set_program_path(Arc::new(move || Some(path.clone()))); + for name in ["container-runner", "test-runner"] { + f.plugins.install_from_dir(&official.join(name), &Consent::all()).unwrap(); + } + std::fs::write(f.root.join("example.tests"), "test adds\n").unwrap(); + let doc = + ide_plugin::ProviderDocument { path: f.root.join("example.tests"), text: "test adds\n".into(), ..doc }; + assert!(eventually(|| { + f.plugins + .language_gutter_actions(&doc) + .iter() + .any(|(owner, action)| owner == "openidea.test-runner" && action.argument.as_deref() == Some("adds")) + })); + f.plugins.refresh_run_configurations(&f.root); + let configs = f.plugins.run_detection().detect(&f.root); + assert!(configs.iter().any(|c| c.id == "plugin:openidea.test-runner:tests")); + } +} + +/// Editor-path calls drop a result that misses their budget, as the first call into a cold +/// component can on a slow machine: ask again until it arrives. +fn eventually(mut ready: impl FnMut() -> bool) -> bool { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + while std::time::Instant::now() < deadline { + if ready() { + return true; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + false +} diff --git a/crates/ide-host/tests/plugin_indexing.rs b/crates/ide-host/tests/plugin_indexing.rs index 63dd97dad..db938890f 100644 --- a/crates/ide-host/tests/plugin_indexing.rs +++ b/crates/ide-host/tests/plugin_indexing.rs @@ -20,7 +20,7 @@ impl Fixture { } fn with_manifest(edit: impl FnOnce(&Path)) -> Self { let tmp = tempfile::tempdir().unwrap(); - let root = tmp.path().canonicalize().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); let host = PluginHost::new(root.join("plugins"), Arc::new(|_: Option<&str>, _| {}), Limits::default()).unwrap(); host.set_project_policy(Arc::new(|_| true)); let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/indexer"); @@ -147,6 +147,48 @@ fn permissions_and_paths_are_checked_before_indexing() { ); } +#[cfg(windows)] +#[test] +fn verbatim_snapshots_use_the_hosts_plain_document_identity() { + let f = Fixture::new(); + let source = f.index("source.idx", SOURCE, &[method()], Some(1), "app"); + let mapping = f.index("mapping.idx", MAPPING, &[], Some(1), "app"); + let verbatim = std::fs::canonicalize(&source).unwrap(); + assert_ne!(source, verbatim); + f.host + .index_navigation( + &f.root, + Snapshot { path: &verbatim, version: Some(2), text: SOURCE, symbols: &[method()], scope: "app" }, + Invocation { workspace_root: Some(f.root.clone()), ..Default::default() }, + ) + .unwrap(); + let result = f.host.navigation_at(&f.root, &mapping, 1, reference_pos(), None, |_| None).unwrap(); + assert_eq!(result.targets.len(), 1, "a different prefix must replace the same document snapshot"); + assert_eq!(result.targets[0].path.0, source); + assert!(f.host.indexed_symbol_at(&f.root, &source, Pos::new(0, 4), None).is_some()); +} + +#[cfg(unix)] +#[test] +fn snapshot_paths_still_reject_symlink_aliases_and_escapes() { + let f = Fixture::new(); + let path = f.index("source.idx", SOURCE, &[method()], Some(1), "app"); + let outside = tempfile::NamedTempFile::new().unwrap(); + for (target, name) in [(path.as_path(), "alias.idx"), (outside.path(), "escape.idx")] { + let alias = f.root.join(name); + std::os::unix::fs::symlink(target, &alias).unwrap(); + assert!( + f.host + .index_navigation( + &f.root, + Snapshot { path: &alias, version: None, text: SOURCE, symbols: &[], scope: "app" }, + Invocation::default(), + ) + .is_err() + ); + } +} + #[test] fn indexed_refactorings_drop_graphs_after_permission_revocation_or_lost_trust() { let f = Fixture::new(); @@ -194,6 +236,7 @@ fn two_windows_of_same_root_keep_unsaved_snapshots_isolated() { &f.root, Snapshot { path, version: Some(1), text, symbols: &symbols, scope: "" }, Invocation { + command_chain: Vec::new(), client: Some(client.into()), workspace_root: Some(f.root.clone()), editor: None, diff --git a/crates/ide-host/tests/plugin_language_features.rs b/crates/ide-host/tests/plugin_language_features.rs new file mode 100644 index 000000000..200b44740 --- /dev/null +++ b/crates/ide-host/tests/plugin_language_features.rs @@ -0,0 +1,234 @@ +//! The API 1.4 fixture exercises every kind through the real component runtime and editor ops. +use ide_core::{CancellationToken, Pos, Range, RequestId}; +use ide_host::plugins::{Consent, HostWorkspace, PluginHost, set_editor_providers}; +use ide_host::{Host, HostConfig, ops}; +use ide_plugin::{Invocation, Limits, ProviderDocument}; +use std::{path::Path, sync::Arc, time::Duration}; +const ID: &str = "test.language-features"; +fn fixture() -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/language-features") +} +fn plugins(base: &Path) -> Arc { + let p = Arc::new( + PluginHost::new( + base.join("plugins"), + Arc::new(|_: Option<&str>, _| {}), + Limits { edit_soft: Duration::from_secs(3), edit_hard: Duration::from_secs(5), ..Default::default() }, + ) + .unwrap(), + ); + p.install_from_dir(&fixture(), &Consent::all()).unwrap(); + p +} +#[test] +fn injections_route_only_declared_embedded_providers_and_filter_diagnostics() { + let tmp = tempfile::tempdir().unwrap(); + let p = plugins(tmp.path()); + let doc = ProviderDocument { + path: tmp.path().join("a.txt"), + language: "text".into(), + version: 9, + text: "xx[[sql]]zz".into(), + }; + assert_eq!(p.provide_injections(&doc).len(), 1); + let items = p.provide_completion(&doc, Pos::new(0, 5), None); + assert_eq!(items.len(), 1); + assert_eq!(items[0].1.label, "sql: sql "); + assert!(p.provide_completion(&doc, Pos::new(0, 1), None).is_empty()); + assert_eq!(p.provide_hover(&doc, Pos::new(0, 5))[0].markdown, "sql: sql "); + let diagnostics = p.provide_diagnostics(&doc, &CancellationToken::new()); + assert_eq!(diagnostics.len(), 1); + assert_eq!(diagnostics[0].1.len(), 1); + assert_eq!(diagnostics[0].1[0].range, Range::new(Pos::new(0, 4), Pos::new(0, 7))); + assert!(p.language_signature_help(&ProviderDocument { language: "rust".into(), ..doc }, Pos::default()).is_none()); +} +#[test] +fn every_new_kind_requires_api4_at_install() { + let tmp = tempfile::tempdir().unwrap(); + let p = plugins(tmp.path()); + let package = tmp.path().join("old"); + std::fs::create_dir_all(package.join("dist")).unwrap(); + std::fs::copy(fixture().join("dist/plugin.wasm"), package.join("dist/plugin.wasm")).unwrap(); + let header = "id='old'\nname='Old'\nversion='1.0.0'\nengines.openidea='^1.3'\nwasm='dist/plugin.wasm'\n"; + for kind in [ + "formatting", + "signature-help", + "document-highlights", + "document-links", + "code-lenses", + "selection-ranges", + "folding", + "colors", + "document-symbols", + "implementation", + "type-definition", + "gutter-actions", + "injections", + ] { + std::fs::write( + package.join("plugin.toml"), + format!("{header}\n[[contributes.providers]]\nkinds=['{kind}']\nlanguages=['text']"), + ) + .unwrap(); + assert!(p.install_from_dir(&package, &Consent::all()).is_err(), "{kind}"); + } +} +#[tokio::test(flavor = "multi_thread")] +async fn editor_ops_merge_the_fixture_format_selection_and_execute_owned_commands() { + let tmp = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap().join("project"); + std::fs::create_dir(&root).unwrap(); + let host = Arc::new(Host::new(HostConfig { + config_dir: tmp.path().join("config"), + data_dir: tmp.path().join("data"), + local_data_dir: tmp.path().join("local"), + languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()), + syntax: Arc::default(), + sink_for: Box::new(|_| Arc::new(ide_core::NullSink)), + owner: |w| w, + runtime: tokio::runtime::Handle::current(), + project_environment: false, + })); + host.open_project("main", &root).unwrap(); + let p = plugins(tmp.path()); + p.set_workspace(Some(Arc::new(HostWorkspace(Arc::downgrade(&host))))); + set_editor_providers(Some(&p)); + let path = root.join("a.txt"); + std::fs::write(&path, "value\nnext\nend").unwrap(); + let doc = ops::doc::open(&host, "main", path.clone()).await.unwrap(); + assert_eq!( + ops::lsp::lsp_signature_help(&host, "main", doc.id, Pos::default()).await.unwrap().unwrap().signatures[0] + .parameters, + vec![(6, 11)] + ); + assert_eq!(ops::lsp::lsp_document_highlights(&host, "main", doc.id, Pos::default()).await.unwrap().len(), 1); + let links = ops::lsp::lsp_document_links(&host, "main", doc.id).await.unwrap(); + assert_eq!(links.len(), 2); + assert_eq!(links[0].path.as_ref().unwrap().0, root.join("next.txt")); + let lenses = ops::lsp::lsp_code_lens(&host, "main", doc.id).await.unwrap(); + assert_eq!(lenses.len(), 1, "undeclared command was dropped"); + ops::lsp::lsp_execute_command(&host, "main", doc.id, lenses[0].command.clone().unwrap()).await.unwrap(); + assert_eq!(p.run_command(ID, "state", None, Invocation::default()).unwrap().as_deref(), Some("lens")); + let outline = ops::doc::doc_outline(&host, "main", doc.id).await.unwrap(); + assert_eq!(outline[0].name, "Outer"); + assert_eq!(outline[0].children[0].name, "Inner"); + assert_eq!(ops::doc::doc_folding(&host, "main", doc.id).await.unwrap()[0].end_line, 2); + assert_eq!(ops::doc::doc_selection_ranges(&host, "main", doc.id, 0).await.unwrap().len(), 2); + for kind in [ide_lsp::types::GotoKind::Implementation, ide_lsp::types::GotoKind::TypeDefinition] { + assert_eq!(ops::lsp::lsp_goto(&host, "main", doc.id, Pos::default(), kind).await.unwrap().len(), 1); + } + assert_eq!( + ops::lsp::lsp_goto_implementation( + &host, + "main", + RequestId(1), + doc.id, + Pos::default(), + ide_host::ops::Channel::new(|_| {}) + ) + .await + .unwrap() + .len(), + 1 + ); + let visuals = ops::lsp::plugin_language_visuals(&host, "main", doc.id).await.unwrap(); + assert_eq!(visuals.colors.len(), 1); + assert_eq!(visuals.gutter_actions.len(), 1); + assert_eq!( + ops::lsp::plugin_color_presentations(&host, "main", doc.id, ID.into(), visuals.colors[0].1.clone()) + .await + .unwrap(), + ["red-plaintext", "#ff0000"] + ); + let project = host.project("main").unwrap(); + project + .settings + .set( + ide_workspace::settings::SettingsScope::Project, + "languages.pluginFormatters", + serde_json::json!({doc.language.as_str():ID}), + ) + .unwrap(); + let options = ide_lsp::types::FormatOptions { tab_size: 2, insert_spaces: true, on_save: false, tool: None }; + let formatted = + ops::lsp::lsp_format(&host, "main", doc.id, Some(Range::new(Pos::default(), Pos::new(0, 5))), options) + .await + .unwrap(); + assert_eq!(formatted[0].new_text, "formatted"); + // The same editor ops are exposed by the server dispatcher, with no desktop provider call. + let dispatcher = ide_host::rpc::Dispatcher::standard(); + assert!(dispatcher.commands().contains(&"plugin_language_visuals")); + + let base = tmp.path().join("server"); + let peer_slot = Arc::new(std::sync::OnceLock::new()); + let remote = ide_host::plugins::remote_host::RemotePlugins::start(&base, &host, peer_slot.clone()).unwrap(); + let mut dispatcher = ide_host::rpc::Dispatcher::standard(); + remote.register(&mut dispatcher); + let (client_io, server_io) = tokio::io::duplex(1 << 20); + let (sr, sw) = tokio::io::split(server_io); + tokio::spawn(ide_host::rpc::serve(host.clone(), dispatcher, peer_slot, sr, sw)); + struct Client; + impl ide_jsonrpc::Handler for Client { + fn on_notification(&self, _: &str, _: serde_json::Value) {} + fn on_request(&self, method: &str, _: serde_json::Value) -> Result { + Err((ide_jsonrpc::codes::METHOD_NOT_FOUND, method.to_owned())) + } + } + let (cr, cw) = tokio::io::split(client_io); + let peer = ide_jsonrpc::Peer::connect(cr, cw, Arc::new(Client)); + let _: serde_json::Value = + peer.request("initialize", serde_json::json!({"version":ide_host::rpc::PROTOCOL_VERSION}), None).await.unwrap(); + async fn invoke(peer: &ide_jsonrpc::Peer, cmd: &str, args: serde_json::Value) -> serde_json::Value { + peer.request( + "invoke", + serde_json::to_value(ide_host::rpc::Invoke { window: "main".into(), cmd: cmd.into(), args }).unwrap(), + None, + ) + .await + .unwrap() + } + invoke(&peer, "project_trust_set", serde_json::json!({"path":root,"trusted":true,"folder":false})).await; + let files = serde_json::json!({ID:p.remote_package_files(ID).unwrap()}); + let synced = invoke( + &peer, + ide_host::plugins::remote_host::SYNC_PACKAGES, + serde_json::json!({"packages":p.remote_packages(),"files":files}), + ) + .await; + assert_eq!(synced["failed"], serde_json::json!([]), "{synced}"); + let listed = invoke(&peer, "plugin_formatters", serde_json::json!({})).await; + assert_eq!(listed[0]["id"], ID); + let visual = invoke(&peer, "plugin_language_visuals", serde_json::json!({"doc":doc.id})).await; + assert_eq!(visual["gutterActions"].as_array().unwrap().len(), 1); + set_editor_providers(Some(&p)); + p.set_enabled(ID, false).unwrap(); + assert!(ops::lsp::plugin_formatters().is_empty()); + set_editor_providers(None); +} + +#[test] +fn formatting_uses_the_command_budget_while_editor_calls_can_expire() { + let tmp = tempfile::tempdir().unwrap(); + let p = PluginHost::new( + tmp.path().join("plugins"), + Arc::new(|_: Option<&str>, _| {}), + Limits { edit_soft: Duration::ZERO, ..Default::default() }, + ) + .unwrap(); + p.install_from_dir(&fixture(), &Consent::all()).unwrap(); + let doc = + ProviderDocument { path: tmp.path().join("a.txt"), language: "text".into(), version: 1, text: "value".into() }; + assert_eq!( + p.language_format( + ID, + &doc, + None, + ide_plugin::language_features::FormatOptions { tab_size: 4, insert_spaces: true } + ) + .unwrap()[0] + .new_text, + "formatted" + ); + assert!(p.language_signature_help(&doc, Pos::default()).is_none()); + assert!(p.info_of(ID).unwrap().stats.slow_calls > 0); +} diff --git a/crates/ide-host/tests/plugin_refactoring.rs b/crates/ide-host/tests/plugin_refactoring.rs index c49901d1c..1556c1485 100644 --- a/crates/ide-host/tests/plugin_refactoring.rs +++ b/crates/ide-host/tests/plugin_refactoring.rs @@ -11,7 +11,7 @@ use std::{path::Path, sync::Arc}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn indexed_usages_rename_preview_staleness_and_grouped_undo() { let tmp = tempfile::tempdir().unwrap(); - let root = tmp.path().canonicalize().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); let first = root.join("definition.idx"); let second = root.join("usage.idx"); let source = "def 😀route\nref 😀route\n"; @@ -35,9 +35,7 @@ async fn indexed_usages_rename_preview_staleness_and_grouped_undo() { project_environment: false, }); let project = host.open_light("main", &first).unwrap(); - // Editors retain ordinary Windows paths; index snapshots use canonical - // (verbatim on Windows) paths and the host's own project-root identity. - let doc = project.documents.open(dunce::simplified(&first)).unwrap(); + let doc = project.documents.open(&first).unwrap(); let plugins = Arc::new(PluginHost::new(root.join("plugins"), Arc::new(|_: Option<&str>, _| {}), Limits::default()).unwrap()); plugins.set_project_policy(Arc::new(|_| true)); diff --git a/crates/ide-host/tests/plugin_workbench.rs b/crates/ide-host/tests/plugin_workbench.rs new file mode 100644 index 000000000..08e790f19 --- /dev/null +++ b/crates/ide-host/tests/plugin_workbench.rs @@ -0,0 +1,354 @@ +use ide_core::{CancellationToken, Diagnostic, Pos, Range, Severity}; +use ide_host::plugins::{HostWorkspace, PluginWorkspace}; +use ide_host::{Host, HostConfig}; +use ide_plugin::TextQuery; +use std::path::PathBuf; +use std::sync::Arc; + +struct Harness { + host: Arc, + root: PathBuf, + _tmp: tempfile::TempDir, +} +impl Harness { + fn new() -> Self { + let tmp = tempfile::tempdir().unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap().join("project"); + std::fs::create_dir_all(&root).unwrap(); + let host = Arc::new(Host::new(HostConfig { + config_dir: tmp.path().join("config"), + data_dir: tmp.path().join("data"), + local_data_dir: tmp.path().join("local"), + languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()), + syntax: Arc::default(), + sink_for: Box::new(|_| Arc::new(ide_core::NullSink)), + owner: |w| w, + runtime: tokio::runtime::Handle::current(), + project_environment: false, + })); + host.open_project("main", &root).unwrap(); + host.open_project("second", &root).unwrap(); + Self { host, root, _tmp: tmp } + } + fn workspace(&self) -> HostWorkspace { + HostWorkspace(Arc::downgrade(&self.host)) + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn window_editor_reports_use_document_language_dirty_state_and_focus() { + let f = Harness::new(); + let path = f.root.join("main.rs"); + std::fs::write(&path, "fn main() {}\n").unwrap(); + let p = f.host.project("main").unwrap(); + let doc = p.documents.open(&path).unwrap(); + p.documents + .apply_edits(doc.id, &[ide_core::TextEdit { range: Range::default(), new_text: "// unsaved\n".into() }]) + .unwrap(); + f.host.mcp_reports.set_open_editors("main", vec![path.clone()]); + f.host.mcp_reports.set_focused_editor( + "main", + Some(ide_host::mcp::FocusedEditor { + path: path.clone().into(), + selection: Range::default(), + selected_text: String::new(), + }), + true, + ); + let workspace = f.workspace(); + let editors = workspace.open_editors("main").unwrap(); + assert_eq!(editors.len(), 1); + assert!(editors[0].dirty && editors[0].active); + assert_eq!(editors[0].language, "rust"); + assert!(workspace.open_editors("second").unwrap().is_empty()); + let second = f.host.project("second").unwrap(); + second.documents.open(&path).unwrap(); + f.host.mcp_reports.set_open_editors("second", vec![path.clone()]); + assert!(!workspace.open_editors("second").unwrap()[0].dirty); + assert!(workspace.open_editors("main").unwrap()[0].dirty); + assert!(workspace.buffer_text_in(Some("main"), &path).unwrap().contains("unsaved")); + assert!(!workspace.buffer_text_in(Some("second"), &path).unwrap().contains("unsaved")); + assert_ne!(workspace.document_in("main", &path), workspace.document_in("second", &path)); + f.host.mcp_reports.forget_window("main"); + assert!(workspace.open_editors("main").unwrap().is_empty()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn text_search_uses_overlay_excludes_utf16_and_limits() { + let f = Harness::new(); + let path = f.root.join("a.txt"); + std::fs::write(&path, "disk\n").unwrap(); + std::fs::create_dir(f.root.join("target")).unwrap(); + std::fs::write(f.root.join("target/ignored.txt"), "needle").unwrap(); + let p = f.host.project("main").unwrap(); + p.settings + .set(ide_workspace::settings::SettingsScope::Project, "files.ignoredNames", serde_json::json!(["target"])) + .unwrap(); + let doc = p.documents.open(&path).unwrap(); + p.documents + .apply_edits(doc.id, &[ide_core::TextEdit { range: Range::default(), new_text: "😀needle\nneedle\n".into() }]) + .unwrap(); + f.host.project("second").unwrap().documents.open(&path).unwrap(); + let workspace = f.workspace(); + let query = TextQuery { pattern: "needle".into(), case_sensitive: true, ..Default::default() }; + let found = workspace.search_text(Some("main"), &f.root, query.clone(), 10, &CancellationToken::new()).unwrap(); + assert_eq!(found.matches.len(), 2, "{found:?}; text={:?}", workspace.buffer_text(&path)); + assert_eq!(found.matches[0].location.range, Range::new(Pos::new(0, 2), Pos::new(0, 8))); + assert!(found.matches.iter().all(|m| m.location.path == path)); + let only_file = TextQuery { include: vec!["a.txt".into()], ..query.clone() }; + assert!( + workspace + .search_text(Some("second"), &f.root, only_file, 10, &CancellationToken::new()) + .unwrap() + .matches + .is_empty() + ); + let capped = workspace.search_text(Some("main"), &f.root, query.clone(), 1, &CancellationToken::new()).unwrap(); + assert_eq!(capped.matches.len(), 1); + assert!(capped.limit_hit); + let excluded = TextQuery { exclude: vec!["*.txt".into()], ..query.clone() }; + assert!( + workspace + .search_text(Some("main"), &f.root, excluded, 10, &CancellationToken::new()) + .unwrap() + .matches + .is_empty() + ); + for query in [ + TextQuery { pattern: "NEEDLE".into(), case_sensitive: true, ..Default::default() }, + TextQuery { pattern: "need".into(), whole_word: true, ..Default::default() }, + TextQuery { include: vec!["*.rs".into()], ..query.clone() }, + ] { + assert!( + workspace + .search_text(Some("main"), &f.root, query, 10, &CancellationToken::new()) + .unwrap() + .matches + .is_empty() + ); + } + let multiline = TextQuery { pattern: "needle\nneedle".into(), regex: true, ..Default::default() }; + let found = workspace.search_text(Some("main"), &f.root, multiline, 10, &CancellationToken::new()).unwrap(); + assert_eq!(found.matches[0].location.range, Range::new(Pos::new(0, 2), Pos::new(1, 6))); + let cancelled = CancellationToken::new(); + cancelled.cancel(); + assert!(matches!( + workspace.search_text(Some("main"), &f.root, query, 10, &cancelled), + Err(ide_plugin::HostError::Cancelled) + )); +} + +#[tokio::test(flavor = "multi_thread")] +async fn problems_share_project_reports_and_keep_sources_separate() { + let f = Harness::new(); + let path = f.root.join("a.txt"); + std::fs::write(&path, "text").unwrap(); + let diagnostic = |source: &str| Diagnostic { + range: Range::default(), + severity: Severity::Warning, + message: source.into(), + source: Some(source.into()), + code: None, + unnecessary: false, + deprecated: false, + server: None, + }; + f.host.mcp_reports.report_problems( + "second", + true, + vec![ide_host::mcp::FileProblems { + path: path.clone().into(), + items: vec![diagnostic("lsp"), diagnostic("plugin")], + }], + ); + let workspace = f.workspace(); + let files = workspace.problems("main", Some(&path)).unwrap(); + assert_eq!(files.len(), 2); + assert!(files.iter().all(|f| f.diagnostics.len() == 1)); + assert!(workspace.problems("main", Some(&f.root.join("other.txt"))).unwrap().is_empty()); + f.host.mcp_reports.forget_window("second"); + assert!(workspace.problems("main", None).unwrap().is_empty()); +} + +#[tokio::test(flavor = "multi_thread")] +async fn search_decodes_closed_documents_the_same_way_as_the_editor() { + let f = Harness::new(); + let path = f.root.join("utf16.txt"); + let mut bytes = vec![0xff, 0xfe]; + for unit in "😀needle\r\n".encode_utf16() { + bytes.extend_from_slice(&unit.to_le_bytes()); + } + std::fs::write(&path, bytes).unwrap(); + let query = TextQuery { pattern: "needle".into(), ..Default::default() }; + let result = f.workspace().search_text(Some("main"), &f.root, query, 10, &CancellationToken::new()).unwrap(); + assert_eq!(result.matches.len(), 1); + assert_eq!(result.matches[0].location.range, Range::new(Pos::new(0, 2), Pos::new(0, 8))); + assert_eq!(result.matches[0].line, "😀needle"); +} + +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread")] +async fn plugin_debug_uses_the_source_language_and_revalidates_its_owned_program() { + use ide_host::plugins::{Consent, PluginEvent, PluginHost, WindowRequest}; + use ide_plugin::{Invocation, Limits}; + let f = Harness::new(); + f.host.set_project_trust(&f.root, true, false).await.unwrap(); + std::fs::write(f.root.join("execution.marker"), "").unwrap(); + std::fs::write(f.root.join("entry.py"), "print('test')\n").unwrap(); + let events = Arc::new(parking_lot::Mutex::new(Vec::new())); + let captured = events.clone(); + let plugins = Arc::new( + PluginHost::new( + f._tmp.path().join("plugins"), + Arc::new(move |client: Option<&str>, event: PluginEvent| { + captured.lock().push((client.map(str::to_owned), event)) + }), + Limits::default(), + ) + .unwrap(), + ); + plugins.set_workspace(Some(Arc::new(f.workspace()))); + f.host.set_plugin_host(&plugins); + let package = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/execution"); + plugins.install_from_dir(&package, &Consent::all()).unwrap(); + plugins + .choose_program("execution", "process:execution-tool", Some(&f.root), Some(std::path::Path::new("/bin/cat"))) + .unwrap(); + let mut config = + plugins.run_detection().detect(&f.root).into_iter().find(|c| c.id == "plugin:execution:detected").unwrap(); + config.args = vec!["entry.py".into()]; + ide_run::save_user_configs(&f.root, &[config]).unwrap(); + let config = f.workspace().run_configuration("main", "execution", "detected").unwrap(); + assert_eq!(config.debug_adapter.as_deref(), Some("debugpy")); + let pinned = plugins.program_for_project("execution", "execution-tool", Some(&f.root)).unwrap(); + assert_eq!(plugins.prepare_debug_run("main", config.clone()).unwrap().command, pinned.to_string_lossy()); + let invocation = + Invocation { client: Some("main".into()), workspace_root: Some(f.root.clone()), ..Default::default() }; + assert!(plugins.run_command("execution", "debug", None, invocation).unwrap().is_some()); + assert!(events.lock().iter().any(|(client, event)| client.as_deref() == Some("main") && matches!(event, PluginEvent::WindowRequest { request: WindowRequest::Run { config, debug: true, .. }, .. } if config.command == pinned.to_string_lossy()))); + plugins.set_capability("execution", "process:execution-tool", false).unwrap(); + assert!(plugins.prepare_debug_run("main", config).is_err()); +} + +#[cfg(unix)] +#[tokio::test(flavor = "multi_thread")] +async fn plugin_run_targets_register_prompt_route_revalidate_and_withdraw() { + use ide_host::plugins::{Consent, PluginEvent, PluginHost, run_targets}; + use ide_plugin::{Invocation, Limits}; + use std::os::unix::fs::PermissionsExt; + let f = Harness::new(); + f.host.set_project_trust(&f.root, true, false).await.unwrap(); + let plugins = Arc::new( + PluginHost::new( + f._tmp.path().join("plugins"), + Arc::new(|_: Option<&str>, _: PluginEvent| {}), + Limits { idle_timeout: Some(std::time::Duration::from_millis(10)), ..Default::default() }, + ) + .unwrap(), + ); + plugins.set_workspace(Some(Arc::new(f.workspace()))); + f.host.set_plugin_host(&plugins); + let package = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../ide-plugin/tests/fixtures/execution"); + plugins.install_from_dir(&package, &Consent::all()).unwrap(); + let tool = f._tmp.path().join("wrapper"); + std::fs::write(&tool, "#!/bin/sh\nprintf 'cwd=%s\\n' \"$PWD\"\nprintf '<%s>\\n' \"$@\"\n").unwrap(); + std::fs::set_permissions(&tool, std::fs::Permissions::from_mode(0o755)).unwrap(); + let invocation = + || Invocation { client: Some("main".into()), workspace_root: Some(f.root.clone()), ..Default::default() }; + assert!(plugins.run_command("execution", "target", None, invocation()).is_err()); + plugins.choose_program("execution", "process:execution-tool", Some(&f.root), Some(&tool)).unwrap(); + plugins.run_command("execution", "target", None, invocation()).unwrap(); + tokio::time::sleep(std::time::Duration::from_millis(30)).await; + plugins.tick_at(std::time::Instant::now()); + assert_eq!(plugins.info_of("execution").unwrap().status, ide_host::plugins::PluginStatus::Inactive); + let targets = run_targets::list(&f.host, &plugins, "main").unwrap(); + assert_eq!(targets.len(), 1); + assert!(targets[0].prompt); + assert!(f.host.project("main").unwrap().settings.effective().run.default_target.is_empty()); + let id = &targets[0].id; + let settings = run_targets::answer(&f.host, &plugins, "main", id, false).unwrap(); + assert!(settings.run.default_target.is_empty()); + // A stale prompt in a second window cannot change an answered selection. + assert!(run_targets::answer(&f.host, &plugins, "second", id, true).unwrap().run.default_target.is_empty()); + plugins.run_command("execution", "untarget", None, invocation()).unwrap(); + assert!(plugins.run_targets(&f.root).is_empty()); + plugins.run_command("execution", "target", None, invocation()).unwrap(); + assert!(!plugins.run_targets(&f.root)[0].prompt); + plugins.run_command("execution", "target", Some("preferred"), invocation()).unwrap(); + let preferred = plugins.run_targets(&f.root).into_iter().find(|t| t.target.id == "preferred").unwrap(); + let settings = run_targets::answer(&f.host, &plugins, "main", &preferred.id, true).unwrap(); + assert_eq!(settings.run.default_target, preferred.id); + assert_eq!(settings.languages.php.run_target, preferred.id); + assert_eq!(settings.languages.node.run_target, preferred.id); + assert_eq!(f.host.project("second").unwrap().settings.effective().run.default_target, preferred.id); + std::fs::write(f.root.join("execution.marker"), "").unwrap(); + std::fs::write(f.root.join("entry.py"), "print('test')\n").unwrap(); + let mut detected = + plugins.run_detection().detect(&f.root).into_iter().find(|c| c.id == "plugin:execution:detected").unwrap(); + detected.args = vec!["entry.py".into()]; + ide_run::save_user_configs(&f.root, &[detected]).unwrap(); + let mut debug = f.workspace().run_configuration("main", "execution", "detected").unwrap(); + debug.extras.target = None; + let error = plugins.prepare_debug_run("main", debug).unwrap_err(); + assert_eq!(error.kind, ide_core::error::ErrorKind::InvalidInput); + assert!(error.to_string().contains("target")); + assert!(plugins.run_command("execution", "debug", None, invocation()).unwrap_err().to_string().contains("target")); + let output = Arc::new(parking_lot::Mutex::new(String::new())); + let captured = output.clone(); + let config = ide_run::RunConfig { + id: "php-target".into(), + name: "PHP target".into(), + kind: "php".into(), + command: "php".into(), + args: vec!["x.php".into(), "a b; $(touch forbidden)".into()], + cwd: None, + env: Default::default(), + detected: false, + is_test: false, + debug_adapter: None, + allow_parallel: false, + extras: Default::default(), + }; + let run = ide_host::ops::run::start( + &f.host, + "main", + config.clone(), + Arc::new(move |chunk| { + if let ide_run::OutputChunk::Stdout { text } = chunk { + captured.lock().push_str(&text); + } + }), + ) + .await + .unwrap(); + for _ in 0..100 { + if f.host.processes.list().iter().any(|r| r.id == run.id && !r.running) { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + } + let text = output.lock().clone(); + assert!(text.contains("\n<--cwd=/container>\n\n\n"), "{text}"); + assert!(text.contains(&format!("cwd={}", f.root.display())), "{text}"); + assert!(!f.root.join("forbidden").exists()); + // Pin checks are repeated at launch, not only at registration. + std::fs::write(&tool, "#!/bin/sh\nexit 99\n").unwrap(); + assert!(ide_host::ops::run::start(&f.host, "main", config.clone(), Arc::new(|_| {})).await.is_err()); + plugins.set_capability("execution", "process:execution-tool", false).unwrap(); + assert!(plugins.run_targets(&f.root).is_empty()); + assert!(ide_host::ops::run::start(&f.host, "main", config, Arc::new(|_| {})).await.is_err()); + plugins.choose_program("execution", "process:execution-tool", Some(&f.root), Some(&tool)).unwrap(); + plugins.run_command("execution", "target", None, invocation()).unwrap(); + f.host.set_project_trust(&f.root, false, false).await.unwrap(); + assert!(plugins.run_targets(&f.root).is_empty()); + assert!( + plugins.run_command("execution", "target", None, invocation()).unwrap_err().to_string().contains("trusted") + ); + f.host.set_project_trust(&f.root, true, false).await.unwrap(); + plugins.run_command("execution", "target", None, invocation()).unwrap(); + f.host.close_project_detached("main"); + assert_eq!(plugins.run_targets(&f.root).len(), 1); + f.host.close_project_detached("second"); + assert!(plugins.run_targets(&f.root).is_empty()); + plugins.shutdown(); +} diff --git a/crates/ide-host/tests/plugins.rs b/crates/ide-host/tests/plugins.rs index 9702d3f99..e2b0cb996 100644 --- a/crates/ide-host/tests/plugins.rs +++ b/crates/ide-host/tests/plugins.rs @@ -75,6 +75,26 @@ impl TestWorkspace { } impl PluginWorkspace for TestWorkspace { + fn run_configuration(&self, _client: &str, plugin: &str, id: &str) -> Result { + if plugin != "execution" || !matches!(id, "detected" | "dynamic") { + return Err(HostError::NotFound(id.into())); + } + Ok(ide_run::RunConfig { + id: format!("plugin:execution:{id}"), + name: "Tests".into(), + kind: "plugin".into(), + command: "execution-tool".into(), + args: vec!["test".into()], + cwd: Some(self.root.clone().into()), + env: Default::default(), + detected: true, + is_test: true, + debug_adapter: None, + allow_parallel: false, + extras: ide_run::RunExtras { params: [("framework".into(), "plugin".into())].into(), ..Default::default() }, + }) + } + fn projects(&self) -> Vec { std::iter::once(self.root.clone()).chain(self.other_roots.lock().iter().cloned()).collect() } @@ -285,6 +305,7 @@ fn fixture(limits: Limits) -> Fixture { impl Fixture { fn inv(&self) -> Invocation { Invocation { + command_chain: Vec::new(), client: Some("main".into()), workspace_root: Some(self.project.clone()), editor: None, @@ -5642,3 +5663,365 @@ fn view_secondary_requires_api_1_3_and_a_boolean_tool_window_placement() { assert!(err.message.contains(message), "{api}, {location}, {value}: {err:?}"); } } + +#[test] +fn api14_denies_new_privileges_and_requires_an_invoking_window() { + let f = fixture(Limits::default()); + f.host.install_from_dir(&sample("api14"), &Consent::default()).unwrap(); + for (command, arg, capability) in [ + ("execute", "test.action", "ide.commands"), + ("execute", "plugin.other.run", "ide.commands"), + ("execute", "plugin.api14.other.run", "ide.commands"), + ("open-editors", "", "editor.read"), + ("open-editor", "a.txt", "project.read"), + ("search-text", "needle", "project.read"), + ("problems", "", "project.read"), + ("decorations", "a.txt", "project.read"), + ("clear-decorations", "", "project.read"), + ("file-decoration", "a.txt", "project.read"), + ("clear-file-decorations", "", "project.read"), + ("vcs-log", "", "vcs.read"), + ("vcs-blame", "a.txt", "vcs.read"), + ("vcs-diff", "", "vcs.read"), + ("ipc", "/tmp/api14-no-grant", "ipc:"), + ("fetch", "http://example.com/", "net.http:"), + ("secret", "value", "secrets"), + ("delete-secret", "", "secrets"), + ] { + let error = f.run("api14", command, Some(arg)).unwrap_err(); + assert!(error.contains(capability) && error.contains("not granted"), "{error}"); + } + for command in ["window-status", "remove-window-status", "execute", "oauth", "output"] { + let error = f.host.run_command("api14", command, Some("value"), Invocation::default()).unwrap_err(); + assert_eq!(error.kind, ErrorKind::InvalidInput, "{command}: {error:?}"); + } + let own = f.run("api14", "execute", Some("plugin.api14.execute")).unwrap_err(); + assert!(own.contains("recursive") && !own.contains("not granted"), "{own}"); + f.host.set_capability("api14", "secrets", true).unwrap(); + f.run("api14", "secret", Some(" raw value ")).unwrap(); + assert_eq!(f.host.declared_secret("api14", "token"), Some(" raw value ".into())); + assert!(f.run("api14", "undeclared-secret", Some("value")).unwrap_err().contains("undeclared")); + f.run("api14", "delete-secret", None).unwrap(); + assert_eq!(f.host.declared_secret("api14", "token"), None); +} + +#[test] +fn api14_window_status_overrides_fall_back_and_are_withdrawn() { + let f = fixture(Limits::default()); + f.install(&sample("api14")); + f.run("api14", "global-status", Some("global")).unwrap(); + f.run("api14", "window-status", Some("main")).unwrap(); + let snapshot = f.host.status_snapshot("main"); + assert!(snapshot.iter().any(|i| i.window && i.item.text == "main")); + assert!(!f.host.status_snapshot("other").iter().any(|i| i.window)); + f.run("api14", "global-status", Some("updated")).unwrap(); + f.run("api14", "remove-window-status", None).unwrap(); + assert_eq!(f.host.status_snapshot("main").len(), 1); + assert_eq!(f.host.status_snapshot("main")[0].item.text, "updated"); + f.run("api14", "window-status", Some("override")).unwrap(); + f.host.forget_client("main"); + assert!(!f.host.status_snapshot("main").iter().any(|i| i.window)); + f.host.reopen_client("main"); + f.run("api14", "window-status", Some("again")).unwrap(); + f.host.set_enabled("api14", false).unwrap(); + assert!(f.host.status_snapshot("main").is_empty()); + assert!(f.take_events().iter().any(|(window, event)| window.as_deref() == Some("main") + && matches!(event, PluginEvent::WindowStatusItemChanged { item: None, .. }))); +} + +#[test] +fn api14_command_requests_preserve_window_result_and_reject_cycles() { + let f = Arc::new(fixture(Limits::default())); + f.install(&sample("api14")); + assert!(f.run("api14", "execute", Some("plugin.api14.execute")).unwrap_err().contains("recursive")); + for answer in [CommandAnswer::Value(Some("result".into())), CommandAnswer::Value(None), CommandAnswer::NotFound] { + f.take_events(); + let runner = f.clone(); + let worker = std::thread::spawn(move || runner.run("api14", "execute", Some("test.action"))); + let deadline = Instant::now() + Duration::from_secs(5); + let request = loop { + if let Some((window, PluginEvent::ExecuteCommand { request_id, arg, chain, .. })) = + f.events.lock().iter().find(|(_, e)| matches!(e, PluginEvent::ExecuteCommand { .. })).cloned() + { + assert_eq!(window.as_deref(), Some("main")); + assert_eq!(arg.as_deref(), Some("argument")); + assert_eq!(chain, ["api14"]); + break request_id; + } + assert!(Instant::now() < deadline); + std::thread::sleep(Duration::from_millis(5)); + }; + assert!(!f.host.answer_command("other", &request, answer.clone())); + assert!(f.host.answer_command("main", &request, answer.clone())); + let result = worker.join().unwrap(); + match answer { + CommandAnswer::Value(value) => assert_eq!(result.unwrap(), value), + CommandAnswer::NotFound => assert!(result.unwrap_err().contains("test.action")), + _ => unreachable!(), + } + } +} + +#[test] +fn api14_settings_and_input_events_use_other_and_capture_the_editor() { + let f = fixture(Limits::default()); + f.install(&sample("api14")); + use ide_plugin::WorkbenchEvent as E; + f.host.post_events(Some("main"), vec![E::SettingsChanged(Vec::new())]); + f.workspace + .settings + .lock() + .insert("api14".into(), serde_json::from_value(serde_json::json!({"enabled": true})).unwrap()); + let path = to_ipc(&f.project.join("src/notes.txt")); + f.host.post_window_events( + "main", + vec![ + E::SettingsChanged(vec!["another-plugin-key".into()]), + E::FileClosed(path.clone()), + E::InputModeChanged(ide_plugin::InputMode { + path: path.clone(), + mode: "visual".into(), + sub: Some("linewise".into()), + }), + ], + Some(PluginEditor { + path: path.clone(), + language: "text".into(), + selections: vec![ide_core::Range::default()], + }), + ); + f.host.flush_events_at(Instant::now() + Duration::from_secs(2)); + assert!(f.host.wait_events(Duration::from_secs(5))); + let received: serde_json::Value = serde_json::from_str(&f.run("api14", "events", None).unwrap().unwrap()).unwrap(); + let events = received.as_array().unwrap(); + assert_eq!(events.len(), 3, "{received}"); + assert_eq!(events[0]["kind"], "settings-changed"); + assert_eq!(events[0]["data"], "[\"enabled\"]"); + assert_eq!(events[2]["kind"], "input-mode-changed"); + assert_eq!(events[2]["editor"][0], path); + assert_eq!(events[2]["editor"][1][0], serde_json::json!([0, 0, 0, 0])); +} + +#[cfg(unix)] +#[test] +fn api14_ipc_echo_checks_the_grant_in_the_wasm_binding() { + use std::io::{Read, Write}; + let f = fixture(Limits::default()); + let pkg = f._tmp.path().join("api14-package"); + copy_dir(&sample("api14"), &pkg); + let path = f._tmp.path().join("echo.sock"); + let manifest = pkg.join("plugin.toml"); + let text = std::fs::read_to_string(&manifest) + .unwrap() + .replace("ipc:$TMPDIR/api14-*", &format!("ipc:{}", f._tmp.path().join("*.sock").display())); + std::fs::write(manifest, text).unwrap(); + f.install(&pkg); + let listener = std::os::unix::net::UnixListener::bind(&path).unwrap(); + let worker = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut bytes = [0; 4]; + stream.read_exact(&mut bytes).unwrap(); + stream.write_all(&bytes).unwrap(); + }); + assert_eq!(f.run("api14", "ipc", path.to_str()).unwrap().as_deref(), Some("echo")); + worker.join().unwrap(); + f.host.set_capability("api14", &format!("ipc:{}", f._tmp.path().join("*.sock").display()), false).unwrap(); + assert!(f.run("api14", "ipc", path.to_str()).unwrap_err().contains("not granted")); +} + +#[test] +fn api14_oauth_wait_excludes_the_call_budget_and_closes_after_redirect() { + use std::io::{Read, Write}; + let f = Arc::new(fixture(Limits { timeout: Duration::from_millis(200), ..Limits::default() })); + f.install(&sample("api14")); + let runner = f.clone(); + let worker = std::thread::spawn(move || runner.run("api14", "oauth", None)); + let deadline = Instant::now() + Duration::from_secs(5); + let uri = loop { + if let Some((_, PluginEvent::OutputChanged { text: Some(text), .. })) = f + .events + .lock() + .iter() + .find(|(_, e)| matches!(e, PluginEvent::OutputChanged { channel, .. } if channel == "oauth")) + .cloned() + { + break text; + } + assert!(Instant::now() < deadline); + std::thread::sleep(Duration::from_millis(5)); + }; + assert!(uri.starts_with("http://127.0.0.1:")); + std::thread::sleep(Duration::from_millis(300)); + let authority = uri.strip_prefix("http://").unwrap().split('/').next().unwrap(); + let mut stream = std::net::TcpStream::connect(authority).unwrap(); + stream.write_all(b"GET /oauth/callback?code=a%2Bb&state=test HTTP/1.1\r\nHost: localhost\r\n\r\n").unwrap(); + let mut response = String::new(); + stream.read_to_string(&mut response).unwrap(); + assert!(response.contains("You can close this tab")); + assert_eq!(worker.join().unwrap().unwrap().as_deref(), Some("code=a%2Bb&state=test")); + assert!(std::net::TcpStream::connect(authority).is_err()); + f.run("api14", "global-status", Some("still running")).unwrap(); +} + +#[cfg(unix)] +#[test] +fn api14_executable_choices_are_pinned_per_project_and_reset_revalidates() { + use std::os::unix::fs::PermissionsExt; + let f = fixture(Limits::default()); + let package = f._tmp.path().join("api14-process"); + copy_dir(&sample("api14"), &package); + let manifest = package.join("plugin.toml"); + let text = + std::fs::read_to_string(&manifest).unwrap().replace("optional = [", "optional = [\"process:api14-tool\", "); + std::fs::write(manifest, text).unwrap(); + f.install(&package); + let executable = |name: &str| { + let path = f._tmp.path().join(format!("{name}-bin")); + std::fs::write(&path, format!("#!/bin/sh\nprintf '{name}'\n")).unwrap(); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap(); + path.canonicalize().unwrap() + }; + let global = executable("global"); + let project = executable("project"); + f.host.choose_program("api14", "process:api14-tool", None, Some(&global)).unwrap(); + f.host.choose_program("api14", "process:api14-tool", Some(&f.project), Some(&project)).unwrap(); + assert_eq!(f.host.program_for("api14", "api14-tool").unwrap(), global); + assert_eq!(f.host.program_for_project("api14", "api14-tool", Some(&f.project)).unwrap(), project); + for command in ["process-run", "process-run-command", "process-start"] { + assert_eq!(f.run("api14", command, None).unwrap().as_deref(), Some("project")); + let invocation = Invocation { client: Some("global".into()), ..Invocation::default() }; + assert_eq!(f.host.run_command("api14", command, None, invocation).unwrap().as_deref(), Some("global")); + } + f.run("api14", "terminal", None).unwrap(); + assert!( + f.take_events().iter().any( + |(_, e)| matches!(e, PluginEvent::OpenTerminal { program, .. } if program.as_ref() == project.as_path()) + ) + ); + f.run("api14", "log", None).unwrap(); + assert!( + f.take_events() + .iter() + .any(|(_, e)| matches!(e, PluginEvent::OpenLogTab { log, .. } if log.program == "api14-tool")) + ); + let unrelated = f._tmp.path().join("unrelated"); + assert_eq!(f.host.program_for_project("api14", "api14-tool", Some(&unrelated)).unwrap(), global); + std::fs::write(&project, "changed").unwrap(); + assert!(f.host.program_for_project("api14", "api14-tool", Some(&f.project)).is_err()); + f.host.choose_program("api14", "process:api14-tool", Some(&f.project), None).unwrap(); + assert_eq!(f.host.program_for_project("api14", "api14-tool", Some(&f.project)).unwrap(), global); + f.host.choose_program("api14", "process:api14-tool", Some(&f.project), Some(&project)).unwrap(); + std::fs::write(&global, "changed").unwrap(); + f.host.choose_program("api14", "process:api14-tool", Some(&f.project), None).unwrap(); + assert!( + !f.host.info_of("api14").unwrap().capabilities.iter().find(|c| c.id == "process:api14-tool").unwrap().granted + ); +} + +#[cfg(unix)] +fn execution_fixture() -> Arc { + use std::os::unix::fs::PermissionsExt; + let f = Arc::new(fixture(Limits { idle_timeout: Some(Duration::from_millis(50)), ..Limits::default() })); + f.install(&sample("execution")); + std::fs::write(f.project.join("execution.marker"), "").unwrap(); + let executable = f._tmp.path().join("execution-bin"); + std::fs::write(&executable, "#!/bin/sh\nprintf '%s\n' \"$@\"\n").unwrap(); + std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o755)).unwrap(); + f.host.choose_program("execution", "process:execution-tool", Some(&f.project), Some(&executable)).unwrap(); + f +} + +#[cfg(unix)] +fn execution_request(f: &Arc, command: &str) -> (String, WindowRequest) { + let command = command.to_owned(); + let runner = f.clone(); + let worker = std::thread::spawn(move || runner.run("execution", &command, None)); + let mut request = None; + assert!(eventually(|| { + for (client, event) in f.take_events() { + if let PluginEvent::WindowRequest { request_id, request: r, .. } = event { + assert_eq!(client.as_deref(), Some("main")); + request = Some((request_id, r)); + } + } + request.is_some() + })); + let (request_id, request) = request.unwrap(); + assert!(!f.host.answer_command("second", &request_id, CommandAnswer::Value(None))); + if !request_id.is_empty() { + assert!(f.host.answer_command("main", &request_id, CommandAnswer::Value(None))); + } + let id = worker.join().unwrap().unwrap().unwrap_or_default(); + (id, request) +} + +#[cfg(unix)] +#[test] +fn api14_stage3_runs_and_terminals_deliver_exactly_one_exit_after_idle_reclamation() { + let f = execution_fixture(); + let (run, request) = execution_request(&f, "run"); + assert!(matches!(request, WindowRequest::Run { config, .. } if config.args == ["test", "appended"])); + assert!(f.run("execution", "debug", None).unwrap_err().contains("debugger")); + let (terminal, _) = execution_request(&f, "terminal"); + assert_eq!(execution_request(&f, "terminal").0, terminal); + assert!(f.run("execution", "write", None).unwrap_err().contains("running terminal")); + std::thread::sleep(Duration::from_millis(100)); + f.host.tick(); + assert_eq!(f.host.instances(), 0); + assert!(!f.host.finish_operation("second", &run, Some(2), false)); + assert!(f.host.finish_operation("main", &run, Some(2), true)); + assert!(!f.host.finish_operation("main", &run, Some(2), true)); + assert!(f.host.finish_operation("main", &terminal, None, true)); + assert!(!f.host.finish_operation("main", &terminal, Some(0), false)); + f.host.flush_events_at(Instant::now() + Duration::from_secs(3)); + assert!(eventually(|| f.host.wait_events(Duration::from_secs(1)))); + let events: serde_json::Value = + serde_json::from_str(&f.run("execution", "events", None).unwrap().unwrap()).unwrap(); + let events = events.as_array().unwrap(); + assert_eq!(events.len(), 2); + assert_eq!(events[0]["kind"], "run-exited"); + let data: serde_json::Value = serde_json::from_str(events[0]["data"].as_str().unwrap()).unwrap(); + assert_eq!(data["id"], run); + assert_eq!(data["killed"], true); + assert_eq!(events[1]["kind"], "terminal-exited"); + assert!(f.run("execution", "write", None).unwrap_err().contains("running terminal")); +} + +#[cfg(unix)] +#[test] +fn api14_stage3_configurations_test_results_and_content_actions_use_real_exports() { + let f = execution_fixture(); + f.host.refresh_run_configurations(&f.project); + let configs = f.host.run_detection().detect(&f.project); + assert!(configs.iter().any(|c| c.id == "plugin:execution:dynamic" + && c.is_test + && c.extras.params.get("framework").map(String::as_str) == Some("plugin"))); + assert_eq!( + f.host.parse_test_results("main", "plugin:execution:dynamic", "batch".into()).unwrap().unwrap().len(), + 3 + ); + let (_, request) = execution_request(&f, "content"); + assert!(matches!(request, WindowRequest::ContentTab { tab } if tab.key == "config")); + f.take_events(); + let action = ide_plugin::ContentTabAction { + request_id: "ct-1".into(), + key: "config".into(), + action: "save".into(), + content: "edited".into(), + }; + assert!(f.host.content_tab_action("second", "execution", action.clone()).is_err()); + f.host.content_tab_action("main", "execution", action).unwrap(); + f.host.flush_events_at(Instant::now() + Duration::from_secs(3)); + assert!(eventually(|| { + f.events.lock().iter().any(|(client, event)| client.as_deref() == Some("main") && matches!(event, PluginEvent::ContentTabReply { request_id, reply, .. } if request_id == "ct-1" && reply.saved == Some(true))) + })); + assert!(f.host.parse_test_results("main", "plugin:execution:dynamic", "trap".into()).unwrap().is_none()); +} + +#[cfg(unix)] +#[test] +fn api14_stage3_terminal_calls_are_denied_without_process_grants() { + let f = execution_fixture(); + f.host.set_capability("execution", "process:execution-tool", false).unwrap(); + assert!(f.run("execution", "terminal", None).unwrap_err().contains("not granted")); +} diff --git a/crates/ide-host/tests/remote_plugins.rs b/crates/ide-host/tests/remote_plugins.rs index 3416eef09..e2bc8e289 100644 --- a/crates/ide-host/tests/remote_plugins.rs +++ b/crates/ide-host/tests/remote_plugins.rs @@ -27,6 +27,7 @@ struct Client { events: Mutex>, plugin_events: Mutex>, secret_requests: Mutex>, + desktop: Option>, } impl Handler for Client { @@ -45,6 +46,19 @@ impl Handler for Client { self.secret_requests.lock().push(r); return Ok(serde_json::to_value(value).unwrap()); } + if let Some(desktop) = &self.desktop { + use ide_host::plugins::remote_host::{AUTH_REQUEST, SECRET_WRITE}; + if method == AUTH_REQUEST { + return Ok( + serde_json::to_value(desktop.remote_auth("main", serde_json::from_value(params).unwrap())).unwrap() + ); + } + if method == SECRET_WRITE { + return Ok( + serde_json::to_value(desktop.remote_secret_write(serde_json::from_value(params).unwrap())).unwrap() + ); + } + } Err((ide_jsonrpc::codes::METHOD_NOT_FOUND, method.to_owned())) } } @@ -207,11 +221,11 @@ async fn remote_projects_run_the_desktops_code_plugins_on_the_server() { }) .await; - // Disabled on the desktop: gone from the server. + // Disabled on the desktop: retained but stopped on the server. desktop.set_enabled("openidea.checkstyle", false).unwrap(); let removed = sync(&peer, &desktop, &[]).await; - assert_eq!(removed.removed, ["openidea.checkstyle"]); - assert!(plugins.host().info_of("openidea.checkstyle").is_err()); + assert!(removed.removed.is_empty()); + assert_eq!(plugins.host().info_of("openidea.checkstyle").unwrap().status, PluginStatus::Disabled); plugins.host().shutdown(); } @@ -350,3 +364,444 @@ async fn remote_projects_get_plugin_views_and_custom_editors() { plugins.host().shutdown(); } + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn api14_remote_calls_keep_browser_listeners_and_secrets_on_the_desktop() { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + let tmp = tempfile::tempdir().unwrap(); + let desktop = Arc::new( + PluginHost::new(tmp.path().join("desktop/plugins"), Arc::new(|_: Option<&str>, _| {}), Limits::default()) + .unwrap(), + ); + desktop.install_from_dir(&fixture("api14"), &Consent::all()).unwrap(); + let project = tmp.path().join("project"); + std::fs::create_dir(&project).unwrap(); + let project = project.canonicalize().unwrap(); + let base = tmp.path().join("server"); + let peer_slot = Arc::new(OnceLock::new()); + let sink_peer = peer_slot.clone(); + let host = Arc::new(Host::new(HostConfig { + config_dir: base.join("config"), + data_dir: base.join("data"), + local_data_dir: base.join("data"), + languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()), + syntax: Arc::default(), + sink_for: Box::new(move |w| rpc::event_sink(sink_peer.clone(), Some(w.into()))), + owner: |w| w, + runtime: tokio::runtime::Handle::current(), + project_environment: false, + })); + let plugins = RemotePlugins::start(&base.join("data"), &host, peer_slot.clone()).unwrap(); + let mut dispatcher = Dispatcher::standard(); + plugins.register(&mut dispatcher); + let (client_io, server_io) = tokio::io::duplex(1 << 20); + let (sr, sw) = tokio::io::split(server_io); + tokio::spawn(rpc::serve(host, dispatcher, peer_slot, sr, sw)); + let (cr, cw) = tokio::io::split(client_io); + let client = Arc::new(Client { desktop: Some(desktop.clone()), ..Client::default() }); + let peer = Peer::connect(cr, cw, client.clone()); + let _: Value = peer.request("initialize", json!({ "version": rpc::PROTOCOL_VERSION }), None).await.unwrap(); + invoke(&peer, "open_project", json!({ "path": project })).await.unwrap(); + invoke(&peer, "project_trust_set", json!({ "path": project, "trusted": true, "folder": false })).await.unwrap(); + sync(&peer, &desktop, &[]).await; + let report = sync(&peer, &desktop, &["api14"]).await; + assert!(report.failed.is_empty(), "{report:?}"); + let run = |command: &str, arg: Option<&str>| json!({ "pluginId": "api14", "commandId": command, "arg": arg, "editor": null }); + let remote_file = project.join("remote.txt"); + std::fs::write(&remote_file, "remote text").unwrap(); + invoke(&peer, "mcp_set_open_editors", json!({"paths": [remote_file]})).await.unwrap(); + invoke(&peer, "mcp_set_focused_editor", json!({"editor": {"path": remote_file, "selection": {"start": {"line": 0, "col": 0}, "end": {"line": 0, "col": 0}}, "selectedText": ""}, "focused": true})).await.unwrap(); + let editors = invoke(&peer, "plugin_run_command", run("open-editors", None)).await.unwrap(); + assert!(editors.as_str().unwrap().contains("remote.txt")); + assert!(editors.as_str().unwrap().contains("active: true")); + let problems = invoke(&peer, "plugin_run_command", run("problems", None)).await.unwrap(); + assert_eq!(problems.as_str(), Some("[]")); + invoke(&peer, "plugin_run_command", run("open-editor", Some("remote.txt"))).await.unwrap(); + invoke(&peer, "plugin_run_command", run("decorations", remote_file.to_str())).await.unwrap(); + let sets = invoke(&peer, "plugin_decoration_sets", json!({})).await.unwrap(); + assert_eq!(sets["api14"][0]["root"], project.to_str().unwrap()); + until("forwarded editor and decoration events", 5, || { + let events = client.plugin_events.lock(); + events.iter().any(|m| m.window.as_deref() == Some("main") && matches!(m.event, PluginEvent::EditorOpen { .. })) + && events.iter().any(|m| matches!(m.event, PluginEvent::DecorationsChanged { .. })) + }) + .await; + invoke(&peer, "plugin_run_command", run("secret", Some("remote-token"))).await.unwrap(); + assert_eq!(desktop.declared_secret("api14", "token").as_deref(), Some("remote-token")); + invoke(&peer, "plugin_run_command", run("delete-secret", None)).await.unwrap(); + assert_eq!(desktop.declared_secret("api14", "token"), None); + invoke(&peer, "plugin_run_command", run("window-status", Some("remote"))).await.unwrap(); + let status = invoke(&peer, "plugin_status_items", json!({})).await.unwrap(); + assert!(status.as_array().unwrap().iter().any(|s| s["window"] == true && s["item"]["text"] == "remote")); + let oauth_peer = peer.clone(); + let oauth = tokio::spawn(async move { + invoke( + &oauth_peer, + "plugin_run_command", + json!({ "pluginId": "api14", "commandId": "oauth", "arg": null, "editor": null }), + ) + .await + }); + until("remote OAuth URI", 5, || { + client + .plugin_events + .lock() + .iter() + .any(|m| matches!(&m.event, PluginEvent::OutputChanged { channel, .. } if channel == "oauth")) + }) + .await; + let uri = client + .plugin_events + .lock() + .iter() + .find_map(|m| match &m.event { + PluginEvent::OutputChanged { channel, text: Some(text), .. } if channel == "oauth" => Some(text.clone()), + _ => None, + }) + .unwrap(); + let authority = uri.strip_prefix("http://").unwrap().split('/').next().unwrap(); + let mut stream = tokio::net::TcpStream::connect(authority).await.unwrap(); + stream.write_all(b"GET /oauth/callback?code=desktop%2Bcode HTTP/1.1\r\nHost: localhost\r\n\r\n").await.unwrap(); + let mut response = String::new(); + stream.read_to_string(&mut response).await.unwrap(); + assert!(response.contains("You can close this tab")); + assert_eq!(oauth.await.unwrap().unwrap(), json!("code=desktop%2Bcode")); + plugins.host().shutdown(); + desktop.shutdown(); + peer.kill(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn api14_stage3_remote_terminal_pins_and_exit_events_stay_on_the_execution_host() { + let tmp = tempfile::tempdir().unwrap(); + let bin = tmp.path().join("bin"); + std::fs::create_dir(&bin).unwrap(); + let executable = bin.join("execution-tool"); + std::fs::write(&executable, "#!/bin/sh\nif [ \"$1\" = wrapped ]; then printf '<%s>\\n' \"$@\" > \"$PWD/wrapped.txt\"; exit; fi\nread line\nprintf '%s\\n' \"$line\"\n").unwrap(); + std::fs::set_permissions(&executable, std::fs::Permissions::from_mode(0o755)).unwrap(); + let path_var = { + let bin = bin.clone(); + Arc::new(move || Some(bin.clone().into_os_string())) + }; + let desktop = + PluginHost::new(tmp.path().join("desktop/plugins"), Arc::new(|_: Option<&str>, _| {}), Limits::default()) + .unwrap(); + desktop.set_program_path(path_var.clone()); + desktop.install_from_dir(&fixture("execution"), &Consent::all()).unwrap(); + let project = tmp.path().join("project"); + std::fs::create_dir(&project).unwrap(); + let project = project.canonicalize().unwrap(); + std::fs::write(project.join("execution.marker"), "").unwrap(); + let base = tmp.path().join("server"); + let peer_slot = Arc::new(OnceLock::new()); + let sink_peer = peer_slot.clone(); + let host = Arc::new(Host::new(HostConfig { + config_dir: base.join("config"), + data_dir: base.join("data"), + local_data_dir: base.join("data"), + languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()), + syntax: Arc::default(), + sink_for: Box::new(move |w| rpc::event_sink(sink_peer.clone(), Some(w.into()))), + owner: |w| w, + runtime: tokio::runtime::Handle::current(), + project_environment: false, + })); + let plugins = RemotePlugins::start(&base.join("data"), &host, peer_slot.clone()).unwrap(); + plugins.host().set_program_path(path_var); + let mut dispatcher = Dispatcher::standard(); + plugins.register(&mut dispatcher); + let (client_io, server_io) = tokio::io::duplex(1 << 20); + let (sr, sw) = tokio::io::split(server_io); + tokio::spawn(rpc::serve(host, dispatcher, peer_slot, sr, sw)); + let (cr, cw) = tokio::io::split(client_io); + let client = Arc::new(Client::default()); + let peer = Peer::connect(cr, cw, client.clone()); + let _: Value = peer.request("initialize", json!({ "version": rpc::PROTOCOL_VERSION }), None).await.unwrap(); + invoke(&peer, "open_project", json!({ "path": project })).await.unwrap(); + invoke(&peer, "project_trust_set", json!({ "path": project, "trusted": true, "folder": false })).await.unwrap(); + sync(&peer, &desktop, &[]).await; + assert!(sync(&peer, &desktop, &["execution"]).await.failed.is_empty()); + invoke( + &peer, + "plugin_run_command", + json!({"pluginId":"execution", "commandId":"target", "arg":null, "editor":null}), + ) + .await + .unwrap(); + let targets = invoke(&peer, "plugin_run_targets", json!({})).await.unwrap(); + assert_eq!(targets.as_array().unwrap().len(), 1); + assert_eq!(targets[0]["pluginId"], "execution"); + assert!(desktop.run_targets(&project).is_empty()); + let id = targets[0]["id"].as_str().unwrap(); + let settings = invoke(&peer, "plugin_answer_run_target", json!({"id":id, "useTarget":false})).await.unwrap(); + assert_eq!(settings["run"]["defaultTarget"], ""); + let config = json!({"id":"wrapped-php", "name":"Wrapped PHP", "kind":"php", "command":"php", "args":["x.php", "literal ; $(touch bad)"], "env":{}, "detected":false, "isTest":false, "target":id}); + invoke(&peer, "run_start", json!({"config":config, "onOutput":{"$channel":38}})).await.unwrap(); + until("remote wrapped run", 5, || project.join("wrapped.txt").exists()).await; + assert_eq!( + std::fs::read_to_string(project.join("wrapped.txt")).unwrap(), + "\n<--cwd=/container>\n\n\n\n" + ); + assert!(!project.join("bad").exists()); + invoke( + &peer, + "plugin_run_command", + json!({"pluginId":"execution", "commandId":"untarget", "arg":null, "editor":null}), + ) + .await + .unwrap(); + assert_eq!(invoke(&peer, "plugin_run_targets", json!({})).await.unwrap(), json!([])); + client.plugin_events.lock().clear(); + client.events.lock().clear(); + let runner = peer.clone(); + let run = tokio::spawn(async move { + invoke( + &runner, + "plugin_run_command", + json!({"pluginId": "execution", "commandId": "terminal", "arg": null, "editor": null}), + ) + .await + .unwrap() + }); + until("terminal request", 5, || { + client.plugin_events.lock().iter().any(|m| matches!(m.event, PluginEvent::WindowRequest { .. })) + }) + .await; + let (request_id, operation) = client + .plugin_events + .lock() + .iter() + .find_map(|m| match &m.event { + PluginEvent::WindowRequest { + request_id, + request: ide_host::plugins::WindowRequest::Terminal { operation, .. }, + .. + } => { + assert_eq!(m.window.as_deref(), Some("main")); + Some((request_id.clone(), operation.clone())) + } + _ => None, + }) + .unwrap(); + let terminal = invoke( + &peer, + "plugin_terminal_session_spawn", + json!({"operation": operation, "cols": 80, "rows": 24, "onOutput": {"$channel": 35}}), + ) + .await + .unwrap(); + assert!(terminal["id"].as_u64().is_some()); + assert!( + invoke( + &peer, + "plugin_terminal_session_spawn", + json!({"operation": operation, "cols": 80, "rows": 24, "onOutput": {"$channel": 36}}) + ) + .await + .is_err() + ); + invoke( + &peer, + "plugin_answer_command", + json!({"requestId": request_id, "answer": {"kind": "value", "value": null}}), + ) + .await + .unwrap(); + assert_eq!(run.await.unwrap(), operation); + invoke( + &peer, + "plugin_run_command", + json!({"pluginId": "execution", "commandId": "write", "arg": "finished\n", "editor": null}), + ) + .await + .unwrap(); + until("PTY exit", 5, || client.events.lock().iter().any(|m| matches!(m.event, IdeEvent::TerminalExited { .. }))) + .await; + plugins.host().flush_events_at(std::time::Instant::now() + Duration::from_secs(3)); + plugins.host().wait_events(Duration::from_secs(5)); + let received = invoke( + &peer, + "plugin_run_command", + json!({"pluginId": "execution", "commandId": "events", "arg": null, "editor": null}), + ) + .await + .unwrap(); + let received: Value = serde_json::from_str(received.as_str().unwrap()).unwrap(); + assert_eq!(received.as_array().unwrap().len(), 1); + assert_eq!(received[0]["kind"], "terminal-exited"); + let data: Value = serde_json::from_str(received[0]["data"].as_str().unwrap()).unwrap(); + assert_eq!(data["id"], operation); + assert_eq!(data["status"], 0); + assert_eq!(data["closed"], false); + + client.plugin_events.lock().clear(); + client.events.lock().clear(); + let runner = peer.clone(); + let run = tokio::spawn(async move { + invoke( + &runner, + "plugin_run_command", + json!({"pluginId": "execution", "commandId": "run", "arg": null, "editor": null}), + ) + .await + .unwrap() + }); + until("run request", 5, || { + client.plugin_events.lock().iter().any(|m| { + matches!(&m.event, PluginEvent::WindowRequest { request: ide_host::plugins::WindowRequest::Run { .. }, .. }) + }) + }) + .await; + let (request_id, operation, config) = client + .plugin_events + .lock() + .iter() + .find_map(|m| match &m.event { + PluginEvent::WindowRequest { + request_id, + request: ide_host::plugins::WindowRequest::Run { operation, config, .. }, + .. + } => Some((request_id.clone(), operation.clone(), config.clone())), + _ => None, + }) + .unwrap(); + let info = invoke(&peer, "run_start", json!({"config": config, "onOutput": {"$channel": 37}})).await.unwrap(); + invoke( + &peer, + "plugin_answer_command", + json!({"requestId": request_id, "answer": {"kind": "value", "value": null}}), + ) + .await + .unwrap(); + assert_eq!(run.await.unwrap(), operation); + invoke(&peer, "run_stdin", json!({"id": info["id"], "data": "finished\n"})).await.unwrap(); + until("run exit", 5, || client.events.lock().iter().any(|m| matches!(&m.event, IdeEvent::RunExited { .. }))).await; + plugins.host().flush_events_at(std::time::Instant::now() + Duration::from_secs(3)); + plugins.host().wait_events(Duration::from_secs(5)); + let received = invoke( + &peer, + "plugin_run_command", + json!({"pluginId": "execution", "commandId": "events", "arg": null, "editor": null}), + ) + .await + .unwrap(); + let received: Value = serde_json::from_str(received.as_str().unwrap()).unwrap(); + assert_eq!(received.as_array().unwrap().len(), 2); + assert_eq!(received[1]["kind"], "run-exited"); + let data: Value = serde_json::from_str(received[1]["data"].as_str().unwrap()).unwrap(); + assert_eq!(data["id"], operation); + assert_eq!(data["status"], 0); + assert_eq!(data["killed"], false); + plugins.host().shutdown(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn api14_database_sources_credentials_and_disable_lifetime_run_on_the_server() { + ide_ssh::secrets::testing::use_memory_store(); + let tmp = tempfile::tempdir().unwrap(); + let id = "test.data-tools"; + let desktop = + PluginHost::new(tmp.path().join("desktop/plugins"), Arc::new(|_: Option<&str>, _| {}), Limits::default()) + .unwrap(); + desktop.install_from_dir(&fixture("data-tools"), &Consent::all()).unwrap(); + let project = tmp.path().join("project"); + std::fs::create_dir(&project).unwrap(); + let project = project.canonicalize().unwrap(); + std::fs::write(project.join("data.sqlite"), "").unwrap(); + let base = tmp.path().join("server"); + let slot = Arc::new(OnceLock::new()); + let sink = slot.clone(); + let host = Arc::new(Host::new(HostConfig { + config_dir: base.join("config"), + data_dir: base.join("data"), + local_data_dir: base.join("local"), + languages: Arc::new(ide_language::LanguageRegistry::load(None).unwrap()), + syntax: Arc::default(), + sink_for: Box::new(move |w| rpc::event_sink(sink.clone(), Some(w.into()))), + owner: |w| w, + runtime: tokio::runtime::Handle::current(), + project_environment: false, + })); + let plugins = RemotePlugins::start(&base.join("data"), &host, slot.clone()).unwrap(); + let mut dispatcher = Dispatcher::standard(); + plugins.register(&mut dispatcher); + let (client_io, server_io) = tokio::io::duplex(1 << 20); + let (sr, sw) = tokio::io::split(server_io); + tokio::spawn(rpc::serve(host.clone(), dispatcher, slot, sr, sw)); + let (cr, cw) = tokio::io::split(client_io); + let client = Arc::new(Client::default()); + let peer = Peer::connect(cr, cw, client.clone()); + let _: Value = peer.request("initialize", json!({"version":rpc::PROTOCOL_VERSION}), None).await.unwrap(); + invoke(&peer, "open_project", json!({"path":project})).await.unwrap(); + invoke(&peer, "project_trust_set", json!({"path":project,"trusted":true,"folder":false})).await.unwrap(); + assert!(sync(&peer, &desktop, &[id]).await.failed.is_empty()); + let source = + json!({"id":"local","name":"Remote local","driver":"sqlite","database":"data.sqlite","options":[]}).to_string(); + invoke(&peer, "plugin_run_command", json!({"pluginId":id,"commandId":"upsert","arg":source,"editor":null})) + .await + .unwrap(); + invoke( + &peer, + "plugin_run_command", + json!({"pluginId":id,"commandId":"credential","arg":"server-only-secret","editor":null}), + ) + .await + .unwrap(); + let sources = invoke(&peer, "db_sources", json!({})).await.unwrap(); + assert_eq!(sources[0]["pluginOwner"], id); + let internal = sources[0]["id"].as_str().unwrap(); + let store = ide_db::secrets::SecretStore::new(base.join("data/db-passwords.json"), &project); + assert_eq!(store.get(internal).as_deref(), Some("server-only-secret")); + until("database source change", 5, || { + client.events.lock().iter().any(|m| matches!(m.event, IdeEvent::DbSourcesChanged)) + }) + .await; + desktop.set_enabled(id, false).unwrap(); + let report = sync(&peer, &desktop, &[]).await; + assert!(report.removed.is_empty()); + assert!(!invoke(&peer, "db_sources", json!({})).await.unwrap().as_array().unwrap().is_empty()); + desktop.set_enabled(id, true).unwrap(); + assert!(sync(&peer, &desktop, &[]).await.failed.is_empty()); + let runner = peer.clone(); + let connection = tokio::spawn(async move { + invoke( + &runner, + "plugin_run_command", + json!({"pluginId":"test.data-tools","commandId":"connect","arg":null,"editor":null}), + ) + .await + }); + until("database connect request", 5, || { + client.plugin_events.lock().iter().any(|m| { + matches!( + m.event, + PluginEvent::WindowRequest { request: ide_host::plugins::WindowRequest::DatabaseConnect { .. }, .. } + ) + }) + }) + .await; + let request = client + .plugin_events + .lock() + .iter() + .find_map(|m| match &m.event { + PluginEvent::WindowRequest { + request_id, + request: ide_host::plugins::WindowRequest::DatabaseConnect { .. }, + .. + } => Some(request_id.clone()), + _ => None, + }) + .unwrap(); + invoke(&peer, "db_connect", json!({"id":internal})).await.unwrap(); + invoke(&peer, "plugin_answer_command", json!({"requestId":request,"answer":{"kind":"value","value":null}})) + .await + .unwrap(); + connection.await.unwrap().unwrap(); + assert!(project.join("data.sqlite").exists()); + desktop.uninstall(id).unwrap(); + assert_eq!(sync(&peer, &desktop, &[]).await.removed, [id]); + assert!(invoke(&peer, "db_sources", json!({})).await.unwrap().as_array().unwrap().is_empty()); + assert!(store.get(internal).is_none()); + plugins.host().shutdown(); +} diff --git a/crates/ide-http/src/client.rs b/crates/ide-http/src/client.rs index 05963a4c5..66fcc97d2 100644 --- a/crates/ide-http/src/client.rs +++ b/crates/ide-http/src/client.rs @@ -26,6 +26,11 @@ pub fn with_proxy(builder: reqwest::ClientBuilder, proxy: &ProxySettings) -> req /// A client for `proxy`. pub fn build(proxy: &ProxySettings) -> IdeResult { + build_with_redirect(proxy, reqwest::redirect::Policy::default()) +} + +/// A client with an explicit redirect policy, for callers enforcing network grants. +pub fn build_with_redirect(proxy: &ProxySettings, redirect: reqwest::redirect::Policy) -> IdeResult { use rustls_platform_verifier::BuilderVerifierExt; let fail = |e: String| IdeError::internal(format!("HTTP client: {e}")); let provider = Arc::new(rustls::crypto::ring::default_provider()); @@ -44,6 +49,7 @@ pub fn build(proxy: &ProxySettings) -> IdeResult { ]; let builder = reqwest::Client::builder() .tls_backend_preconfigured(tls) + .redirect(redirect) .resolve_to_addrs("localhost", &loopback) .connect_timeout(Duration::from_secs(30)) .user_agent(concat!("OpenIDEA-HttpClient/", env!("CARGO_PKG_VERSION"))); diff --git a/crates/ide-plugin/Cargo.toml b/crates/ide-plugin/Cargo.toml index da304e85e..d58c78131 100644 --- a/crates/ide-plugin/Cargo.toml +++ b/crates/ide-plugin/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true [dependencies] ide-core.workspace = true +dunce.workspace = true ide-extension.workspace = true blake3.workspace = true serde.workspace = true @@ -16,6 +17,7 @@ thiserror.workspace = true tracing.workspace = true parking_lot.workspace = true semver = "1" +url = "2" wasmtime = { version = "49.0.4", default-features = false, features = ["runtime", "cranelift", "component-model", "std", "parallel-compilation", "async"] } wasmtime-wasi = { version = "49.0.4", default-features = false, features = ["p2"] } diff --git a/crates/ide-plugin/src/execution.rs b/crates/ide-plugin/src/execution.rs new file mode 100644 index 000000000..36c85bd39 --- /dev/null +++ b/crates/ide-plugin/src/execution.rs @@ -0,0 +1,153 @@ +//! Runs, keyed terminals and plugin-owned content editors (API 1.4). + +use serde::{Deserialize, Serialize}; +use specta::Type; + +/// ContentAction data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct ContentAction { + pub id: String, + pub label: String, + pub icon: Option, + pub primary: bool, + pub save: bool, +} + +/// ContentTab data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct ContentTab { + pub key: String, + pub title: String, + pub file_name: Option, + pub language: Option, + pub content: String, + pub read_only: bool, + pub actions: Vec, +} + +/// ContentDiff data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct ContentDiff { + pub left: String, + pub right: String, + pub left_label: Option, + pub right_label: Option, + pub title: Option, +} + +/// ContentTabReply data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct ContentTabReply { + pub content: Option, + pub saved: Option, + pub read_only: Option, + pub status: Option, + pub error: Option, + pub diff: Option, +} + +/// PluginRunConfiguration data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct PluginRunConfiguration { + pub id: String, + pub name: String, + pub program: String, + pub args: Vec, + pub cwd: Option, + pub test: bool, + pub framework: Option, +} + +/// RunExited data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct RunExited { + pub id: String, + pub config: String, + pub status: Option, + pub killed: bool, +} + +/// TerminalExited data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct TerminalExited { + pub id: String, + pub key: String, + pub status: Option, + pub closed: bool, +} + +/// ContentTabAction data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct ContentTabAction { + pub request_id: String, + pub key: String, + pub action: String, + pub content: String, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct TestStart { + pub id: String, + pub parent: Option, + pub name: String, + pub location: Option, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct TestPassed { + pub id: String, + pub duration_ms: Option, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct TestFailed { + pub id: String, + pub duration_ms: Option, + pub message: String, + pub expected: Option, + pub actual: Option, + pub location: Option, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct TestSkipped { + pub id: String, + pub reason: Option, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct TestOutput { + pub id: String, + pub text: String, +} + +/// A test update, carried by the optional test-results export. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum PluginTestEvent { + SuiteStarted(TestStart), + TestStarted(TestStart), + Passed(TestPassed), + Failed(TestFailed), + Skipped(TestSkipped), + Output(TestOutput), + Other { kind: String, data: String }, +} diff --git a/crates/ide-plugin/src/language_features.rs b/crates/ide-plugin/src/language_features.rs new file mode 100644 index 000000000..7530054d8 --- /dev/null +++ b/crates/ide-plugin/src/language_features.rs @@ -0,0 +1,109 @@ +//! Optional API 1.4 language features and embedded language ranges. +use ide_core::Range; +use serde::{Deserialize, Serialize}; +use specta::Type; +/// A parameter label, as text or UTF-16 offsets within the signature. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +pub enum ParameterLabel { + Text(String), + Offsets((u32, u32)), +} +/// FormatOptions data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FormatOptions { + pub tab_size: u32, + pub insert_spaces: bool, +} +/// Parameter data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct Parameter { + pub label: ParameterLabel, + pub documentation: Option, +} +/// Signature data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct Signature { + pub label: String, + pub documentation: Option, + pub parameters: Vec, +} +/// SignatureHelp data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct SignatureHelp { + pub signatures: Vec, + pub active_signature: u32, + pub active_parameter: u32, +} +/// Highlight data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct Highlight { + pub range: Range, + pub kind: String, +} +/// DocumentLink data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct DocumentLink { + pub range: Range, + pub target: String, +} +/// CodeLens data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct CodeLens { + pub range: Range, + pub title: String, + pub command: String, + pub argument: Option, +} +/// FoldRange data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct FoldRange { + pub start_line: u32, + pub end_line: u32, + pub kind: String, +} +/// ColorInfo data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct ColorInfo { + pub range: Range, + pub red: f32, + pub green: f32, + pub blue: f32, + pub alpha: f32, +} +/// DocumentSymbol data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct DocumentSymbol { + pub name: String, + pub detail: String, + pub kind: String, + pub range: Range, + pub selection_range: Range, + pub parent: Option, +} +/// GutterAction data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct GutterAction { + pub line: u32, + pub icon: Option, + pub tooltip: String, + pub command: String, + pub argument: Option, +} +/// Injection data. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct Injection { + pub range: Range, + pub language: String, +} diff --git a/crates/ide-plugin/src/lib.rs b/crates/ide-plugin/src/lib.rs index 1878bb790..6a000bfda 100644 --- a/crates/ide-plugin/src/lib.rs +++ b/crates/ide-plugin/src/lib.rs @@ -23,3 +23,10 @@ pub use runtime::{ pub use sandbox::{MAX_READ_BYTES, inside_root, read_text, resolve_in_root, resolve_new_in_root, resolve_real}; pub use types::*; pub use wasmtime::component::Component; + +mod execution; +pub use execution::*; + +pub use ide_core::remote_settings::PluginRunTarget; + +pub mod language_features; diff --git a/crates/ide-plugin/src/runtime.rs b/crates/ide-plugin/src/runtime.rs index 28fa745a3..6c1bda1dc 100644 --- a/crates/ide-plugin/src/runtime.rs +++ b/crates/ide-plugin/src/runtime.rs @@ -16,6 +16,8 @@ use crate::types::{ FieldValue, ServiceLog, ServiceLogToggle, ServicePane, ServiceTab, TableSort, View, ViewBody, ViewButton, ViewEvent, ViewField, ViewListItem, ViewTable, ViewTreeItem, Viewport, }; +use crate::wit_host::injections_bindings::exports::openidea::plugin::injections as wit_inj; +use crate::wit_host::language_features_bindings::exports::openidea::plugin::language_features as wit_lf; use crate::wit_host::{ bindings, commit_checks_bindings, details_bindings, endpoints_bindings, file_actions_bindings, hover_bindings, indexing_bindings, inlay_bindings, providers_bindings, semantic_bindings, syntax_bindings, views_bindings, @@ -33,6 +35,8 @@ use wasmtime::{Config, Engine, Store, StoreLimits, StoreLimitsBuilder, UpdateDea use wasmtime_wasi::p2::pipe::MemoryOutputPipe; use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +use crate::wit_host::run_configurations_bindings::exports::openidea::plugin::run_configurations as wit_configs; +use crate::wit_host::test_results_bindings::exports::openidea::plugin::test_results as wit_tests; use bindings::openidea::plugin::events as wit_events; use bindings::openidea::plugin::types as wit_types; use commit_checks_bindings::exports::openidea::plugin::commit_checker as wit_cc; @@ -50,7 +54,7 @@ use views_bindings::exports::openidea::plugin::view_provider as wit_view; use views_bindings::openidea::plugin::view_types as wit_vt; /// The API version of the world above, `major.minor` of [`host_api`]. -pub const API_VERSION: &str = "1.3"; +pub const API_VERSION: &str = "1.4"; const EPOCH_TICK: Duration = Duration::from_millis(10); pub(crate) const MAX_TEXT: usize = 1 << 20; pub(crate) const MAX_TITLE: usize = 256; @@ -62,6 +66,190 @@ pub const CANCELLED: &str = "was cancelled"; /// which routes notifications and status items to windows and edits to open /// documents. Capabilities are already checked when these are called. pub trait HostApi: Send + Sync { + /// Creates/replaces this plugin's non-secret project connection settings. + fn database_upsert(&self, root: &Path, source: crate::PluginDataSource) -> Result<(), HostError> { + let _ = (root, source); + Err(HostError::Invalid("Database subsystem is unavailable on this host".into())) + } + /// Removes only an owned data source and its credentials. + fn database_remove(&self, root: &Path, id: &str) -> Result<(), HostError> { + let _ = (root, id); + Err(HostError::Invalid("Database subsystem is unavailable on this host".into())) + } + /// Private ids of only this plugin's sources. + fn database_sources(&self, root: &Path) -> Result, HostError> { + let _ = root; + Err(HostError::Invalid("Database subsystem is unavailable on this host".into())) + } + /// Writes a password to the Database credential store, without a read path. + fn database_credential(&self, root: &Path, id: &str, password: String) -> Result<(), HostError> { + let _ = (root, id, password); + Err(HostError::Invalid("Database subsystem is unavailable on this host".into())) + } + /// Requests normal connection/introspection in the invoking project's window. + fn database_connect( + &self, + root: &Path, + client: &str, + id: &str, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + let _ = (root, client, id, cancel, timeout); + Err(HostError::Invalid("Database subsystem is unavailable on this host".into())) + } + /// Stdio argv only; no URL or bearer token. + fn mcp_stdio_command(&self, client: &str) -> Option> { + let _ = client; + None + } + + /// Registers a project-scoped wrapper after validating its process grant and pin. + fn register_run_target(&self, root: &Path, target: crate::PluginRunTarget) -> Result<(), HostError> { + let _ = (root, target); + Err(HostError::Failed("run targets unavailable".into())) + } + /// Withdraws this plugin's target, preserving the user's remembered prompt answer. + fn unregister_run_target(&self, root: &Path, id: &str) { + let _ = (root, id); + } + + /// Starts one of this plugin's configurations in a window. + fn start_run( + &self, + client: &str, + config: &str, + args: Vec, + debug: bool, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result { + let _ = (client, config, args, debug, cancel, timeout); + Err(HostError::Failed("runs unavailable".into())) + } + /// Opens/reuses a keyed terminal, retained across idle instance reclamation. + fn open_terminal_session( + &self, + client: &str, + key: String, + title: String, + cmd: ProcessCommand, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result { + let _ = (client, key, title, cmd, cancel, timeout); + Err(HostError::Failed("terminals unavailable".into())) + } + /// Writes only to an owned, live terminal. + fn write_terminal( + &self, + client: &str, + key: &str, + text: String, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + let _ = (client, key, text, cancel, timeout); + Err(HostError::Failed("terminals unavailable".into())) + } + /// Opens a content tab through the shared editor implementation. + fn open_content_tab( + &self, + client: &str, + tab: crate::ContentTab, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + let _ = (client, tab, cancel, timeout); + Err(HostError::Failed("content tabs unavailable".into())) + } + /// Answers a content action in the invoking window. + fn reply_content_tab(&self, client: &str, request: &str, reply: crate::ContentTabReply) -> Result<(), HostError> { + let _ = (client, request, reply); + Err(HostError::Failed("content tabs unavailable".into())) + } + /// The invoking window's editors. + fn open_editors(&self, client: &str) -> Result, HostError> { + let _ = client; + Ok(Vec::new()) + } + /// Opens a validated file and optionally selects a range. + fn editor_open(&self, client: &str, path: &Path, selection: Option) -> Result<(), HostError> { + let _ = (client, path, selection); + Err(HostError::Failed("editor unavailable".into())) + } + /// Changes the selections, or reveals a range, only in an editor of this window. + fn editor_ranges( + &self, + client: &str, + path: &Path, + ranges: Vec, + reveal: bool, + ) -> Result<(), HostError> { + let _ = (client, path, ranges, reveal); + Err(HostError::Failed("editor unavailable".into())) + } + /// Find in Files with the project's document overlay. + fn search_text( + &self, + client: Option<&str>, + root: &Path, + query: crate::TextQuery, + limit: u32, + cancel: &CancellationToken, + deadline: Instant, + ) -> Result { + let _ = (client, root, query, limit, cancel, deadline); + Err(HostError::Failed("search unavailable".into())) + } + /// Window-reported Problems view snapshot. + fn problems(&self, client: &str, path: Option<&Path>) -> Result, HostError> { + let _ = (client, path); + Ok(Vec::new()) + } + /// Replace or clear an editor decoration set. + fn decorations(&self, path: Option<&Path>, set: &str, items: Vec) -> Result<(), HostError> { + let _ = (path, set, items); + Err(HostError::Failed("decorations unavailable".into())) + } + /// Replace or clear a tree decoration set. + fn file_decorations( + &self, + path: Option<&Path>, + set: &str, + item: Option, + ) -> Result<(), HostError> { + let _ = (path, set, item); + Err(HostError::Failed("decorations unavailable".into())) + } + /// Trusted repository history. + fn vcs_log( + &self, + root: &Path, + path: Option<&str>, + limit: u32, + cancel: &CancellationToken, + ) -> Result, HostError> { + let _ = (root, path, limit, cancel); + Err(HostError::Failed("git unavailable".into())) + } + /// Trusted repository attribution, including unsaved documents. + fn vcs_blame(&self, client: Option<&str>, root: &Path, path: &str) -> Result, HostError> { + let _ = (client, root, path); + Err(HostError::Failed("git unavailable".into())) + } + /// Trusted repository working-tree patch, bounded to 4 MiB. + fn vcs_diff( + &self, + root: &Path, + path: Option<&str>, + against: Option<&str>, + cancel: &CancellationToken, + ) -> Result, HostError> { + let _ = (root, path, against, cancel); + Err(HostError::Failed("git unavailable".into())) + } + /// Shows a notification; `actions` name commands the plugin declares. fn notify( &self, @@ -101,8 +289,45 @@ pub trait HostApi: Send + Sync { let _ = (client, view); Ok(()) } + /// Whether this stable id is a command declared by this plugin. + fn owns_command(&self, id: &str) -> bool { + let _ = id; + false + } + /// Executes a stable command in the invoking window and waits for its result. + fn execute_command( + &self, + invocation: &Invocation, + id: &str, + arg: Option, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result, HostError> { + let _ = (invocation, id, arg, cancel, timeout); + Err(HostError::NotFound("command".into())) + } + /// Appends text, or clears (`None`), in a plugin-owned output channel. + fn output_write(&self, channel: &str, text: Option<&str>) -> Result<(), HostError> { + let _ = (channel, text); + Err(HostError::Failed("output unavailable".into())) + } + /// Reveals an output channel in the invoking window. + fn output_reveal(&self, client: &str, channel: &str) -> Result<(), HostError> { + let _ = (client, channel); + Err(HostError::Failed("output unavailable".into())) + } + /// Checks manifest minimum API requirements for new declarations or subscriptions. + fn require_api(&self, what: &str, minor: u64) -> Result<(), HostError> { + let _ = (what, minor); + Ok(()) + } /// Sets (`Some`) or removes the plugin's status bar item `id`. fn status_item(&self, id: &str, item: Option); + /// Sets or removes the status override in the invoking window. + fn window_status_item(&self, client: &str, id: &str, item: Option) -> Result<(), HostError> { + let _ = (client, id, item); + Err(HostError::Invalid("window status unavailable".into())) + } /// Sets (`Some`, validated JSON text) or removes the plugin's context key, /// for window `client` (every window if `None`). fn set_context(&self, client: Option<&str>, key: &str, value: Option) -> Result<(), HostError> { @@ -181,6 +406,62 @@ pub trait HostApi: Send + Sync { /// Performs `request` (its host is granted), filling in secret placeholders. fn fetch(&self, request: HttpRequest, cancel: &CancellationToken) -> Result; fn secret_is_set(&self, name: &str) -> bool; + + /// Stores or deletes a secret declared by this plugin; never returns its value. + fn secret_set(&self, name: &str, value: Option<&str>) -> Result<(), HostError> { + let _ = (name, value); + Err(HostError::NotFound("secret".into())) + } + /// Creates a desktop loopback listener and returns its handle and redirect URI. + fn begin_loopback(&self, client: &str, path: &str) -> Result<(u64, String), HostError> { + let _ = (client, path); + Err(HostError::Failed("loopback unavailable".into())) + } + /// Awaits a browser redirect, excluded from the caller's execution budget. + fn wait_loopback( + &self, + id: u64, + timeout: Duration, + cancel: &CancellationToken, + ) -> Result, HostError> { + let _ = (id, timeout, cancel); + Err(HostError::NotFound("loopback".into())) + } + /// Releases a desktop listener when its resource or plugin stops. + fn close_loopback(&self, id: u64) { + let _ = id; + } + /// Connects to an approved host-local socket; owned by this plugin instance. + fn ipc_connect(&self, name: &str, cancel: &CancellationToken, timeout: Duration) -> Result { + let _ = (name, cancel, timeout); + Err(HostError::Failed("IPC unavailable".into())) + } + /// Writes to an owned IPC connection. + fn ipc_write( + &self, + id: u64, + bytes: Vec, + cancel: &CancellationToken, + timeout: Duration, + ) -> Result<(), HostError> { + let _ = (id, bytes, cancel, timeout); + Err(HostError::NotFound("connection".into())) + } + /// Reads from an owned IPC connection, empty on timeout or EOF. + fn ipc_read( + &self, + id: u64, + max: usize, + timeout: Duration, + cancel: &CancellationToken, + ) -> Result, HostError> { + let _ = (id, max, timeout, cancel); + Err(HostError::NotFound("connection".into())) + } + /// Closes an owned IPC connection, including when its resource is dropped. + fn ipc_close(&self, id: u64) { + let _ = id; + } /// Runs the approved file of `process:` with `cmd.env` added to /// the inherited environment, and waits. fn run_process( @@ -631,6 +912,8 @@ pub struct Runtime { /// What the component exports: the `plugin` world, plus optional interfaces. struct Exports { + language_features: Option, + injections: Option, plugin: bindings::Plugin, syntax: Option, providers: Option, @@ -644,6 +927,9 @@ struct Exports { commit_checks: Option, inlay: Option, endpoints: Option, + run_configurations: Option, + test_results: Option, + mcp: Option, } /// Most results one syntax call may return (more are dropped). @@ -653,6 +939,30 @@ const MAX_BATCH: usize = 1000; fn event_to_wit(e: WorkbenchEvent) -> wit_events::Event { match e { + WorkbenchEvent::RunExited(value) => wit_events::Event::Other(wit_types::Extension { + kind: "run-exited".into(), + data: serde_json::to_string(&value).unwrap(), + }), + WorkbenchEvent::TerminalExited(value) => wit_events::Event::Other(wit_types::Extension { + kind: "terminal-exited".into(), + data: serde_json::to_string(&value).unwrap(), + }), + WorkbenchEvent::ContentTabAction(value) => wit_events::Event::Other(wit_types::Extension { + kind: "content-tab-action".into(), + data: serde_json::to_string(&value).unwrap(), + }), + WorkbenchEvent::FileClosed(p) => wit_events::Event::Other(wit_types::Extension { + kind: "file-closed".into(), + data: serde_json::to_string(&p).expect("path serializes"), + }), + WorkbenchEvent::SettingsChanged(keys) => wit_events::Event::Other(wit_types::Extension { + kind: "settings-changed".into(), + data: serde_json::to_string(&keys).expect("keys serialize"), + }), + WorkbenchEvent::InputModeChanged(mode) => wit_events::Event::Other(wit_types::Extension { + kind: "input-mode-changed".into(), + data: serde_json::to_string(&mode).expect("mode serializes"), + }), WorkbenchEvent::FileOpened(p) => wit_events::Event::FileOpened(p), WorkbenchEvent::FileSaved(p) => wit_events::Event::FileSaved(p), WorkbenchEvent::FilesChanged(batch) => wit_events::Event::Other(wit_types::Extension { @@ -737,7 +1047,19 @@ impl Runtime { let commit_check_indices = wit_cc::GuestIndices::new(&pre).ok(); let inlay_indices = wit_inlay::GuestIndices::new(&pre).ok(); let endpoint_indices = wit_endpoints::GuestIndices::new(&pre).ok(); + let configs_indices = wit_configs::GuestIndices::new(&pre).ok(); + let tests_indices = wit_tests::GuestIndices::new(&pre).ok(); + let lf_indices = wit_lf::GuestIndices::new(&pre).ok(); + let inj_indices = wit_inj::GuestIndices::new(&pre).ok(); + let mcp_indices = + crate::wit_host::mcp_bindings::exports::openidea::plugin::mcp_provider::GuestIndices::new(&pre).ok(); let instance = pre.instantiate(&mut store).map_err(api_hint)?; + let mcp = mcp_indices.map(|i| i.load(&mut store, &instance)).transpose().map_err(api_hint)?; + let run_configurations = + configs_indices.map(|i| i.load(&mut store, &instance)).transpose().map_err(api_hint)?; + let test_results = tests_indices.map(|i| i.load(&mut store, &instance)).transpose().map_err(api_hint)?; + let language_features = lf_indices.map(|i| i.load(&mut store, &instance)).transpose().map_err(api_hint)?; + let injections = inj_indices.map(|i| i.load(&mut store, &instance)).transpose().map_err(api_hint)?; let plugin = plugin_indices.load(&mut store, &instance).map_err(api_hint)?; let syntax = match syntax_indices { Some(indices) => Some(indices.load(&mut store, &instance).map_err(api_hint)?), @@ -790,6 +1112,8 @@ impl Runtime { Ok(Self { store, exports: Exports { + language_features, + injections, plugin, syntax, providers, @@ -802,6 +1126,9 @@ impl Runtime { commit_checks, inlay, endpoints, + run_configurations, + test_results, + mcp, file_actions, }, limits: setup.limits, @@ -809,6 +1136,55 @@ impl Runtime { }) } + /// Complete configurations from the optional API 1.4 export. + pub fn run_configurations( + &mut self, + root: String, + timeout: Duration, + ) -> Result, CallError> { + if self.exports.run_configurations.is_none() { + return Err(CallError::missing_export("run-configurations")); + } + let invocation = Invocation { workspace_root: Some(root.clone().into()), ..Default::default() }; + let items = self.call(invocation, timeout, |e, store| { + e.run_configurations.as_ref().unwrap().call_configurations(store, &root) + })?; + if items.len() > 1000 { + return Err(CallError::Guest(GuestError::Invalid("too many run configurations".into()))); + } + Ok(items + .into_iter() + .map(|c| crate::PluginRunConfiguration { + id: c.id, + name: c.name, + program: c.program, + args: c.args, + cwd: c.cwd, + test: c.test, + framework: c.framework, + }) + .collect()) + } + /// One editor-budget batch of a plugin test parser. + pub fn parse_test_results( + &mut self, + config: String, + chunk: String, + invocation: Invocation, + timeout: Duration, + ) -> Result, CallError> { + if self.exports.test_results.is_none() { + return Err(CallError::missing_export("test-results")); + } + let items = self.call(invocation, timeout, |e, store| { + e.test_results.as_ref().unwrap().call_parse(store, &config, &chunk) + })?; + if items.len() > 10000 { + return Err(CallError::Guest(GuestError::Invalid("too many test events".into()))); + } + Ok(items.into_iter().map(test_event_from_wit).collect()) + } + /// The component exports `syntax-provider`. pub fn has_syntax(&self) -> bool { self.exports.syntax.is_some() @@ -1809,6 +2185,392 @@ fn event_to_wit_view(e: ViewEvent) -> wit_vt::ViewEvent { } } +fn test_event_from_wit(event: wit_tests::TestEvent) -> crate::PluginTestEvent { + use crate::{PluginTestEvent as E, TestFailed, TestOutput, TestPassed, TestSkipped, TestStart}; + let location = |l: wit_types::Location| crate::Location { + path: l.path.into(), + range: ide_core::Range::new( + ide_core::Pos::new(l.range.start.line, l.range.start.character), + ide_core::Pos::new(l.range.end.line, l.range.end.character), + ), + }; + let start = |v: wit_tests::TestStart| TestStart { + id: v.id, + parent: v.parent, + name: v.name, + location: v.location.map(location), + }; + match event { + wit_tests::TestEvent::SuiteStarted(v) => E::SuiteStarted(start(v)), + wit_tests::TestEvent::TestStarted(v) => E::TestStarted(start(v)), + wit_tests::TestEvent::Passed(v) => E::Passed(TestPassed { id: v.id, duration_ms: v.duration_ms }), + wit_tests::TestEvent::Failed(v) => E::Failed(TestFailed { + id: v.id, + duration_ms: v.duration_ms, + message: v.message, + expected: v.expected, + actual: v.actual, + location: v.location.map(location), + }), + wit_tests::TestEvent::Skipped(v) => E::Skipped(TestSkipped { id: v.id, reason: v.reason }), + wit_tests::TestEvent::Output(v) => E::Output(TestOutput { id: v.id, text: v.text }), + wit_tests::TestEvent::Other(v) => E::Other { kind: v.kind, data: v.data }, + } +} + +fn parameter_from(v: wit_lf::Parameter) -> crate::language_features::Parameter { + crate::language_features::Parameter { + label: parameter_label_from(v.label), + documentation: v.documentation.map(|v| crate::runtime::clip(v, 65536)), + } +} +fn signature_from(v: wit_lf::Signature) -> crate::language_features::Signature { + crate::language_features::Signature { + label: crate::runtime::clip(v.label, 65536), + documentation: v.documentation.map(|v| crate::runtime::clip(v, 65536)), + parameters: v.parameters.into_iter().take(10000).map(parameter_from).collect(), + } +} +fn signature_help_from(v: wit_lf::SignatureHelpResult) -> crate::language_features::SignatureHelp { + crate::language_features::SignatureHelp { + signatures: v.signatures.into_iter().take(10000).map(signature_from).collect(), + active_signature: v.active_signature, + active_parameter: v.active_parameter, + } +} +fn highlight_from(v: wit_lf::Highlight) -> crate::language_features::Highlight { + crate::language_features::Highlight { range: range_from_wit(v.range), kind: crate::runtime::clip(v.kind, 65536) } +} +fn document_link_from(v: wit_lf::DocumentLink) -> crate::language_features::DocumentLink { + crate::language_features::DocumentLink { + range: range_from_wit(v.range), + target: crate::runtime::clip(v.target, 65536), + } +} +fn code_lens_from(v: wit_lf::CodeLens) -> crate::language_features::CodeLens { + crate::language_features::CodeLens { + range: range_from_wit(v.range), + title: crate::runtime::clip(v.title, 65536), + command: crate::runtime::clip(v.command, 65536), + argument: v.argument.map(|v| crate::runtime::clip(v, 65536)), + } +} +fn fold_range_from(v: wit_lf::FoldRange) -> crate::language_features::FoldRange { + crate::language_features::FoldRange { + start_line: v.start_line, + end_line: v.end_line, + kind: crate::runtime::clip(v.kind, 65536), + } +} +fn color_info_from(v: wit_lf::ColorInfo) -> crate::language_features::ColorInfo { + crate::language_features::ColorInfo { + range: range_from_wit(v.range), + red: v.red, + green: v.green, + blue: v.blue, + alpha: v.alpha, + } +} +fn document_symbol_from(v: wit_lf::DocumentSymbol) -> crate::language_features::DocumentSymbol { + crate::language_features::DocumentSymbol { + name: crate::runtime::clip(v.name, 65536), + detail: crate::runtime::clip(v.detail, 65536), + kind: crate::runtime::clip(v.kind, 65536), + range: range_from_wit(v.range), + selection_range: range_from_wit(v.selection_range), + parent: v.parent, + } +} +fn gutter_action_from(v: wit_lf::GutterAction) -> crate::language_features::GutterAction { + crate::language_features::GutterAction { + line: v.line, + icon: v.icon.map(|v| crate::runtime::clip(v, 65536)), + tooltip: crate::runtime::clip(v.tooltip, 65536), + command: crate::runtime::clip(v.command, 65536), + argument: v.argument.map(|v| crate::runtime::clip(v, 65536)), + } +} +fn parameter_label_from(v: wit_lf::ParameterLabel) -> crate::language_features::ParameterLabel { + use crate::language_features::ParameterLabel as P; + match v { + wit_lf::ParameterLabel::Text(t) => P::Text(clip(t, 4096)), + wit_lf::ParameterLabel::Offsets(t) => P::Offsets(t), + } +} +impl Runtime { + fn language_feature_call( + &mut self, + timeout: Duration, + f: impl FnOnce(&wit_lf::Guest, &mut Store) -> wasmtime::Result, + ) -> Result { + if self.exports.language_features.is_none() { + return Err(CallError::missing_export("language-features")); + } + self.call(Invocation::default(), timeout, |e, s| f(e.language_features.as_ref().expect("checked"), s)) + } + pub fn language_format( + &mut self, + doc: &ProviderDocument, + range: Option, + options: crate::language_features::FormatOptions, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| { + p.call_format( + s, + &d, + range.map(range_to_wit), + wit_lf::FormatOptions { tab_size: options.tab_size, insert_spaces: options.insert_spaces }, + ) + })?; + let result = result.map_err(|e| CallError::Guest(e.into()))?; + Ok(result.into_iter().take(10000).map(edit_from_wit).collect()) + } + pub fn language_signature_help( + &mut self, + doc: &ProviderDocument, + at: ide_core::Pos, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_signature_help(s, &d, pos_to_wit(at)))?; + Ok(result.map(signature_help_from)) + } + pub fn language_document_highlights( + &mut self, + doc: &ProviderDocument, + at: ide_core::Pos, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_document_highlights(s, &d, pos_to_wit(at)))?; + Ok(result.into_iter().take(10000).map(highlight_from).collect()) + } + pub fn language_document_links( + &mut self, + doc: &ProviderDocument, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_document_links(s, &d))?; + Ok(result.into_iter().take(10000).map(document_link_from).collect()) + } + pub fn language_code_lenses( + &mut self, + doc: &ProviderDocument, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_code_lenses(s, &d))?; + Ok(result.into_iter().take(10000).map(code_lens_from).collect()) + } + pub fn language_selection_ranges( + &mut self, + doc: &ProviderDocument, + at: ide_core::Pos, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_selection_ranges(s, &d, pos_to_wit(at)))?; + Ok(result.into_iter().take(10000).map(range_from_wit).collect()) + } + pub fn language_folding( + &mut self, + doc: &ProviderDocument, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_folding(s, &d))?; + Ok(result.into_iter().take(10000).map(fold_range_from).collect()) + } + pub fn language_colors( + &mut self, + doc: &ProviderDocument, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_colors(s, &d))?; + Ok(result.into_iter().take(10000).map(color_info_from).collect()) + } + pub fn language_color_presentations( + &mut self, + doc: &ProviderDocument, + color: crate::language_features::ColorInfo, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| { + p.call_color_presentations( + s, + &d, + wit_lf::ColorInfo { + range: range_to_wit(color.range), + red: color.red, + green: color.green, + blue: color.blue, + alpha: color.alpha, + }, + ) + })?; + Ok(result.into_iter().take(10000).map(|v| crate::runtime::clip(v, 65536)).collect()) + } + pub fn language_document_symbols( + &mut self, + doc: &ProviderDocument, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_document_symbols(s, &d))?; + Ok(result.into_iter().take(10000).map(document_symbol_from).collect()) + } + pub fn language_implementation( + &mut self, + doc: &ProviderDocument, + at: ide_core::Pos, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_implementation(s, &d, pos_to_wit(at)))?; + Ok(result.into_iter().take(10000).map(location_from_wit).collect()) + } + pub fn language_type_definition( + &mut self, + doc: &ProviderDocument, + at: ide_core::Pos, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_type_definition(s, &d, pos_to_wit(at)))?; + Ok(result.into_iter().take(10000).map(location_from_wit).collect()) + } + pub fn language_gutter_actions( + &mut self, + doc: &ProviderDocument, + timeout: Duration, + ) -> Result, CallError> { + let d = wit_lf::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.language_feature_call(timeout, |p, s| p.call_gutter_actions(s, &d))?; + Ok(result.into_iter().take(10000).map(gutter_action_from).collect()) + } + pub fn provide_injections( + &mut self, + doc: &ProviderDocument, + timeout: Duration, + ) -> Result, CallError> { + if self.exports.injections.is_none() { + return Err(CallError::missing_export("injections")); + } + let d = wit_inj::Document { + path: to_ipc(&doc.path), + language: doc.language.clone(), + version: doc.version, + text: doc.text.clone(), + }; + let result = self.call(Invocation::default(), timeout, |e, s| { + e.injections.as_ref().expect("checked").call_injections(s, &d) + })?; + Ok(result + .into_iter() + .take(10000) + .map(|i| crate::language_features::Injection { + range: range_from_wit(i.range), + language: clip(i.language, 64), + }) + .collect()) + } +} + +impl Runtime { + /// Optional MCP export, with the same wall-clock and fuel budgets as commands. + pub fn mcp_call(&mut self, tool: &str, arguments: &str, invocation: Invocation) -> Result { + if self.exports.mcp.is_none() { + return Err(CallError::missing_export("mcp-provider")); + } + let result = self.call(invocation, self.limits.timeout, |e, store| { + let ctx = store.data_mut().new_invocation()?; + let result = e.mcp.as_ref().unwrap().call_call( + &mut *store, + tool, + arguments, + wasmtime::component::Resource::new_borrow(ctx.rep()), + ); + store.data_mut().drop_invocation(ctx); + result + })?; + let result = result.map_err(|e| CallError::Guest(e.into()))?; + if result.len() > 4 << 20 { + return Err(CallError::Guest(GuestError::Invalid("MCP result exceeds 4 MiB".into()))); + } + Ok(result) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/ide-plugin/src/sandbox.rs b/crates/ide-plugin/src/sandbox.rs index 153cae609..0e320cf3c 100644 --- a/crates/ide-plugin/src/sandbox.rs +++ b/crates/ide-plugin/src/sandbox.rs @@ -17,7 +17,7 @@ fn joined_path(root: &Path, path: &str) -> Result<(PathBuf, PathBuf), String> { if path.is_empty() { return Err("empty path".into()); } - let root = root.canonicalize().map_err(|e| format!("project root unavailable: {e}"))?; + let root = dunce::canonicalize(root).map_err(|e| format!("project root unavailable: {e}"))?; let requested = Path::new(path); let joined = if requested.is_absolute() { requested.to_path_buf() } else { root.join(requested) }; Ok((root, joined)) @@ -31,7 +31,7 @@ pub fn resolve_real(joined: &Path) -> PathBuf { let mut tail = Vec::new(); let mut candidate = joined.to_path_buf(); let mut real = loop { - match candidate.canonicalize() { + match dunce::canonicalize(&candidate) { Ok(real) => break real, Err(_) => { let Some(last) = candidate.components().next_back() else { @@ -58,7 +58,7 @@ pub fn resolve_real(joined: &Path) -> PathBuf { /// path — after following `..` and symlinks — stays inside `root`. pub fn resolve_in_root(root: &Path, path: &str) -> Result { let (root, joined) = joined_path(root, path)?; - let real = joined.canonicalize().map_err(|_| format!("{path}: no such file"))?; + let real = dunce::canonicalize(&joined).map_err(|_| format!("{path}: no such file"))?; if !real.starts_with(&root) { return Err(format!("{path}: access outside the project root is not allowed")); } @@ -83,7 +83,7 @@ pub fn resolve_new_in_root(root: &Path, path: &str) -> Result { if path.is_empty() { return Err("empty path".into()); } - let root = root.canonicalize().map_err(|e| format!("project root unavailable: {e}"))?; + let root = dunce::canonicalize(root).map_err(|e| format!("project root unavailable: {e}"))?; let requested = Path::new(path); let joined = if requested.is_absolute() { requested.to_path_buf() } else { root.join(requested) }; let mut lexical = PathBuf::new(); @@ -108,7 +108,7 @@ pub fn resolve_new_in_root(root: &Path, path: &str) -> Result { missing.push(dir); existing = above; } - let mut real = existing.canonicalize().map_err(|e| format!("{path}: {e}"))?; + let mut real = dunce::canonicalize(existing).map_err(|e| format!("{path}: {e}"))?; real.extend(missing.into_iter().rev()); real.push(name); if !real.starts_with(&root) { @@ -207,9 +207,8 @@ fn resolve(granted: &[Capability], path: &str, wanted: Wanted) -> Result asked.canonicalize().map_err(|_| Refusal::Invalid(format!("{path}: no such file")))?, - Wanted::Write => asked - .canonicalize() + Wanted::Read => dunce::canonicalize(&asked).map_err(|_| Refusal::Invalid(format!("{path}: no such file")))?, + Wanted::Write => dunce::canonicalize(&asked) .or_else(|_| resolve_unsaved(&asked)) .map_err(|e| Refusal::Invalid(format!("{path}: {e}")))?, }; @@ -223,7 +222,7 @@ fn resolve(granted: &[Capability], path: &str, wanted: Wanted) -> Result strip_verbatim(&real), Err(_) => strip_verbatim(&expand_tilde(base)), }; @@ -246,20 +245,73 @@ fn resolve(granted: &[Capability], path: &str, wanted: Wanted) -> Result Result { let name = asked.file_name().ok_or_else(|| "not a file name".to_string())?; let parent = asked.parent().ok_or_else(|| "no directory to put it in".to_string())?; - let dir = parent.canonicalize().map_err(|_| "its directory does not exist".to_string())?; + let dir = dunce::canonicalize(parent).map_err(|_| "its directory does not exist".to_string())?; Ok(dir.join(name)) } -/// `\\?\C:\…` (what `canonicalize` returns on Windows) as `C:\…`. +/// Simplifies Windows drive paths only when the plain form preserves their meaning. fn strip_verbatim(path: &Path) -> String { - let text = path.to_string_lossy(); - text.strip_prefix(r"\\?\").unwrap_or(&text).to_owned() + dunce::simplified(path).to_string_lossy().into_owned() } #[cfg(test)] mod tests { use super::*; + #[cfg(windows)] + #[test] + fn grant_paths_simplify_drive_paths_and_preserve_unc_prefixes() { + assert_eq!(strip_verbatim(Path::new(r"\\?\C:\project\a.txt")), r"C:\project\a.txt"); + for path in [r"C:\project\a.txt", r"\\?\UNC\server\share\a.txt", r"\\server\share\a.txt"] { + assert_eq!(strip_verbatim(Path::new(path)), path); + } + } + + #[cfg(windows)] + #[test] + fn resolvers_return_plain_paths_for_plain_and_verbatim_inputs() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().join("project"); + std::fs::create_dir(&root).unwrap(); + std::fs::write(root.join("a.txt"), "text").unwrap(); + let plain = dunce::canonicalize(&root).unwrap(); + let verbatim = std::fs::canonicalize(&root).unwrap(); + assert_ne!(plain, verbatim); + let file = plain.join("a.txt"); + let missing = plain.join("new/deep/a.txt"); + for root in [&plain, &verbatim] { + for path in ["a.txt", file.to_str().unwrap(), verbatim.join("a.txt").to_str().unwrap()] { + assert_eq!(resolve_in_root(root, path).unwrap(), file); + assert_eq!(resolve_new_in_root(root, path).unwrap(), file); + assert!(inside_root(root, path).unwrap()); + } + for path in ["new/deep/a.txt", missing.to_str().unwrap(), verbatim.join("new/deep/a.txt").to_str().unwrap()] + { + assert_eq!(resolve_new_in_root(root, path).unwrap(), missing); + assert_eq!(resolve_real(&root.join(path)), missing); + assert!(inside_root(root, path).unwrap()); + } + let outside = tmp.path().join("outside.txt"); + std::fs::write(&outside, "secret").unwrap(); + assert!(resolve_in_root(root, outside.to_str().unwrap()).is_err()); + assert!(resolve_new_in_root(root, outside.to_str().unwrap()).is_err()); + } + for root in [&plain, &verbatim] { + let read = Capability::FsRead(root.to_string_lossy().into_owned()); + let write = Capability::FsWrite(root.to_string_lossy().into_owned()); + for path in [file.clone(), verbatim.join("a.txt")] { + assert_eq!(resolve_granted(std::slice::from_ref(&read), path.to_str().unwrap()).unwrap(), file); + assert_eq!(resolve_writable(std::slice::from_ref(&write), path.to_str().unwrap()).unwrap(), file); + } + for path in [plain.join("new.txt"), verbatim.join("new.txt")] { + assert_eq!( + resolve_writable(std::slice::from_ref(&write), path.to_str().unwrap()).unwrap(), + plain.join("new.txt") + ); + } + } + } + #[test] fn confines_reads_to_root() { let tmp = tempfile::tempdir().unwrap(); @@ -310,7 +362,7 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let root = tmp.path().join("proj"); std::fs::create_dir_all(root.join("src")).unwrap(); - let root_real = root.canonicalize().unwrap(); + let root_real = dunce::canonicalize(&root).unwrap(); assert_eq!(resolve_new_in_root(&root, "src/new/a.txt").unwrap(), root_real.join("src/new/a.txt")); assert_eq!(resolve_new_in_root(&root, "src/../b.txt").unwrap(), root_real.join("b.txt")); @@ -329,7 +381,7 @@ mod tests { std::fs::write(root.join("target.txt"), "t").unwrap(); std::os::unix::fs::symlink(root.join("target.txt"), root.join("link.txt")).unwrap(); std::os::unix::fs::symlink(tmp.path(), root.join("out")).unwrap(); - let root_real = root.canonicalize().unwrap(); + let root_real = dunce::canonicalize(&root).unwrap(); assert_eq!(resolve_new_in_root(&root, "link.txt").unwrap(), root_real.join("link.txt")); assert!(resolve_new_in_root(&root, "out/x.txt").unwrap_err().contains("outside")); @@ -362,13 +414,13 @@ mod tests { /// The grant a manifest author would write for `dir`: the plain path, no /// `\\?\` prefix. fn grant_for(dir: &Path) -> Capability { - Capability::FsRead(strip_verbatim(&dir.canonicalize().unwrap())) + Capability::FsRead(strip_verbatim(&dunce::canonicalize(dir).unwrap())) } fn text_at(dir: &Path, name: &str, body: &str) -> PathBuf { let file = dir.join(name); std::fs::write(&file, body).unwrap(); - file.canonicalize().unwrap() + dunce::canonicalize(file).unwrap() } #[test] diff --git a/crates/ide-plugin/src/types.rs b/crates/ide-plugin/src/types.rs index 81c4f0952..c32a11eef 100644 --- a/crates/ide-plugin/src/types.rs +++ b/crates/ide-plugin/src/types.rs @@ -25,6 +25,8 @@ pub struct EditorContext { /// Where a command or event batch comes from: the window and its project. #[derive(Clone, Debug, Default)] pub struct Invocation { + /// Plugin instances waiting on this command chain; never re-enter one. + pub command_chain: Vec, pub client: Option, pub workspace_root: Option, /// Settings actions can read the global layer while retaining their project context. @@ -35,7 +37,7 @@ pub struct Invocation { } /// A failed host call, as the plugin sees it (WIT `types.error`). -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum HostError { /// Not granted; names the capability. Denied(String), @@ -98,6 +100,12 @@ pub enum Prompt { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Type)] #[serde(rename_all = "kebab-case")] pub enum EventKind { + FileClosed, + SettingsChanged, + InputModeChanged, + RunExited, + TerminalExited, + ContentTabAction, FileOpened, FileSaved, FilesChanged, @@ -108,7 +116,13 @@ pub enum EventKind { } impl EventKind { - pub const ALL: [Self; 7] = [ + pub const ALL: [Self; 13] = [ + Self::FileClosed, + Self::SettingsChanged, + Self::InputModeChanged, + Self::RunExited, + Self::TerminalExited, + Self::ContentTabAction, Self::FileOpened, Self::FileSaved, Self::FilesChanged, @@ -120,6 +134,12 @@ impl EventKind { pub fn name(self) -> &'static str { match self { + Self::FileClosed => "file-closed", + Self::SettingsChanged => "settings-changed", + Self::InputModeChanged => "input-mode-changed", + Self::RunExited => "run-exited", + Self::TerminalExited => "terminal-exited", + Self::ContentTabAction => "content-tab-action", Self::FileOpened => "file-opened", Self::FileSaved => "file-saved", Self::FilesChanged => "files-changed", @@ -138,8 +158,8 @@ impl EventKind { /// manifest declares). pub fn capability(self) -> Option { match self { - Self::ActionPerformed => Some(ide_extension::Capability::InputEvents), - Self::Scheduled => None, + Self::ActionPerformed | Self::InputModeChanged => Some(ide_extension::Capability::InputEvents), + Self::Scheduled | Self::RunExited | Self::TerminalExited | Self::ContentTabAction => None, _ => Some(ide_extension::Capability::Events), } } @@ -188,10 +208,24 @@ pub struct FilesChanged { pub removed: Vec, } +/// The focused editor's input mode and optional visual sub-mode. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +pub struct InputMode { + pub path: String, + pub mode: String, + pub sub: Option, +} + /// A workbench event delivered to plugins. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase", tag = "kind", content = "value")] pub enum WorkbenchEvent { + RunExited(crate::RunExited), + TerminalExited(crate::TerminalExited), + ContentTabAction(crate::ContentTabAction), + FileClosed(String), + SettingsChanged(Vec), + InputModeChanged(InputMode), FileOpened(String), FileSaved(String), FilesChanged(FilesChanged), @@ -204,6 +238,12 @@ pub enum WorkbenchEvent { impl WorkbenchEvent { pub fn kind(&self) -> EventKind { match self { + Self::RunExited(_) => EventKind::RunExited, + Self::TerminalExited(_) => EventKind::TerminalExited, + Self::ContentTabAction(_) => EventKind::ContentTabAction, + Self::FileClosed(_) => EventKind::FileClosed, + Self::SettingsChanged(_) => EventKind::SettingsChanged, + Self::InputModeChanged(_) => EventKind::InputModeChanged, Self::FileOpened(_) => EventKind::FileOpened, Self::FileSaved(_) => EventKind::FileSaved, Self::FilesChanged(_) => EventKind::FilesChanged, @@ -350,6 +390,8 @@ pub struct HttpRequest { pub headers: Vec<(String, String)>, pub body: Option>, pub timeout: Duration, + /// Grants checked again before following a redirect. + pub grants: Vec, } #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -371,6 +413,8 @@ pub struct ProcessOutput { /// the environment to add to the inherited one (WIT `process.command`). #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct ProcessCommand { + /// The invoking project whose executable override applies, independent of cwd. + pub approval_root: Option, pub program: String, pub args: Vec, pub env: Vec<(String, String)>, @@ -388,23 +432,27 @@ pub struct ProcessChunk { /// Where `net:` requests may go: the URL's host when it is `https`, or /// `http` to the loopback interface. pub fn net_host(url: &str) -> Result { - let (scheme, rest) = url.split_once("://").ok_or_else(|| format!("{url:?} is not an absolute URL"))?; - let authority = rest.split(['/', '?', '#']).next().unwrap_or_default(); - let authority = authority.rsplit_once('@').map_or(authority, |(_, h)| h); - let host = if let Some(v6) = authority.strip_prefix('[') { - v6.split(']').next().unwrap_or_default().to_owned() - } else { - authority.split(':').next().unwrap_or_default().to_ascii_lowercase() - }; - if host.is_empty() { - return Err(format!("{url:?} has no host")); + let parsed = url::Url::parse(url).map_err(|e| format!("invalid URL: {e}"))?; + if !matches!(parsed.scheme(), "http" | "https") { + return Err(format!("unsupported scheme {:?}", parsed.scheme())); } - let loopback = matches!(host.as_str(), "localhost" | "127.0.0.1" | "::1"); - match scheme.to_ascii_lowercase().as_str() { - "https" => Ok(host), - "http" if loopback => Ok(host), - "http" => Err(format!("{url:?}: plain http is only allowed to localhost")), - other => Err(format!("{url:?}: unsupported scheme {other:?}")), + match parsed.host() { + Some(url::Host::Domain(host)) => Ok(host.trim_end_matches('.').to_ascii_lowercase()), + Some(url::Host::Ipv4(host)) => Ok(host.to_string()), + Some(url::Host::Ipv6(host)) => Ok(host.to_string()), + None => Err("URL has no host".into()), + } +} + +/// The grant required by a URL; loopback HTTP retains the original `net:` rule. +pub fn net_capability(url: &str) -> Result { + let host = net_host(url)?; + if url::Url::parse(url).is_ok_and(|u| u.scheme() == "http") + && !matches!(host.as_str(), "localhost" | "127.0.0.1" | "::1") + { + Ok(ide_extension::Capability::NetHttp(host)) + } else { + Ok(ide_extension::Capability::Net(host)) } } @@ -427,7 +475,10 @@ mod tests { assert_eq!(net_host("https://API.example.com:8443/v1?x").unwrap(), "api.example.com"); assert_eq!(net_host("http://localhost:11434/api").unwrap(), "localhost"); assert_eq!(net_host("https://user:pw@h.io/").unwrap(), "h.io"); - assert!(net_host("http://example.com/").is_err()); + assert_eq!( + net_capability("http://example.com/").unwrap(), + ide_extension::Capability::NetHttp("example.com".into()) + ); assert!(net_host("file:///etc/passwd").is_err()); assert!(net_host("example.com").is_err()); } @@ -442,7 +493,8 @@ pub struct ProviderDocument { pub text: String, } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)] +#[serde(rename = "PluginLocation")] pub struct Location { pub path: PathBuf, pub range: ide_core::Range, @@ -710,3 +762,96 @@ pub struct NotificationAction { pub command: String, pub argument: Option, } + +/// An open editor reported by its window. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct OpenEditor { + pub path: String, + pub language: String, + pub dirty: bool, + pub active: bool, +} +/// Find in Files options. +#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct TextQuery { + pub pattern: String, + pub regex: bool, + pub case_sensitive: bool, + pub whole_word: bool, + pub include: Vec, + pub exclude: Vec, +} +/// A search hit, using UTF-16 coordinates. +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +pub struct TextMatch { + pub location: Location, + pub line: String, +} +/// Bounded search results. +#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct TextSearch { + pub matches: Vec, + pub limit_hit: bool, +} +/// A file's problems from one producer. +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +pub struct FileProblems { + pub path: String, + pub source: String, + pub diagnostics: Vec, +} +/// An editor decoration. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +pub struct Decoration { + pub range: ide_core::Range, + pub style: String, + pub hover: Option, + pub after: Option, +} +/// A tree decoration. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)] +pub struct FileDecoration { + pub badge: Option, + pub style: String, + pub tooltip: Option, +} +/// Commit metadata, date in Unix seconds. +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct CommitInfo { + pub id: String, + pub short_id: String, + pub author: String, + pub author_email: String, + pub date: i64, + pub subject: String, + pub body: String, + pub parents: Vec, +} +/// One zero-based line's attribution, date in Unix seconds. +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +pub struct BlameLine { + pub line: u32, + pub commit: String, + pub author: String, + pub date: i64, + pub summary: String, +} + +/// Non-secret settings of a plugin-owned project database source (API 1.4). +#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginDataSource { + pub id: String, + pub name: String, + pub driver: String, + pub host: Option, + pub port: Option, + pub database: Option, + pub user: Option, + pub url: Option, + pub options: Vec<(String, String)>, +} diff --git a/crates/ide-plugin/src/wit_host.rs b/crates/ide-plugin/src/wit_host.rs index f1b8008c0..30d6869f9 100644 --- a/crates/ide-plugin/src/wit_host.rs +++ b/crates/ide-plugin/src/wit_host.rs @@ -6,7 +6,7 @@ use crate::runtime::{MAX_TITLE, StoreData, clip}; use crate::sandbox; use crate::types::{ EditorContext, EventKind, HostError, HttpRequest, NotificationLevel, PickItem, ProcessChunk, ProcessCommand, - Prompt, StatusItem, net_host, + Prompt, StatusItem, net_capability, }; use ide_core::ipc_path::to_ipc; use ide_extension::Capability; @@ -25,6 +25,8 @@ pub(crate) mod bindings { "openidea:plugin/invocation.invocation": super::InvocationRes, "openidea:plugin/tasks.task": super::TaskRes, "openidea:plugin/process.child": super::ChildRes, + "openidea:plugin/ipc.connection": super::ConnectionRes, + "openidea:plugin/auth.loopback": super::LoopbackRes, }, }); } @@ -42,6 +44,7 @@ pub(crate) mod syntax_bindings { "openidea:plugin/storage": super::bindings::openidea::plugin::storage, "openidea:plugin/workspace": super::bindings::openidea::plugin::workspace, "openidea:plugin/editor": super::bindings::openidea::plugin::editor, + "openidea:plugin/decorations": super::bindings::openidea::plugin::decorations, "openidea:plugin/invocation": super::bindings::openidea::plugin::invocation, "openidea:plugin/vcs": super::bindings::openidea::plugin::vcs, "openidea:plugin/commit": super::bindings::openidea::plugin::commit, @@ -50,10 +53,17 @@ pub(crate) mod syntax_bindings { "openidea:plugin/diagnostics": super::bindings::openidea::plugin::diagnostics, "openidea:plugin/net": super::bindings::openidea::plugin::net, "openidea:plugin/secrets": super::bindings::openidea::plugin::secrets, + "openidea:plugin/run": super::bindings::openidea::plugin::run, "openidea:plugin/process": super::bindings::openidea::plugin::process, + "openidea:plugin/output": super::bindings::openidea::plugin::output, + "openidea:plugin/auth": super::bindings::openidea::plugin::auth, + "openidea:plugin/command-runner": super::bindings::openidea::plugin::command_runner, + "openidea:plugin/ipc": super::bindings::openidea::plugin::ipc, "openidea:plugin/fs": super::bindings::openidea::plugin::fs, "openidea:plugin/views": super::bindings::openidea::plugin::views, "openidea:plugin/settings": super::bindings::openidea::plugin::settings, + "openidea:plugin/database": super::bindings::openidea::plugin::database, + "openidea:plugin/mcp": super::bindings::openidea::plugin::mcp, }, }); } @@ -71,6 +81,7 @@ pub(crate) mod providers_bindings { "openidea:plugin/storage": super::bindings::openidea::plugin::storage, "openidea:plugin/workspace": super::bindings::openidea::plugin::workspace, "openidea:plugin/editor": super::bindings::openidea::plugin::editor, + "openidea:plugin/decorations": super::bindings::openidea::plugin::decorations, "openidea:plugin/invocation": super::bindings::openidea::plugin::invocation, "openidea:plugin/vcs": super::bindings::openidea::plugin::vcs, "openidea:plugin/commit": super::bindings::openidea::plugin::commit, @@ -79,10 +90,17 @@ pub(crate) mod providers_bindings { "openidea:plugin/diagnostics": super::bindings::openidea::plugin::diagnostics, "openidea:plugin/net": super::bindings::openidea::plugin::net, "openidea:plugin/secrets": super::bindings::openidea::plugin::secrets, + "openidea:plugin/run": super::bindings::openidea::plugin::run, "openidea:plugin/process": super::bindings::openidea::plugin::process, + "openidea:plugin/output": super::bindings::openidea::plugin::output, + "openidea:plugin/auth": super::bindings::openidea::plugin::auth, + "openidea:plugin/command-runner": super::bindings::openidea::plugin::command_runner, + "openidea:plugin/ipc": super::bindings::openidea::plugin::ipc, "openidea:plugin/fs": super::bindings::openidea::plugin::fs, "openidea:plugin/views": super::bindings::openidea::plugin::views, "openidea:plugin/settings": super::bindings::openidea::plugin::settings, + "openidea:plugin/database": super::bindings::openidea::plugin::database, + "openidea:plugin/mcp": super::bindings::openidea::plugin::mcp, }, }); } @@ -100,6 +118,7 @@ pub(crate) mod views_bindings { "openidea:plugin/storage": super::bindings::openidea::plugin::storage, "openidea:plugin/workspace": super::bindings::openidea::plugin::workspace, "openidea:plugin/editor": super::bindings::openidea::plugin::editor, + "openidea:plugin/decorations": super::bindings::openidea::plugin::decorations, "openidea:plugin/invocation": super::bindings::openidea::plugin::invocation, "openidea:plugin/vcs": super::bindings::openidea::plugin::vcs, "openidea:plugin/commit": super::bindings::openidea::plugin::commit, @@ -108,17 +127,24 @@ pub(crate) mod views_bindings { "openidea:plugin/diagnostics": super::bindings::openidea::plugin::diagnostics, "openidea:plugin/net": super::bindings::openidea::plugin::net, "openidea:plugin/secrets": super::bindings::openidea::plugin::secrets, + "openidea:plugin/run": super::bindings::openidea::plugin::run, "openidea:plugin/process": super::bindings::openidea::plugin::process, + "openidea:plugin/output": super::bindings::openidea::plugin::output, + "openidea:plugin/auth": super::bindings::openidea::plugin::auth, + "openidea:plugin/command-runner": super::bindings::openidea::plugin::command_runner, + "openidea:plugin/ipc": super::bindings::openidea::plugin::ipc, "openidea:plugin/fs": super::bindings::openidea::plugin::fs, "openidea:plugin/views": super::bindings::openidea::plugin::views, "openidea:plugin/settings": super::bindings::openidea::plugin::settings, + "openidea:plugin/database": super::bindings::openidea::plugin::database, + "openidea:plugin/mcp": super::bindings::openidea::plugin::mcp, }, }); } use bindings::openidea::plugin::{ - commit, detail_types, diagnostics, editor, events, fs, invocation, log, net, process, secrets, settings, storage, - tasks, types as wit, ui, vcs, views, workspace, + commit, detail_types, diagnostics, editor, events, fs, invocation, ipc, log, net, process, secrets, settings, + storage, tasks, types as wit, ui, vcs, views, workspace, }; /// A project handle: its root. @@ -319,6 +345,14 @@ impl StoreData { self.ask(Prompt::QuickPick { title: clip(title, MAX_TITLE), items, multiple }) } + fn require_window(&self) -> Result<(), wit::Error> { + self.require_interactive()?; + if self.invocation.client.is_none() { + return Err(wit::Error::Invalid("requires an invoking window".into())); + } + Ok(()) + } + /// APIs reserved for commands and events exclude code-action resolution. fn require_interactive(&self) -> Result<(), wit::Error> { if self.interactive { @@ -381,6 +415,29 @@ impl ui::Host for StoreData { Ok(()) } + fn set_window_status_item(&mut self, id: String, item: ui::StatusItem) -> Fallible<()> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + let item = StatusItem { + text: clip(item.text, MAX_STATUS_TEXT), + tooltip: item.tooltip.map(|t| clip(t, 1024)), + command: item.command.map(|c| clip(c, 128)), + }; + Ok(self + .host + .window_status_item(self.invocation.client.as_deref().expect("checked"), &clip(id, 64), Some(item)) + .map_err(to_wit)) + } + fn remove_window_status_item(&mut self, id: String) -> Fallible<()> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + Ok(self + .host + .window_status_item(self.invocation.client.as_deref().expect("checked"), &clip(id, 64), None) + .map_err(to_wit)) + } fn set_context(&mut self, key: String, value: ui::ContextValue) -> Fallible<()> { let valid_key = (1..=64).contains(&key.len()) && key.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-')); @@ -532,6 +589,44 @@ impl workspace::Host for StoreData { } impl workspace::HostProject for StoreData { + fn search_text( + &mut self, + this: Resource, + query: workspace::TextQuery, + limit: u32, + ) -> Fallible { + if let Err(e) = self.require(Capability::ProjectRead) { + return Ok(Err(e)); + } + let root = &self.table.get(&this)?.root; + let query = crate::TextQuery { + pattern: query.pattern, + regex: query.regex, + case_sensitive: query.case_sensitive, + whole_word: query.whole_word, + include: query.include, + exclude: query.exclude, + }; + Ok(self + .host + .search_text(self.client(), root, query, limit.min(10_000), &self.cancel.token(), self.deadline) + .map(|s| workspace::TextSearch { + limit_hit: s.limit_hit, + matches: s + .matches + .into_iter() + .map(|m| workspace::TextMatch { + location: wit::Location { + path: to_ipc(&m.location.path), + range: range_to_wit(&m.location.range), + }, + line: m.line, + }) + .collect(), + }) + .map_err(to_wit)) + } + fn root(&mut self, this: Resource) -> Fallible { if let Err(e) = self.require(Capability::ProjectRead) { return Ok(Err(e)); @@ -659,11 +754,94 @@ impl workspace::HostProject for StoreData { } } -impl editor::Host for StoreData {} +impl editor::Host for StoreData { + fn open_editors(&mut self) -> Fallible> { + if let Err(e) = self.require(Capability::EditorRead).and_then(|_| self.require_window()) { + return Ok(Err(e)); + } + Ok(self + .host + .open_editors(self.client().unwrap()) + .map(|items| { + items + .into_iter() + .map(|e| editor::OpenEditor { + path: e.path, + language: e.language, + dirty: e.dirty, + active: e.active, + }) + .collect() + }) + .map_err(to_wit)) + } + fn open(&mut self, path: String, selection: Option) -> Fallible<()> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + let real = match self.openable_path(&path) { + Ok(p) => p, + Err(e) => return Ok(Err(e)), + }; + Ok(self.host.editor_open(self.client().unwrap(), &real, selection.map(core_range)).map_err(to_wit)) + } +} + +fn core_range(r: wit::Range) -> ide_core::Range { + ide_core::Range::new(pos(r.start), pos(r.end)) +} +impl StoreData { + fn decoration_path(&self, path: &str) -> Result { + if Path::new(path).is_absolute() { + return Ok(PathBuf::from(path)); + } + let root = self + .invocation + .workspace_root + .as_ref() + .ok_or_else(|| wit::Error::Invalid("relative decorations need an invoking project".into()))?; + sandbox::resolve_in_root(root, path).map_err(wit::Error::Invalid) + } + fn openable_path(&self, path: &str) -> Result { + if let Ok(dir) = self.host.plugin_dir() + && let Ok(real) = sandbox::resolve_granted(&[Capability::FsRead(to_ipc(&dir))], path) + { + return Ok(real); + } + self.require(Capability::ProjectRead)?; + let root = self + .invocation + .workspace_root + .as_ref() + .ok_or_else(|| wit::Error::Invalid("no project is open in this window".into()))?; + sandbox::resolve_in_root(root, path).map_err(wit::Error::Invalid) + } +} impl detail_types::Host for StoreData {} impl editor::HostTextEditor for StoreData { + fn set_selections(&mut self, this: Resource, selections: Vec) -> Fallible<()> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + if selections.is_empty() || selections.len() > 1000 { + return Ok(Err(wit::Error::Invalid("requires 1-1000 selections".into()))); + } + let path = &self.table.get(&this)?.ctx.path; + Ok(self + .host + .editor_ranges(self.client().unwrap(), path, selections.into_iter().map(core_range).collect(), false) + .map_err(to_wit)) + } + fn reveal(&mut self, this: Resource, range: wit::Range) -> Fallible<()> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + let path = &self.table.get(&this)?.ctx.path; + Ok(self.host.editor_ranges(self.client().unwrap(), path, vec![core_range(range)], true).map_err(to_wit)) + } + fn path(&mut self, this: Resource) -> wasmtime::Result { Ok(to_ipc(&self.table.get(&this)?.ctx.path)) } @@ -748,6 +926,69 @@ impl invocation::HostInvocation for StoreData { } impl vcs::Host for StoreData { + fn log( + &mut self, + project: Resource, + path: Option, + limit: u32, + ) -> Fallible> { + if let Err(e) = self.require(Capability::VcsRead) { + return Ok(Err(e)); + } + let root = &self.table.get(&project)?.root; + Ok(self + .host + .vcs_log(root, path.as_deref(), limit.min(1000), &self.cancel.token()) + .map(|v| { + v.into_iter() + .map(|c| vcs::CommitInfo { + id: c.id, + short_id: c.short_id, + author: c.author, + author_email: c.author_email, + date: c.date, + subject: c.subject, + body: c.body, + parents: c.parents, + }) + .collect() + }) + .map_err(to_wit)) + } + fn blame(&mut self, project: Resource, path: String) -> Fallible> { + if let Err(e) = self.require(Capability::VcsRead) { + return Ok(Err(e)); + } + let root = &self.table.get(&project)?.root; + Ok(self + .host + .vcs_blame(self.client(), root, &path) + .map(|v| { + v.into_iter() + .map(|c| vcs::BlameLine { + line: c.line, + commit: c.commit, + author: c.author, + date: c.date, + summary: c.summary, + }) + .collect() + }) + .map_err(to_wit)) + } + fn diff( + &mut self, + project: Resource, + path: Option, + against: Option, + ) -> Fallible> { + if let Err(e) = self.require(Capability::VcsRead) { + return Ok(Err(e)); + } + let root = &self.table.get(&project)?.root; + Ok(self.host.vcs_diff(root, path.as_deref(), against.as_deref(), &self.cancel.token()).map_err(to_wit)) + } + fn status(&mut self, project: Resource) -> Fallible> { if let Err(e) = self.require(Capability::VcsRead) { return Ok(Err(e)); @@ -850,6 +1091,18 @@ impl events::Host for StoreData { let Some(kind) = EventKind::parse(&kind) else { return Ok(Err(wit::Error::Invalid(format!("no event kind {kind:?} in this OpenIDEA")))); }; + if matches!( + kind, + EventKind::FileClosed + | EventKind::SettingsChanged + | EventKind::InputModeChanged + | EventKind::RunExited + | EventKind::TerminalExited + | EventKind::ContentTabAction + ) && let Err(e) = self.host.require_api(kind.name(), 4) + { + return Ok(Err(to_wit(e))); + } if let Some(cap) = kind.capability() && let Err(e) = self.require(cap) { @@ -941,6 +1194,40 @@ impl StoreData { } impl diagnostics::Host for StoreData { + fn problems(&mut self, path: Option) -> Fallible> { + if let Err(e) = self.require(Capability::ProjectRead) { + return Ok(Err(e)); + } + if self.client().is_none() { + return Ok(Err(wit::Error::Invalid("requires an invoking window".into()))); + } + let real = match path { + None => None, + Some(path) => { + let Some(root) = self.invocation.workspace_root.as_ref() else { + return Ok(Err(wit::Error::Invalid("no project in this window".into()))); + }; + match sandbox::resolve_new_in_root(root, &path) { + Ok(p) => Some(p), + Err(e) => return Ok(Err(wit::Error::Invalid(e))), + } + } + }; + Ok(self + .host + .problems(self.client().unwrap(), real.as_deref()) + .map(|v| { + v.into_iter() + .map(|f| diagnostics::FileProblems { + path: f.path, + source: f.source, + diagnostics: f.diagnostics.into_iter().map(diagnostic_to_wit).collect(), + }) + .collect() + }) + .map_err(to_wit)) + } + fn publish(&mut self, path: String, items: Vec) -> Fallible<()> { if let Err(e) = self.require(Capability::ProjectRead) { return Ok(Err(e)); @@ -952,11 +1239,11 @@ impl diagnostics::Host for StoreData { impl net::Host for StoreData { fn fetch(&mut self, req: net::Request) -> Fallible { - let host = match net_host(&req.url) { + let capability = match net_capability(&req.url) { Ok(h) => h, Err(e) => return Ok(Err(wit::Error::Invalid(e))), }; - if let Err(e) = self.require(Capability::Net(host)) { + if let Err(e) = self.require(capability) { return Ok(Err(e)); } if req.headers.iter().any(|h| h.value.contains("{{secret:")) @@ -974,6 +1261,7 @@ impl net::Host for StoreData { headers: req.headers.into_iter().map(|h| (h.name, h.value)).collect(), body: req.body, timeout: wanted.min(self.remaining()), + grants: self.granted.clone(), }; let token = self.cancel.token(); Ok(self @@ -989,6 +1277,21 @@ impl net::Host for StoreData { } impl secrets::Host for StoreData { + fn set(&mut self, name: String, value: String) -> Fallible<()> { + if let Err(e) = self.require(Capability::Secrets) { + return Ok(Err(e)); + } + if name.len() > 128 || value.len() > 64 << 10 { + return Ok(Err(wit::Error::Invalid("secret name or value too long".into()))); + } + Ok(self.host.secret_set(&name, Some(&value)).map_err(to_wit)) + } + fn delete(&mut self, name: String) -> Fallible<()> { + if let Err(e) = self.require(Capability::Secrets) { + return Ok(Err(e)); + } + Ok(self.host.secret_set(&name, None).map_err(to_wit)) + } fn is_set(&mut self, name: String) -> wasmtime::Result { Ok(self.granted.iter().any(|g| g.covers(&Capability::Secrets)) && self.host.secret_is_set(&name)) } @@ -1013,7 +1316,12 @@ impl StoreData { return Err(wit::Error::Invalid(format!("the value of {name:?} is too long or holds a NUL"))); } } - Ok(ProcessCommand { program: cmd.program, args: cmd.args, env: cmd.env }) + Ok(ProcessCommand { + program: cmd.program, + args: cmd.args, + env: cmd.env, + approval_root: self.invocation.workspace_root.clone(), + }) } /// The project a call runs in, when it named one. @@ -1120,6 +1428,68 @@ impl process::HostChild for StoreData { } impl views::Host for StoreData { + fn open_terminal_session(&mut self, key: String, title: String, cmd: process::Command) -> Fallible { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + let cmd = match self.command(cmd) { + Ok(cmd) => cmd, + Err(e) => return Ok(Err(e)), + }; + Ok(self + .host + .open_terminal_session( + self.invocation.client.as_deref().unwrap(), + key, + title, + cmd, + &self.cancel.token(), + self.remaining(), + ) + .map_err(to_wit)) + } + fn write_terminal(&mut self, key: String, text: String) -> Fallible<()> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + if text.len() > MAX_STDIN_BYTES { + return Ok(Err(wit::Error::Invalid("terminal write too large".into()))); + } + Ok(self + .host + .write_terminal( + self.invocation.client.as_deref().unwrap(), + &key, + text, + &self.cancel.token(), + self.remaining(), + ) + .map_err(to_wit)) + } + fn open_content_tab(&mut self, tab: views::ContentTab) -> Fallible<()> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + Ok(self + .host + .open_content_tab( + self.invocation.client.as_deref().unwrap(), + tab.into(), + &self.cancel.token(), + self.remaining(), + ) + .map_err(to_wit)) + } + fn reply_content_tab(&mut self, request_id: String, reply: views::ContentTabReply) -> Fallible<()> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + Ok(self + .host + .reply_content_tab(self.invocation.client.as_deref().unwrap(), &request_id, reply.into()) + .map_err(to_wit)) + } + fn refresh(&mut self, view: String) -> wasmtime::Result<()> { self.host.refresh_view(&clip(view, 64)); Ok(()) @@ -1443,6 +1813,108 @@ fn validate_ui_path(path: &str) -> Result<(), wit::Error> { Ok(()) } +impl From for crate::ContentAction { + fn from(v: bindings::openidea::plugin::views::ContentAction) -> Self { + Self { id: v.id, label: v.label, icon: v.icon, primary: v.primary, save: v.save } + } +} + +impl From for crate::ContentTab { + fn from(v: bindings::openidea::plugin::views::ContentTab) -> Self { + Self { + key: v.key, + title: v.title, + file_name: v.file_name, + language: v.language, + content: v.content, + read_only: v.read_only, + actions: v.actions.into_iter().map(Into::into).collect(), + } + } +} + +impl From for crate::ContentDiff { + fn from(v: bindings::openidea::plugin::views::ContentDiff) -> Self { + Self { left: v.left, right: v.right, left_label: v.left_label, right_label: v.right_label, title: v.title } + } +} + +impl From for crate::ContentTabReply { + fn from(v: bindings::openidea::plugin::views::ContentTabReply) -> Self { + Self { + content: v.content, + saved: v.saved, + read_only: v.read_only, + status: v.status, + error: v.error, + diff: v.diff.map(Into::into), + } + } +} + +impl bindings::openidea::plugin::run_targets::Host for StoreData { + fn register( + &mut self, + project: Resource, + target: bindings::openidea::plugin::run_targets::RunTarget, + ) -> Fallible<()> { + if let Err(e) = self.require(Capability::Process(target.program.clone())) { + return Ok(Err(e)); + } + let root = self.table.get(&project)?.root.clone(); + Ok(self + .host + .register_run_target( + &root, + crate::PluginRunTarget { + id: target.id, + name: target.name, + program: target.program, + args: target.args, + workdir_args: target.workdir_args, + container_root: target.container_root, + commands: target.commands, + }, + ) + .map_err(to_wit)) + } + fn unregister(&mut self, project: Resource, id: String) -> wasmtime::Result<()> { + let root = self.table.get(&project)?.root.clone(); + self.host.unregister_run_target(&root, &id); + Ok(()) + } +} + +impl bindings::openidea::plugin::run::Host for StoreData { + fn start(&mut self, config: String, args: Vec, debug: bool) -> Fallible { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + if args.len() > MAX_ARGS || args.iter().any(|a| a.len() > MAX_ARG_BYTES || a.contains('\0')) { + return Ok(Err(wit::Error::Invalid("invalid run arguments".into()))); + } + Ok(self + .host + .start_run( + self.invocation.client.as_deref().unwrap(), + &config, + args, + debug, + &self.cancel.token(), + self.remaining(), + ) + .map_err(to_wit)) + } +} +pub(crate) mod run_configurations_bindings { + wasmtime::component::bindgen!({ world: "configurations-provider", path: "../../wit" }); +} +pub(crate) mod test_results_bindings { + wasmtime::component::bindgen!({ world: "test-results-provider", path: "../../wit", + with: { "openidea:plugin/types": super::bindings::openidea::plugin::types }, + }); +} + #[cfg(test)] mod project_ui_tests { use super::validate_ui_path; @@ -1489,3 +1961,310 @@ pub(crate) fn diagnostics_from_wit(items: Vec, source: }) .collect() } + +/// An IPC connection owned by this plugin instance. +pub struct ConnectionRes { + id: u64, + host: std::sync::Arc, +} +impl Drop for ConnectionRes { + fn drop(&mut self) { + self.host.ipc_close(self.id); + } +} +impl ipc::Host for StoreData { + fn connect(&mut self, name: String) -> Fallible> { + if let Err(e) = ide_extension::validate_ipc_name(&name, false) { + return Ok(Err(wit::Error::Invalid(e))); + } + if let Err(e) = self.require(Capability::Ipc(name.clone())) { + return Ok(Err(e)); + } + let name = match ide_extension::expand_ipc_name(&name) { + Ok(name) => name, + Err(e) => return Ok(Err(wit::Error::Invalid(e))), + }; + match self.host.ipc_connect(&name, &self.cancel.token(), self.remaining()) { + Ok(id) => self.push(ConnectionRes { id, host: self.host.clone() }).map(Ok), + Err(e) => Ok(Err(to_wit(e))), + } + } +} +impl ipc::HostConnection for StoreData { + fn write(&mut self, this: Resource, bytes: Vec) -> Fallible<()> { + if bytes.len() > 1 << 20 { + return Ok(Err(wit::Error::Invalid("at most 1 MiB per write".into()))); + } + let id = self.table.get(&this)?.id; + Ok(self.host.ipc_write(id, bytes, &self.cancel.token(), self.remaining()).map_err(to_wit)) + } + fn read(&mut self, this: Resource, max_bytes: u32, timeout_ms: u32) -> Fallible> { + let id = self.table.get(&this)?.id; + let timeout = Duration::from_millis(u64::from(timeout_ms)).min(self.remaining()); + Ok(self.host.ipc_read(id, (max_bytes as usize).min(1 << 20), timeout, &self.cancel.token()).map_err(to_wit)) + } + fn close(&mut self, this: Resource) -> Fallible<()> { + let id = self.table.get(&this)?.id; + self.host.ipc_close(id); + Ok(Ok(())) + } + fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { + self.table.delete(this)?; + Ok(()) + } +} + +impl bindings::openidea::plugin::command_runner::Host for StoreData { + fn execute(&mut self, id: String, arg: Option) -> Fallible> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + if !self.host.owns_command(&id) + && let Err(e) = self.require(Capability::IdeCommands) + { + return Ok(Err(e)); + } + if id.len() > 256 || arg.as_ref().is_some_and(|a| a.len() > 64 << 10) { + return Ok(Err(wit::Error::Invalid("command id or argument too long".into()))); + } + Ok(self + .host + .execute_command(&self.invocation, &id, arg, &self.cancel.token(), self.remaining()) + .map_err(to_wit)) + } +} + +/// A desktop OAuth listener; its URI remains available after the one-shot wait. +pub struct LoopbackRes { + id: u64, + uri: String, + host: std::sync::Arc, +} +impl Drop for LoopbackRes { + fn drop(&mut self) { + self.host.close_loopback(self.id); + } +} +impl bindings::openidea::plugin::auth::Host for StoreData { + fn begin_loopback(&mut self, path: String) -> Fallible> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + match self.host.begin_loopback(self.invocation.client.as_deref().expect("checked"), &path) { + Ok((id, uri)) => self.push(LoopbackRes { id, uri, host: self.host.clone() }).map(Ok), + Err(e) => Ok(Err(to_wit(e))), + } + } +} +impl bindings::openidea::plugin::auth::HostLoopback for StoreData { + fn redirect_uri(&mut self, this: Resource) -> wasmtime::Result { + Ok(self.table.get(&this)?.uri.clone()) + } + fn wait(&mut self, this: Resource, timeout_ms: u32) -> Fallible> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + let id = self.table.get(&this)?.id; + let started = Instant::now(); + let result = self.host.wait_loopback(id, Duration::from_millis(u64::from(timeout_ms)), &self.cancel.token()); + let waited = started.elapsed(); + self.deadline += waited; + self.call_started += waited; + Ok(result.map_err(to_wit)) + } + fn drop(&mut self, this: Resource) -> wasmtime::Result<()> { + self.table.delete(this)?; + Ok(()) + } +} + +impl bindings::openidea::plugin::output::Host for StoreData { + fn append(&mut self, channel: String, text: String) -> Fallible<()> { + Ok(self.host.output_write(&channel, Some(&text)).map_err(to_wit)) + } + fn clear(&mut self, channel: String) -> Fallible<()> { + Ok(self.host.output_write(&channel, None).map_err(to_wit)) + } + fn reveal(&mut self, channel: String) -> Fallible<()> { + if let Err(e) = self.require_window() { + return Ok(Err(e)); + } + Ok(self.host.output_reveal(self.invocation.client.as_deref().expect("checked"), &channel).map_err(to_wit)) + } +} + +fn diagnostic_to_wit(d: ide_core::Diagnostic) -> diagnostics::Diagnostic { + diagnostics::Diagnostic { + range: range_to_wit(&d.range), + severity: match d.severity { + ide_core::Severity::Error => diagnostics::Severity::Error, + ide_core::Severity::Warning => diagnostics::Severity::Warning, + ide_core::Severity::Info => diagnostics::Severity::Info, + ide_core::Severity::Hint => diagnostics::Severity::Hint, + }, + message: d.message, + code: d.code, + source: d.source, + tags: [(d.unnecessary, "unnecessary"), (d.deprecated, "deprecated")] + .into_iter() + .filter(|(yes, _)| *yes) + .map(|(_, tag)| tag.into()) + .collect(), + related: Vec::new(), + } +} +impl bindings::openidea::plugin::decorations::Host for StoreData { + fn set( + &mut self, + path: String, + set_id: String, + items: Vec, + ) -> Fallible<()> { + if let Err(e) = self.require(Capability::ProjectRead) { + return Ok(Err(e)); + } + if items.len() > 10_000 { + return Ok(Err(wit::Error::Invalid("at most 10000 decorations per file".into()))); + } + let items = items + .into_iter() + .map(|d| crate::Decoration { range: core_range(d.range), style: d.style, hover: d.hover, after: d.after }) + .collect(); + let real = match self.decoration_path(&path) { + Ok(real) => real, + Err(e) => return Ok(Err(e)), + }; + Ok(self.host.decorations(Some(&real), &set_id, items).map_err(to_wit)) + } + fn clear(&mut self, set_id: String) -> Fallible<()> { + if let Err(e) = self.require(Capability::ProjectRead) { + return Ok(Err(e)); + } + Ok(self.host.decorations(None, &set_id, Vec::new()).map_err(to_wit)) + } + fn set_file( + &mut self, + path: String, + set_id: String, + item: bindings::openidea::plugin::decorations::FileDecoration, + ) -> Fallible<()> { + if let Err(e) = self.require(Capability::ProjectRead) { + return Ok(Err(e)); + } + let real = match self.decoration_path(&path) { + Ok(real) => real, + Err(e) => return Ok(Err(e)), + }; + Ok(self + .host + .file_decorations( + Some(&real), + &set_id, + Some(crate::FileDecoration { badge: item.badge, style: item.style, tooltip: item.tooltip }), + ) + .map_err(to_wit)) + } + fn clear_file(&mut self, set_id: String) -> Fallible<()> { + if let Err(e) = self.require(Capability::ProjectRead) { + return Ok(Err(e)); + } + Ok(self.host.file_decorations(None, &set_id, None).map_err(to_wit)) + } +} + +pub(crate) mod language_features_bindings { + wasmtime::component::bindgen!({ world: "editor-language-features", path: "../../wit", + with: { "openidea:plugin/types": super::bindings::openidea::plugin::types }, + }); +} +pub(crate) mod injections_bindings { + wasmtime::component::bindgen!({ world: "language-injections", path: "../../wit", + with: { "openidea:plugin/types": super::bindings::openidea::plugin::types }, + }); +} + +impl bindings::openidea::plugin::database::Host for StoreData { + fn upsert_source( + &mut self, + project: Resource, + source: bindings::openidea::plugin::database::DataSource, + ) -> Fallible<()> { + if let Err(e) = self.require(Capability::DatabaseSources) { + return Ok(Err(e)); + } + let root = self.table.get(&project)?.root.clone(); + Ok(self + .host + .database_upsert( + &root, + crate::PluginDataSource { + id: source.id, + name: source.name, + driver: source.driver, + host: source.host, + port: source.port, + database: source.database, + user: source.user, + url: source.url, + options: source.options, + }, + ) + .map_err(to_wit)) + } + fn remove_source(&mut self, project: Resource, id: String) -> Fallible<()> { + if let Err(e) = self.require(Capability::DatabaseSources) { + return Ok(Err(e)); + } + let root = self.table.get(&project)?.root.clone(); + Ok(self.host.database_remove(&root, &id).map_err(to_wit)) + } + fn sources(&mut self, project: Resource) -> Fallible> { + if let Err(e) = self.require(Capability::DatabaseSources) { + return Ok(Err(e)); + } + let root = self.table.get(&project)?.root.clone(); + Ok(self.host.database_sources(&root).map_err(to_wit)) + } + fn set_credential(&mut self, project: Resource, id: String, password: String) -> Fallible<()> { + if let Err(e) = self.require(Capability::DatabaseSources) { + return Ok(Err(e)); + } + if password.len() > 64 << 10 { + return Ok(Err(wit::Error::Invalid("credential exceeds 64 KiB".into()))); + } + let root = self.table.get(&project)?.root.clone(); + Ok(self.host.database_credential(&root, &id, password).map_err(to_wit)) + } + fn connect(&mut self, project: Resource, id: String) -> Fallible<()> { + if let Err(e) = self.require(Capability::DatabaseSources).and_then(|_| self.require_window()) { + return Ok(Err(e)); + } + let root = self.table.get(&project)?.root.clone(); + let started = Instant::now(); + let result = self.host.database_connect( + &root, + self.client().unwrap(), + &id, + &self.cancel.token(), + Duration::from_secs(180), + ); + let waited = started.elapsed(); + self.deadline += waited; + self.call_started += waited; + Ok(result.map_err(to_wit)) + } +} +impl bindings::openidea::plugin::mcp::Host for StoreData { + fn stdio_command(&mut self) -> wasmtime::Result>> { + Ok(self.client().and_then(|client| self.host.mcp_stdio_command(client))) + } +} + +pub(crate) mod mcp_bindings { + wasmtime::component::bindgen!({ world: "mcp-tools", path: "../../wit", + with: { + "openidea:plugin/types": super::bindings::openidea::plugin::types, + "openidea:plugin/invocation": super::bindings::openidea::plugin::invocation, + }, + }); +} diff --git a/crates/ide-plugin/tests/api_compat.rs b/crates/ide-plugin/tests/api_compat.rs index 7926dae3f..6e9778a63 100644 --- a/crates/ide-plugin/tests/api_compat.rs +++ b/crates/ide-plugin/tests/api_compat.rs @@ -32,6 +32,7 @@ fn components() -> Vec { repo().join("crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm"), repo().join("crates/ide-plugin/tests/fixtures/diagnostics/dist/plugin.wasm"), repo().join("crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm"), + repo().join("crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm"), repo().join("crates/ide-plugin/tests/fixtures/symbols/dist/plugin.wasm"), ]; let official = repo().join("plugins/official"); @@ -88,15 +89,15 @@ fn the_sdk_carries_the_hosts_wit() { #[test] fn the_version_check_runs_compatible_components_and_explains_the_rest() { - let host = v("1.3.0"); - for ok in ["1.0.0", "1.3.0", "1.3.7", "1.0.0-rc.1"] { + let host = v("1.4.0"); + for ok in ["1.0.0", "1.3.0", "1.3.7", "1.4.0", "1.0.0-rc.1"] { assert_eq!(check_api(&v(ok), &host), Ok(()), "{ok}"); } - let newer = check_api(&v("1.4.0"), &host).unwrap_err(); - assert!(newer.contains("1.4") && newer.contains("update OpenIDEA"), "{newer}"); + let newer = check_api(&v("1.5.0"), &host).unwrap_err(); + assert!(newer.contains("1.5") && newer.contains("update OpenIDEA"), "{newer}"); assert!(check_api(&v("2.0.0"), &host).unwrap_err().contains("update OpenIDEA")); let older = check_api(&v("0.2.0"), &host).unwrap_err(); - assert!(older.contains("rebuild it with the 1.3 SDK"), "{older}"); + assert!(older.contains("rebuild it with the 1.4 SDK"), "{older}"); // 0.x: every minor is its own API. assert_eq!(check_api(&v("0.2.0"), &v("0.2.5")), Ok(())); assert!(check_api(&v("0.1.0"), &v("0.2.0")).is_err()); diff --git a/crates/ide-plugin/tests/fixtures/api14/Cargo.lock b/crates/ide-plugin/tests/fixtures/api14/Cargo.lock new file mode 100644 index 000000000..fcc4da830 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/api14/Cargo.lock @@ -0,0 +1,337 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "openidea-plugin" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "openidea-plugin-api14" +version = "0.1.0" +dependencies = [ + "openidea-plugin", + "serde_json", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/ide-plugin/tests/fixtures/api14/Cargo.toml b/crates/ide-plugin/tests/fixtures/api14/Cargo.toml new file mode 100644 index 000000000..26cadfe50 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/api14/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "openidea-plugin-api14" +version = "0.1.0" +edition = "2024" +publish = false + +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openidea-plugin = { path = "../../../../../sdk/rust/openidea-plugin" } +serde_json = "1" + +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 +panic = "abort" diff --git a/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm new file mode 100644 index 0000000000000000000000000000000000000000..708ba22f1f488a786a614a3f1ba9758eba610f70 GIT binary patch literal 234726 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ⅈ^1jKuSX>Q9 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®9XM8D{pw%pZltrDM>C&e}3o8s5w{ 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 CkR7K0 literal 0 HcmV?d00001 diff --git a/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources new file mode 100644 index 000000000..71bbe995a --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm.sources @@ -0,0 +1 @@ +cb74e35e0b0fa2bf7d46b143190d4e72d90abfa7a95962c037495b722d8c73d3 diff --git a/crates/ide-plugin/tests/fixtures/data-tools/plugin.toml b/crates/ide-plugin/tests/fixtures/data-tools/plugin.toml new file mode 100644 index 000000000..0fda63df1 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/data-tools/plugin.toml @@ -0,0 +1,68 @@ +id = "test.data-tools" +name = "Data tools fixture" +version = "0.1.0" +engines.openidea = "^1.4" +wasm = "dist/plugin.wasm" +activation = ["on-command:*"] +[capabilities] +required = ["project.read"] +optional = ["database.sources"] + +[[contributes.commands]] +id = "upsert" +title = "upsert" + +[[contributes.commands]] +id = "remove" +title = "remove" + +[[contributes.commands]] +id = "sources" +title = "sources" + +[[contributes.commands]] +id = "credential" +title = "credential" + +[[contributes.commands]] +id = "connect" +title = "connect" + +[[contributes.commands]] +id = "stdio" +title = "stdio" + +[[contributes.mcpTools]] +name = "echo" +description = "Fixture echo" +inputSchema = "arguments.json" +readOnly = true +destructive = false + +[[contributes.mcpTools]] +name = "write" +description = "Fixture write" +inputSchema = "arguments.json" +readOnly = false +destructive = false + +[[contributes.mcpTools]] +name = "erase" +description = "Fixture erase" +inputSchema = "arguments.json" +readOnly = false +destructive = true + +[[contributes.mcpTools]] +name = "spin" +description = "Fixture spin" +inputSchema = "arguments.json" +readOnly = true +destructive = false + +[[contributes.mcpTools]] +name = "denied" +description = "Fixture denied" +inputSchema = "arguments.json" +readOnly = true +destructive = false diff --git a/crates/ide-plugin/tests/fixtures/data-tools/src/lib.rs b/crates/ide-plugin/tests/fixtures/data-tools/src/lib.rs new file mode 100644 index 000000000..98bd09662 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/data-tools/src/lib.rs @@ -0,0 +1,39 @@ +use openidea_plugin::{Error, Invocation, McpProvider, Plugin, PluginDataSource, host, register_plugin}; +#[derive(Default)] +struct DataTools; +impl Plugin for DataTools { + fn run_command(&mut self, id: &str, ctx: &Invocation, arg: Option<&str>) -> Result, Error> { + if id == "stdio" { + return Ok(Some(serde_json::to_string(&host::mcp_stdio_command()).unwrap())); + } + let p = ctx.project().ok_or("project required")?; + match id { + "upsert" => { + let source: PluginDataSource = + serde_json::from_str(arg.unwrap()).map_err(|e| Error::Invalid(e.to_string()))?; + p.upsert_data_source(&source)?; + } + "remove" => p.remove_data_source(arg.unwrap_or("local"))?, + "sources" => return Ok(Some(serde_json::to_string(&p.data_sources()?).unwrap())), + "credential" => p.set_data_source_credential("local", arg.unwrap_or("secret"))?, + "connect" => p.connect_data_source(arg.unwrap_or("local"))?, + _ => return Err(Error::NotFound(id.into())), + } + Ok(None) + } +} +impl McpProvider for DataTools { + fn call(&mut self, tool: &str, arguments: &str, ctx: &Invocation) -> Result { + match tool { + "spin" => loop { + std::hint::spin_loop(); + }, + "denied" => { + host::run_process("sh", &[], None)?; + Ok("unexpected".into()) + } + _ => Ok(format!("{}:{}:{}", tool, ctx.project().ok_or("project required")?.root()?, arguments)), + } + } +} +register_plugin!(DataTools, mcp); diff --git a/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/detail-views/dist/plugin.wasm index cdcf91b87537613220e01e592a9889db4d1013c4..7197508c83cff1a5404445a558471f687c171969 100644 GIT binary patch delta 5505 zcmd5=dt6l4wVrRyfV?Jv@{)%h1|Jx~@HUMxapLRxsx&ceqY<=BWM(^6^%#5S4_jmuif6V^Q z+H38#zP-f@p6X4l z*VVj$TjsP;NusojqSP{LB&!;&iQcxoO&^&2*PHBDoY?Ec+fJ-+QvYi}W>#afVHPfJ z_c{eg;iOYI?!<$qj_VGib)|56cPdXfF-9u8YYk=f9;cdNcNOFhMnHJKiwC-tCw|dn+7M4?7-FpZFk9{qFc!$CMAgu|}V$lmUbO zvZ1upWb#*h6pLbsD=THmeqvA78Nvxi_8k&wk7(EcHqR%!< zwj<1@Sc`d_VtHo$kF4aJ+*z$-AOb)~tHiNL%%uHQT2K_GfU2v-oM?QCOWN2NJWDt& zst02=E{gyKlc}OcJg>mU;z|W|RNE|qVljmdHH$g17!!0z4`{~|CX-^aS2v4&v53c6 zaV8d{hV|9xxodls7g{mfP11<$oE59ugae3Cv0`s|am<9DZ$<(|>w6CWBeL zvpF6J{(H6bILT^qPU`Pk!w5uxb~g*hqxe0ewq_)DKg3WZCL)gxZ4)99V{lTOOGM$2 zdYyfu$>efqgWc#;bh5D5Hfsw;;Vs~j@J>Ri-_dQ7fCBqTZF~|ogFR{=C1d@=BF;@k zfsA-672Bw>M$ApaGHR_6e@w$b>Z%dFPVASCV)TjC>BwW~NkH47WOL;Li| z@f2Z$YuW96TJLN$QF!exnLDW?tBJ-rtE^Y-p35`n6-VY`2-WTqSLR}o#0goLE^%8H zDg%tMHZR3u?-dEz_=(hP%0{}xu55lw8UH&QnKoAxADhn%D`=eJHBm-$$78#N^DtK0 z@0o`eB;J{a6hC8}(ogXfmBk1Xne!1Zm9NZ4w41uc<~Pv_C-d4}+QIqo=e=~7_~p;= zoW#Hd_^reX3y?3-z7Wk4KV67WiG2&PSYl2NhDxl+!3c?34w6UqzGWoH#Ef0pad!E{ zS_0s2blPMNT-o};B4kCn=6uv_v*@#RGZY<*kQ`^|6HQ%ayJe}>OJ;);L-)ck1Fu}n z#?stA5xbt^m7S6=G5RIXfR0 z;K$POu*JM#_KKN{k^Gn|NkKE+*>!T(L-NY|7sR^q&STpb<4Nh`=3;EU=iQIJmlvW{ zYSLcfSDZ2TC9IHm^(Bm!7_kH&4RnPIuvrxI43CiF&JvVMv+|cQO5%~1aYTz) zMMiqCIVnF|j1-CG#duoc?~9Qe>Ixe%*)!z$5~R3QA@`}a zlwheG{ap!8Nj$mSh|;$l?@Rf;6$YytD-2ePevVY`*LMCK(R%6sVkLhw+3+tbje*ux zI3eY8t1#BbnPJqGW1u*{27yxZ%q!UMW^+H$yRR6pbjE57c9&s@&HFyeg{v`7`uTV@ zuDc?b?~8DK4R$Eb*rp8f3FM08Pv~7t^UTWbwMcf2w}{VHAVU0OE#^wsnd|TaiEpgK zNr}&|$6YR@=S#8fpD@2sYU~&N%aHCixj)q}mZ4HQ>n+0^iBn!RQXaBF&zv@41AptJ z;9BTDmSti_}c%H#fh&OS$yX;T$e$s zHoKkgkn)_@5iN1m>qY^zzK+#W9(%eG>qg_bNj|q4&&T+g8h47|3m70S9)MLOUqGn%hM!_O>m>gm=NZ;+QiAO9&fT=L zP9)nXoFj_tc0Ofz$VDxN{Du>iRwzC$SE8l`1EjU={eFNr&j1fM34G8*dRyO$@MvcY z*V$ZAA{4We?rrUGD_Z#6Z_rJ5n!5fcz-`#IL!YwK2TwV`tvW3ZwqrEs{IBh3k!#FL z7R%p!hA?regFk+~+W8I)w@_`JcIQ3hdGQkHOS$%aR^B$Acgg1p@#X;@ds{-7xO@Qq zV)p@r2ORiZKS(9=v9D3r4LG$?^0CXF)&^rW#JnyHqP=xub{8KK7)!hO%h6h|{j!To zg1Q>CZ-0ds5qa9McctUr9It3a$MB>VCqrvHf!VTb1V6-xDStwlw*PndPpj0`cJpkh zqEQrGM5y)b2AxPWS97agEuq$GgIf%Db?IFSoz^idV3Tf~A&nblX@!b{3$mP@LIn#k zS9TinG5+vys;!GgQ?v&)RS7&1C?ZUmrL(X;o|rp$~PwRfq{eF18-~A%eWkY6WZT+XE3`WxYyBR z7Ml4>3`52Ph41TOp{Mv>HCsGXBKs>nTiPpM;YWHAeDpQK9}L&ea|9<_Pg-oBz6jbA zefV7f6NMN3+KXR><9_rmLxDdXqza)844_43Y80X@kiw_;8oP;W9(;Uty`29?MSy!m zuhtVtIYg~Oo7SJs=nv7Mr1?@;qc}f^6ejW2Ad16jEpjl0@?T|RyK*h;y@YssPS3fp z+BzGJ(Dw>g$A?4cnwekkJ@GV}H*4+BBPhWeCxlNDl~QYywk3&v8sxWZdCvU8;+*A$ zg<9`8dNY;whl;8aDvUiqCR3b=e*r z4pOFQ+gH&xLYEf5hF1C_O}x2@VvwdC+eD>i7RS9#eA~lcCYsA|QJ1u!EwtE&#WmX~ zli{On#5Y`;k4oioNL8H_#UJV@g8_mjvU0MZ7x*$vyxBlg8G0KiiF;#q&^54lXD3Z& zaZ)3NNDp&%(M%RwcF}nrqm{iyx1`nDy_6ns@6IXiQBnFXg@~fP6u{c6E^XfYB?}94 zh9-!P!(`L#7v_}A&o3O7p)L)L6iIJWe>PwJHVuD7I-9?sB!9*HlAQPysaA`^q_c}} z(+CFaqemGM_E93k?0v+SS7O6H+AA;YjJrB*;yZK*UM%xJQJv2r2l+~w6j9YiG4j!D zlVrkuOI8)-6pu?Dl~&g&vn&R6QZ!F+OegJ-6TI*qrAYIzRGmBbBpsnN*%Ohd&I@Bx zQGSF*$&Sbb=Y5+ve?%uYzm*UC)H&O{^VD2hl73OX7uh^?$Ccz`mI3Bykad$AsKE{wp;=W&1b=w$cp8a$G==#JXn$(`Ldmt> zHK+FLFOHQ^SEa-j%dA>kiofG}?Qv7^a+9;RPDhu4%?4hqRsP;3GwYH;MoHQ01_4Q| z*{&-V8`yB_m}W3eV+zB%!7Mefn`Bm1>CCflDAnOpIN~3|)A-syVjy3qMLf>e+K6-; zN1ZS)XU_DTd8S1U;jE#q%(xrTm#=*ylleL!GLf%KBm42yrLBA}SKJi!FPX}mSz*e5 z&U{>{%kAvmJM#l51q;KJ<+Ey)%$Z?Ia^7yGYhIZ9i@fzVr6n&+DOq?FC*00OZ%5LJ z_3qYr0ky%Jd=Kmp4t1M!j-(Z!dT5o=V`L`|jRKv(k58 zs8ZK79-k_=nug+xJ9*zHpqBN@z5S!;`g-^11MWu@>+vDVkHEp|&nNikE-(w$jn2&L}qPdXFbE>j9WJH^zHmI-XJ%&nZ878rM13pey{ zhPK7-XZJU>>y?x%u_)E9Rqki6TnqKTSVj0!^VE9{5uNH=29#AiCgklVKi-BQC!)`S<3=KqW8%0z& zeu*j(8IE{S6pjmYa-%vf0{e(A)`@#j$U?KoiiU&QD#feO_#L&?i5t-vBrz@qi{dJ_ z%j}ZOrg)1v!)|$c`LAqb4ZkLo ziHaUrjA~)EV8PCQ{U*-g%~bz)aM z5>X+}#G_yD!<)2PH@?b!wws+MDMU_R6_P3m4*V#BGcbZ<5*$;sx;z1YG|S|l>VuKs z7~Wcw2!!pckoKOjnT(Y7sntn{23@Ta?nm)wMs-PF?0ATvNJ&91eZ5JD6t3_Z@mUJ; zdtTIHPcfN11|4wfYj!PEI9uw}Y5nj9uvPf?M`}=;mXICJHR`bbSOd1Gel!5fALem# zD)OYqQ>mz=OBG`BAk3rI3h~Jx#87*MXf?1?8VYe(EKWl%!}T;|(XnzdJRM*DJYR8j zFouZY!5IH@p31y5OV^W?vPN9(Q~!QUb|#Q3Zk6=KO11x!=uLEWqX>KocCm0M2GGq) zv3Dq5G!`2r{q9pd1!v2JEd#?m9TD7-o@#jpp5b{ZSI;{!TP81t4MQeRl)8BsPm*YH zB20WW97SwS%^886W*+9iQOLv&b>b*QfXRZR9AHRJU;~1~UKJSoGwl%6}ZH{ZT7+JcmgP?a$#is8uIS zzzY9}!RDAn~5@vS#yaUlQ{#II5ZhOsb#ykG#N7_4$s0^ ziIrI>x9YkMKfA^063N;4jpVG%Mw-OtY<^N1|0f%n4v&`rN5D86D8ueILVEN1W4Y;5 zFht7loPrl5{xk&xgLFN6CwrhMFGQrsoQg!rd}S))yrEkhK_hH1B5&EQHcUk@zfQM{ zKl~2QNer2WKS?}44YMRVr(?UsYts=Sv28kLOU%hZFNtM2NRp`LU_f8j8*;=F$;1(R zs$(n##9OQg))yVJ2A*pDVg|BeJvkpXJ1knZ-U!A185oeD^HWUCW~XJY%}-W?3q$wa za0GrcF`m2d{7gLKO?RI;-r`%h12eIbWggGP5*hTNTx3g(nuVlPqkjJBbkH2Ed+<@! z6N`5Ecko#_3y(_!4YSbIaBAU^Asf0c%N&*qn=ON^h50gri#6gu8pT_a}AC3ikJFWysk)@_=NC#8*Bv$5g6 z^&qysDjzP%8T1lA=!`R8!UBm`Ucz9B(R1)ojK`hTVX>RX`M4B6&A}=uw(4c{lX&Q5 zT$ET*pqu-kKrgRzE|TT?%X4u_;<$Ob{L*>&OwzwD#6XFw3h|W0KNn(VgvV|4NMD!3 ziZIa23h%&LSA@Az`^O@jl6Z2y?xk%$j!62Q1$tCB7wA#V_&rj2UETJ3#A&1d?n3_G zWWm2K)D>-ua6;0bEy9ohqr%v-3Nhl`5`;+3)30E+H=2%t-g`xVq|+Cphj$Fa9sV6c z&R>iv($2?=al_-m+`+?*C8)6*zD?;A5W*A5f1~#>#?u|5=XtNmppuh zRylR}3jW8Fj6ap?rTn&3FXfSy`U~ramHPV8HHZ_>tin>SqmEU)xJs{L$m)Mm#SyFZ zD!#QEH>A^zYrMv5Bt2;@;v_Cws}F!{Efz~UX`Ma*S?lm0e$-y2K3#zqz`L8;O-K%^ z->m&jTAT;fPd8z;nb#+7#!Ga%Rz0;DeR;pQS-rCb-+*^qoAx7Iv~Pt?RP0BD=w1yQ zUE-ZYHKM6ih>_J?`*tD1YH*m%{96r%^8ZZWHhc@Zz4=fbhDqE~hgdaxJ9Y+hf6RM0 zl+z-tnP1>bkMO(sl79c1yl>U+tqm(>gwAx{ZCnzs?_tT$_b~Uget(zmyWQ532sQSy zPLn;H!z_^8Q~BH_Z_Bd@sN&*j_EG;`tQdl&xM?nP_Oj zU@rX^P1q|N&3%jKKYoTtacV!;&ZVB)k3JS^*{uHb4s!j-wN-?jM;Ed92)DG^AnGJ> z+8~ZH!6Tq`N~^z1ySMH6O50tf$nNN(5pbR=)mkPOTeoU)o)uG?(Vebu789F!+rU`d z%>N~=Lj6NCj|sJJReyL7FCwFxNAaW|S4M3-fr)aI1mAQMqdrB6y8A=? zw@vb@S~zt&y-gHcK!o+|3N4U0-iEZwc?o~S;;hhv>&9#Cp0x&jP0E#t(DR52_6(g= z*TA!#ny@z-=;M|eWDE?S~*bo(I5Tz2{{%-Z!_ctQv;peqQ+QhhM6vH5hWoMHP*Go zHyHl(_I$WE{~sAvuZBy#9zr=JwuVrM`rFR*zHyrrL3;wJeVaJfo$MUQo$i!?GBvgb zMex_O{wDJ@_kL0QeN!}=t1fd;lJ*+$EWF>7uA2GD-yK8AO zL)1D+shR6&b^w#jm6XX~Q;5&M)Mpi1B~{LBk)#L`l+NH3G=iC{1--yWW#U>jjb=!$ zA^uKPC)dzbFgdiAMl!jomcpflZLiUICjb5#onsyK_#1RrO1)Q4Y1aEEQVEZWV{cQq z*jGmbE(hKmnUjm$9e5Dk>_ zk*Qknyi#z82FaD^6lF>zi;52qQ9rp5n`|7%i3nAT?sXUSWPYwn0|S_k?+8(f(?3Ry zX`p4&_|`^xObaGKeA7r>#Fa*}@$#)k>dg?|M0~0zGMcCd2Q;OLmdZ|CK0tgdD8k+J g7{hQk@j0I;bW&-dZOeCM8f&bg=EGk12r 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 delta 5045 zcmcIoc~lff8lSIvI0ukpxJT?6Q9(fjL{Sl$2oiM_JR>2 zqlpC3b-D;n&?*R%PU+6LgjSUw*XWjO%oZCq*sy}`@tZug*+vC(kv1E#nW-wSe6!d_ z?5VF)>=^jwg<@5r%Em0UVOP#(=p6X{gZKh;GcwRbU?HR#@GGbh4VG( z?8n}uCvL6?i1X%~vuDhoId7I`L==Nz#DI)z0lhJGSG`*tY?pk7>p^ENIG(SWBf6QJ z3utWmJDupg=~>RSL^4`+q`iQ48E%vU$CUvTQJ>)^>HPS21bS_A@m{Cuvwo{Z@pM_LVdSp-F}2mD1}g{_c(;q!or>GC74k0U^KWX93;j z6&r$zRDXMH{si==6A7&-38E*8LrD87+EiRknpe@Hl5oWXL&%j>X)uCXNHdd!k%s9?NE(SpXP z71BiQ3(LoO6I+ogq=}z-0jXO+t$=VlAgq{Yc2CkLy!>^A#6n7_8BV8uRsXI`UK$xw4KfI2DT$yG{jm zk|12-^r^?R`c?)y^*D1`z+7&8@ghK})Vl$||Iu0=@(6E@EzU=4rJ?78fwY#>$$veA zAa(n);(229;)OTi2px1WgDC0R1_Z z36Rb4t^g4n-Lx>2;|ML}aNMPZ1W(nH=q7rw_+SWP53~@)nO$AL5NwZFYx2zS23x>Z zCTF{V4!G27t}ub)C$4ac;}SP`i=)*I{t#-{cE>n{k@n=w$lm4-bGewi2lU_==K;|i zU-p1_y`vx*4iyXA=>b8|QIsYRm>Ooce?%~8QPxpWG&Io@;<>@qp3sfsUQftE^+%pC z!jV*sVAMM6Owz$hl)0vZ54cK>7fj>$sTV{?*sA0@fYb^)lBWJS>cPMp0=R6DHzae+ z_J(kqAJJ_9@ztrKCRd*P3U7!)KL@-al*^v>hMo3);u&tXuv{O&fnw~Ok0SV!55&jW zbM?(~6vY;kVjugbn`5+}QK&n<3#4!x#a&>1hjmZ19_`mywB+selU>Z zQ9n5EXq}fyD+*(rT(VvMu!@UC2S8trIRS8mW1?O)_l90ApH&YreBVD1nmD!ws`6qG zT;lXUgCK!pbTB@=5$6QMn}PN$Lo%IR-U)^T2TR|H6&nJ%T)QF!&T*U@s(RTL3P(9T zB}|Q~FiefgBODTOUrr4NgEGLc8oTatruxX-tQ>+!iz#syO z9N~11ba#Y$2VRYUZjL$An_N4^+@%{#=Pq))!5xk{qUsurZxwAWH3Nuy7aTNvggu4P z&czsRgm`;kT2>MUA*|F0uW>^Uj4+5}UlW|=_{aoLanK%hhh@JKZ(*dG*WpM=a)@-U z@Y6`xz>RI}0k3du?x9v%?x{pA-|Y#@l}gTzQqvgK2f~>Y1^c-4)M)j{*%7TW&qu=+ zhojE5TiP3vlGXH08Z$2dCXb+3>R^9OuusUdMZMQsEh>7yV=oWkeG6rlXz0RYLEi?p zEC!Zw;z;Y3`~L2^08oq zN?e27cRW?B8rX(u5XN2}izjp?D_H`1mNyp9r&9HLJHNKD{bM04%(78=N3gxJnxo87 z_D2$f2@sa_>wHy_sxKF2jo8IWH8u;v(8JR#jLgF3bWjGHz4jIP{1qA1x;v?H`Cm#n z8@5?AW&q_Oo-U5uMb2d&I*#kGROgBb!+bdpY_Xf+{?;&iqd!Wxdahgy4qOi^ z0Ptv7b{=9}F?CjT2_I3la!e!q?CrU5-n@4g&YCfA!7MrMCgdg#s|y(D>Z{4sXfy{w zqX|VM_+RYltC@&ci9~lNdK1z{C%Tk<8%WN`cY;Vi0{L=qD8WAOm4}59N+4anWFV=Y zF8Hy+A7}$~g+#f3FLFzOL>AJAq#>EphaBf*cpMpoFe#1N$dKYJG(CmE}h`>ncYt(kq7}9B!Pb&f@%SRHFSn|uskb+{FxguWGeAxW-9Uv qGf5vT+^$UG$15d`B5??}Mk%KKM=Obq7)|>9-=b7LxYZa*{C@#ah2Tp7 diff --git a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources index a10b690a5..270ae051b 100644 --- a/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/endpoints/dist/plugin.wasm.sources @@ -1 +1 @@ -cacd381d84e1070fffe8f0ac64fad91b768df63b759c0b3e5e2d22a6a2fdeb73 +6ab6593d3d7226a1a52bcdbe827697be39e9ed5117d238ba2366ad1a6911a623 diff --git a/crates/ide-plugin/tests/fixtures/execution/Cargo.lock b/crates/ide-plugin/tests/fixtures/execution/Cargo.lock new file mode 100644 index 000000000..9b44fecc2 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/execution/Cargo.lock @@ -0,0 +1,337 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "openidea-plugin" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "openidea-plugin-execution" +version = "0.1.0" +dependencies = [ + "openidea-plugin", + "serde_json", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/ide-plugin/tests/fixtures/execution/Cargo.toml b/crates/ide-plugin/tests/fixtures/execution/Cargo.toml new file mode 100644 index 000000000..c0affd517 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/execution/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "openidea-plugin-execution" +version = "0.1.0" +edition = "2024" +publish = false + +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openidea-plugin = { path = "../../../../../sdk/rust/openidea-plugin" } +serde_json = "1" + +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 +panic = "abort" diff --git a/crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/execution/dist/plugin.wasm new file mode 100644 index 0000000000000000000000000000000000000000..996f085fa605e7cb04003933c27cd1ef39bf17cc GIT binary patch literal 199189 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 -> Result, Error> { + match id { + "target" => { + let project = ctx.project().ok_or_else(|| Error::Invalid("no project".into()))?; + host::register_run_target( + &project, + &PluginRunTarget { + id: arg.unwrap_or("wrapper").into(), + name: "Execution target".into(), + program: "execution-tool".into(), + args: vec!["wrapped".into()], + workdir_args: vec!["--cwd={cwd}".into()], + container_root: Some("/container".into()), + commands: vec![("composer".into(), vec!["composer".into()])], + }, + )?; + Ok(None) + } + "untarget" => { + host::unregister_run_target(&ctx.project().unwrap(), arg.unwrap_or("wrapper")); + Ok(None) + } + "run" | "debug" => { + host::start_run(arg.unwrap_or("detected"), &["appended".into()], id == "debug").map(Some) + } + "terminal" => host::open_terminal_session( + "session", + "Execution", + &host::Command::new("execution-tool").args(["interactive"]), + ) + .map(Some), + "write" => { + host::write_terminal("session", arg.unwrap_or("input\n"))?; + Ok(None) + } + "content" => { + host::open_content_tab(&ContentTab { + key: "config".into(), + title: "Config".into(), + content: "hello".into(), + actions: vec![ContentAction { + id: "save".into(), + label: "Save".into(), + save: true, + ..Default::default() + }], + ..Default::default() + })?; + Ok(None) + } + "events" => host::storage::get("events"), + _ => Err(Error::NotFound(id.into())), + } + } + fn handle_events(&mut self, events: &[Event], _: &Invocation) { + let mut received: Vec = + host::storage::get("events").unwrap().and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default(); + for event in events { + if let Event::Other { kind, data } = event { + received.push(serde_json::json!({"kind": kind, "data": data})); + if kind == "content-tab-action" { + let a: ContentTabAction = serde_json::from_str(data).unwrap(); + host::reply_content_tab( + &a.request_id, + &ContentTabReply { saved: Some(true), status: Some("Saved".into()), ..Default::default() }, + ) + .unwrap(); + } + } + } + host::storage::set("events", &serde_json::to_string(&received).unwrap()).unwrap(); + } +} +impl RunConfigurations for Execution { + fn configurations(&mut self, _: &str) -> Vec { + vec![PluginRunConfiguration { + id: "dynamic".into(), + name: "Dynamic tests".into(), + program: "execution-tool".into(), + args: vec!["test".into()], + test: true, + framework: Some("plugin".into()), + ..Default::default() + }] + } +} +impl TestResults for Execution { + fn parse(&mut self, _: &str, chunk: &str) -> Vec { + if chunk == "trap" { + panic!("parser failed"); + } + vec![ + PluginTestEvent::SuiteStarted(TestStart { + id: "suite".into(), + parent: None, + name: "Suite".into(), + location: None, + }), + PluginTestEvent::TestStarted(TestStart { + id: "test".into(), + parent: Some("suite".into()), + name: "Test".into(), + location: None, + }), + PluginTestEvent::Passed(TestPassed { id: "test".into(), duration_ms: Some(7) }), + ] + } +} +register_plugin!(Execution, run_configurations, test_results); diff --git a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm index a4f2867c3ce00d005f3c05578f2277caaaa44cad..c026fa17eb3e78bd355c5717eece54d61256916d 100644 GIT binary patch delta 4789 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 AWB>pF diff --git a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources index 3fa85d991..154dd39da 100644 --- a/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/file-templates/dist/plugin.wasm.sources @@ -1 +1 @@ -0d418585dca868fbceb6432d06c5c838c3d42a9e99e7a9c7641199f25fb8fbe8 +86238b45b09b0274d59282b35295aaee650e999aa309640d9347787e7d461ec4 diff --git a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm index 3f37ce42e065eedf89c3ab6019e6148b47ee3cb2..fdba6cd2676774fa0ca6869004eb36a3ca3e8b81 100644 GIT binary patch delta 20376 zcmch930PFevhY;T00RSZRQ5$?22>D1R9pfs9G9qx`*q2paW{(l7ISZI0`3}j8*PlJ z#FZ#029t4%VvHIM?wY7U#kfR`>lKZO7+-bwIRhx(d*A=Q@7J%pPj^*US65e8Rrfi} z=4`f=-f$%gAJJVNPmS@haV)M4 z38rT(L5OhQ(vUaN4%pJlGYne>dd7k^q?he@}_iI_sTum$c#HOs#y4pETwpKZ=m7@3eT4nh$*|8?ays=h}cvS-y zXk{b^7O#@P1z*ZCHT65e<~5P$PLr+Ii;y1O?bF&Fg%&<`!bn3G)~`RQi4utSjl%pr zkY0%OVzSO5!Q5uAb3kOk-9 zES!g*;EplJc+GeX?ir7eo5o+^I{XID;Tik|OUZOHpDZH>$wu-JJ}0}$98%Qy1C)>yYl1;M6Is}>3HLf(SFn(@aZu|*Xc$4v%@woAn@fYK- z#;e9%#vZvqRD;11 zJ}DJTb%AXBj5hLT+Yjb>#M{Wz#Dd`dS5wS$ZFkDw4(O#OlfWBzgQIL=UX zwKr+g5UH*0!DVZ4fxR0q21;1|A|Ou zwZzh)uT*OYGigv*TPUCd!eTtHZ~<6NYVBm@GR+9POHwD%Io2WmdV6DRFGZP8Ew&@r z|D5d|m_y_3i(j8CfsrIcO=;t05|mVvYF30~v8p61IxTz@v^-t(#XY@5toRA)9bUik zR8g@K^{Q+=Q@*vh!q%xwPYg02*;Ib8PUx-(GWh-)e#+VNgqV`IGe@%D_5_9 z*yVWnP9OE3)uSq>=^PeI^?G5{H9GE})oYaEZq-}s)_4b&!K4e|0TKw=GAL=!~7q$-?DG# zy*5p}0c9iHu(p&A{dRyS&OoFO+wR#4{A?)9wE z^GZi61JW7;lI_Tkz&9X%X1zDzHoaXh&Ziu&7RzG*NTDa%R;4ZLho?v&Qq!oQYz!I~ z+U&eF5&W;3l`lCXylZ)4C`}XEj~zrLC9g)B1sB)-^PK5X_)c;-})Wnj37# z{M!bxFr7YVP!+e0OT(Y4P8IAM)l5i6BRmz7m8tYz!@)3>9&7ANhc!~Qr*9jrsjlNb zF>u_|X!!LO?1wEyyoJtg+*0%zfqB!{Mq!@w+L{{9;-`s;nK@Hdvl^%KYF5*bSF_rrw86^j-wWQqXnqKZk4xf1 zatEAEgl^>|}I-5HY+ z%iH2@;EQS{$}Cy0tktbXC0dj@bj2GTVF|tSMiw%=9kMKG z0>$d4W-O(>JB}__KijcWxq7`$5jBPLvshil7Pu^ofp|vG=qWv;MNP4}(~jNS(F2|8 z<5+)o8iQjE?>q(^^kV0C;4AuCmvxvIb;-hfR@YOQ_x{@s%mca&!2F-xxJkd=tv2R? z-Qxn}h$wC=#25lpNKP;Wpux(c{knfA97ObnuI@M767E`s!8&3Z{MR)c=p%Nqy#tXH(~XAS7nUeyGt zc-pbIThKwVKS4D#BwW3fWHbz(m?>N6sNM}UPr2)*F}%2UwHmD?k_0H57QGiBhzHa7 z_Y%b%9-*WM_o)ZjbZMWKxIP#91i%*hxKAps(!{tx8wdjodP7IJ7=wama6++vM z%4V9}FQL*#&YsQW>BBAYLHeMdPZyK2nK#-^t-V}4tBQ6p2cz<%-fr@&QrfOlelGVB zQkH05l|7A;s(=3ooM@B&t>7q~)W5F3;0QOri`8Vq2v-Qipd6rw`j6#!1Hy4aeFylv z2t3-ke>hz^z>G4@8W37_tK>ft9Yvv(+8TOxK;yVF?YFgdX^Ke_YWs5-qdqmX!5!)x zENNhMZE>6v^Kvf>_NPk*R`M1+l;JUNU|f(cG!t((HY7g6-1{Ue8|kxw0ZuTtF)x7Y z3<|&*wjHz%Rqo!PFwDIN55sWlqrojvRrd^T0()u6;M$l+y&tO)BA#L#uQKDL(J}9b zRgwz}s!uYm-CnxleShrt_4^+<0jNP7kT4{vv1Hl6N#Qz+^~8-O;Jl)Ojqv@6YtC&W zXI$(2<&b_a(z6YXEES7D`<}ivG!6%!HMG`?$a{xYgS5P&p|c?&5#tK|IAbSjY9`l2 z9EmRu8E=Q=NP@e>gu*0hiC9Cc)<%a8+u(6wrNmcCXz1`!cuJT%ym4Upc&TiJaB+Bl zXiAlves=UJIO!5$>>v`iA!eS+3|Yq=8IelIjc5f^=(Z7Ka3|LqSsyOZ_eMU0UudsU zEue_59Q6>MQ93$7Gdeu5{BiVf_=~n2QwL_zxbZ#m4vmQgn4R}zY$JfVd2!>z07-S8 zP^<10!M5t5NSEa)oy09JbxR=XqvnO~NVKZxg*HwIf;4(yLPFIwa-p~tK;yx=UQ4Tf za2vi@G?7OW!4o_9U63fla1Wy=HS)OaKx_Rb=oDf0Y()(Tq58L$yK zx~E(}BWjP5NlhP*f>m_<$Ez^+|71i+ra(1#dZ7~HTH8E~NyxEK-{my(lW15@3qI+J z$YCifG2fHY2HmJz>Rikhr+$cerAZSYlTMu!i_dwJI>8ERoNULI8k2cT_sK)>dH3W{ zXK{fie7YR-sx7_sm-wlqS|rQ!v zi)3YssN|Qe9M>y$x)$e}yf~O%p`YT@$g}`IX(WZ&_DojRi9w7OYD@E>xoHX_?M=%P zouWK6RP=Bf*hEWKc_Q%J#q&{mU6(YB_-<7`k1I?4p(jor&#-Y=#G&28Ii=I=C5=OL zcVgz!v6M$x#6lttOF~Y+?B#}dhD0bwD2WC6=L*3YF_vzT(#6Y&;R7FJCmq9TVsNmO zHA2o#u&R(tudxW&Nj=g-JPta!q;ctymC`v08?GaoVTIDc=^b$Wx2AWGUM4gUEi>-y zuLTm)7RFQBQ9R0tXvI@mOyid}x8&{?vv*g1n8E{E-#of>FtOd2;@_!lNn&-=mM>Lm z{?5Fhj4&|kHL5PZ=&@Z=GAjjnQ7fL44ksnYaX9<}xx?3JWez!zPcXfO;&eBb)LVvE}E{rtG|Twyzy)70F<(|AHW`3Xo9aeCb>3!yuo2xPBCTOK+^NDO&BYkJiuHjhk%#7rxZ(i_ro5h1Q}DyYP)>659xu zj3bPOeTz11uDXw7@RK*!4qP;ol25(XBaI^WxpM*>O`$+}1hZDyJzgCh$6hpA!wyc*)P`rERT3wVpi2 z(s#+xRr%bzm@r;U+8z{kQcSi?Jej>uz+I%!=$^-T#Y=;Jzk|8?NhA8l4vS~Lp~akeWDIYSOf3v8zS{is_BZ+Kw;wRHFet^e z>aK=ROyAhG8S_7Py+w)+&~D$vk<|Tk_@2-pz3&3NlJZ3FWH+`zODQ^j^UVi-I`}@0 ztp{kw57BnJqmbIT-HkBcxH|&#R6ioSTBrHi*g$X7oT;Hq=o)5hESBFv){wp+q9yMg`OOJRp4 zOfww_!*GJr6z1-%1e&EbAkeL}W08azoiKhR*O<8?VW?$Ncku;@6ZWfwl`_MRsan5> z66THlI-*E%K)edjfs=pWYNmQQiSmd`>o{M+N@u}UUhZhz*2q7NJ2VN!`{kjAelwTz zCBa=bZyF~k_7!UKUxxxb)=Dx2@vVmw;WygraC3aOk#YEKyc>LcI2`k!A8OWFF3~ix zSc1`ZXnbh2#opjkx40Yd#oOo~8nvFYf{$t^c&mW-IKqKiU6fT)b84XGND62b?j9HO zyoXfPnV<7sO0Go9q5a1BvWIP;s=Iz+?83a;f}J)m@DIJu_cBScx}Y6XfnQqSlbL5c zxEfdR5O2HQKawc&n)L9INcf#TJd)s*IvM++cH(%-A2i`;7jkw5oqIG8p3?P4ZKPOK zEKfOggJnJe6HUGC<7(w|%%tXHQNd3+0lN#o$5c}=N=JJ`k~kxHi{1X%1d_Fa?mJc& zJ^G_#tuaDMJYL`L_>p{^m--OziFs$Mi;C~%9^_3uUI{$)7QB>xKv$gTUB1EXB)>m@ z^JFB+%%Kxb+EO+g6Qq7FPl|IZ?vc=eW1ni>DIx2dE;HViZg?!7sR<*mRXHE!= zBb>3b<(5Fvq;<@VkQA3s!c(d5sn-7aVwZChW$Nsu>*V?ZI_6ZEtqiP%p{p0@*VmFA z6|uIQs!KK&(5t6vlCwF~>vSL%d6B0r;9n?J71VU`CKy#U7ah!I`o@`9pY>7+b8sEh z0)eOG(m7{>aecGSOsp!7e*C_gs97d7_+ypZwBC>7+R7s#wicO^BU14n#!$*#xwtCE zc513_@p6U6D%v^t1uuD*-uZDMZm5)>>XV`zy7i~G>~h^ir2l}^Qzmn{+koo=|Bu0B z?AfR)Q^bOESEUFvPePlV4Xgx8SpZ#dHo$Ldfmns*pec#$LwBE5Z7+_+r)kyZ!}r`` zYCUJeSK=+sHN$JNN#{Z^UvVzFnH~KbLf=w2|jsJqxN!7mn~K54n1)`gv0NhU#w&Z{lFkYg*UwnD2TM5 zs|PF*f&eSG2+`3xn!DlZ(o$`NBjP1^DLGM zh~^5lZtkBkWwg#zs&z;I^hbhEFZ6>2wD-k;8Z(8}M-$9fOe&cb__Ao^J-DfvO_yEl zfE{jLtW#w+?_ec17apn@aIkr_)}=P?Po!FY3Uu$60d&!&SVZ4@DF)`!dzV6B9`!7$ z1CMEwqSx^B^FdK9yHUM^+r8SFV2o$7%mx<6h0khF>G_!sytq zO~~;~>UASAh^Z%_?w(MJ$<4!>*^UXbs@X&i*_bspdRN3TNZ z`l=)NiPUJ#^NDwTWP!PnFSBPWxk6NyIbbR-GtSrx~Zv{O)w0Q*ynIaUZfvs!J|CcQ*-12i*&M<4*}b$iW2~Sh8!yJ67ib zxln@iC33a8IqyDn5S?i*xuY2v#={C6^O0e?o{SNU5-@qhrLyBPY!keXC)L@X1%a%H#(nQ0~x@`}JZk@gDp@jxCqVJm28Y{8$C0 z2P-7e>p?II@xurIMu_4_A-gfdyse zc&p6wS^CM7j{f<=#&E-eR+)-+xYRfAo4KO4=V2aEUzr4Cjb-{_N_x z^?zk{t5MU_r*Lmk$s2k{ZnQS%^(g5FTpPDP z9iDT%n$Kf&voFZdzRzp>W@x^OzfnL7Cf+F=rk_7=?5Ry#qF#HBx4(-cA%+~9OhXg# ztANF0pcfqe>L^r)x+TKkiCe-K58YREl_`4?zsn*B#KUp+Hi0lK{!XAi{KRqyw1czk zcLEda*EmUh9d2-*yYQn%j4ZjU#m~14$|*L_2(<#Qay*j6|8t%6v=g*MXeV;m2_po+ zTz1O{ZDA&>ZvuO;^hW$FZ{xSgDakyxMJsJoX0y>I7}Eb7r>2JmazHD-&oiK>*8QEh zr$X~5P}Ktl<%Hzah}&aI6|-nfBOQm3#47eL3v-g3d2KW87hfHmy)i^f=$ zE#h#g6f=hlBv!jGkhlVx>_P?2C6%q)RGhM;T z%MGsZe)zu~k$;7OqsF^|op*T44Z?Z(u^TS<4R+oQI>Aj=%M3Aai@jxrH{1WlB_N~} zNqlgD_&Z!zrI5GUT=lhyGMae|jbHF7{}K?qw6?l7Bnr2QDBXr6!0M(HvU_F-bN`iN zVD)F_?+)!+U*J{z48fJ3VO^E$yvofPf4ij!ASy~ECxx*hW|7It4UU7iI3~Wlk)*bA zo~?C<{&1d|J)k?xX771`9g9gG5RS!K57fZfj&mMxm%t+Sy%*F#-MNQFof5H<4M_+Z z7ef>YXk=876%Z<+{3tj>_Xi{iUAvDmm$j+{G0`t~GGhniwt@~gia*P%1b)FU_cHLs zz?kYscC`}3wfmV5$D55QlR1d`Fz!;$}?8KGIWIL zY;tAz4yHNURDm2)?JtfMWHnggfZ!z!PEN!f_^JrM51YxV_(EjLBw>UG2?TA+q;8Vd zB)|^NZ6w?5$R#SHW(DxQ^nsK4Xn4@67G2~Ic$UKdYJuU+U=_wE*8zkf+dj%lsrpdXT*SozX)K%J^~;1O(wXJj7FqoG+0-!xF&4Dn_D?`l7j$cx6)YXB|gM*$1LIYmb!_VY- zIV1==^KwTJRO99OAZW_Vz+iZrm*az>9xtmvGpfAn)ey(kzESBb~xBA zVk8^B4N_*Xx5K4m=Y~U6eJK-e1-BY;eEDDFLYG&7-L-V;IQq?SXdJHt75-5U8cM6F z6W%|R>uGgE+o~(EE0=Tk2vk$#G$jH8xD=K~z)D?TOjZlmm)6yxI|pS{m#7D-LtGu_ zV4a*xpfN6nz|k$qHhpohGk%)fgcBKnCbX2k2dDq625jV@4K?M0T&M{j@Ot-1XvE8T zk#NMRDV}zd)!3oTm~4C$41rJCg(wK+*dg4X1eh`xvDGBOT!@_KY#$<%vwB~yiUp+0Y~Tn`Sx9QJcP=*p{g>Z7;C@^AHJ_bK(^J6>-W zFL&n5c-h{o5rnYQ@h~T}LPojCz^h1Ldt;$0ThIW4IFalI5av{8AG>Q={@E`Lpf5*i z)ex@pvUVfqNT2cg6C9D3evRcs+BJrcc>O?Qc#W6v8hp>oA6|prFpEVcKqD;rCE(u2 z6)#9Yr#qk3NrWJ${K|_KKVXaG{bwdZ11Cy35wnwtFps13ZvyY~a!(T}>rb0XR(CXo z3*PhjezxMV1#K@Vv)H<3kc0yMwV4!fVsnWb+5&>v^ycsdCsa2{N_TXU1ZF0|QYR1P z+3cvw9X(C`H?rKRN?GC{*Uj1{A#7ee1hV-_;LV4`AwpP}miYH1SUJ&B%JyDMDO-Ch zDchB;q>`L&1t(CL>smugSjcX*#y_o@#Xf!=8|Sl5ZQwmDHnoAbU@>!Rivi3Mme>}8 zvDULKG;r5mUMQcj#cknu=n^g;E*rGq@)jANv~Qmlv&ros79*Z5?Et^AVSluPMfIIG z7AQ{bxw>@ca5YyvCD*b04fq?paa&Zapr1yZ?gGv9>>|>KBS2UMLXMbqoyoo@3x1TA=>C=-ez5vRPb#Ud)^-2fi(6`2e?Vnrcn1M z4cSK>;bS<*{^$rhP#9Y~K@V8UES(|7vn*$iVJt1uR)n!BVctb}5g*%i8x(+k<-uCPBPe?Fg`8iIHk z>x7WBj}tV%1!|$hh(L_&iZQ;LBxu+WV^=v0Y&fAC-U12HzNEl!WL>NtY9>qQ23yUA z5_X({gpUuxmZxI2VuOEg0r(b<}zr?i-O( zaBr|<(X2N{dijpIy`h_%dCnI?a68$}fzYO`M6F>iDqATf6W*ZDuo0J!QnaxosHK}yD@cS-piEm5L7DkgH z&Rn9`en^IGs2HP+UC4d~MyXxMC}%nPI+d@CU6@l2 z#fkBm6f+)h)Lj)1vt6>vF$y#c)_fvVGk+(SW%UcSK@*{#S$6@Bl@p;cmLQAuZj$~sZ?W3YGK6~ zPL07iy#ixlirA(Nk);C3Ncj-JRO(4g>aL10oEl?NOUey{3*hw%Tk5oR9tPe0NE7y7 zdyiMTqE`W&qIG%>lRk8*=L=4G1;hVUZ~OnHbFkqVWYfbojVK8pQSj5x*a z979Td_zQYk`YLFSTyx!Vyh^wg5PoIIAb7UDFk+a~A7yAGIv!G(1;HCQ9ZmY4s%Gmg z($=%Z@OG&!7v}%jVhnK6Z2xTuUjA=ne_sOi(3od0kHP+&m!x3<>zm`Lu-!{&@7yl4R*)AUW$9_B5!4(h%U$F-(kc}I~tj0>X z30s+x30q0VbH{;9m`LDj)@(Iiwmot5Tn#mVT>pjjT?aMT+BL9<9G~KNZ7oiaEW68F z8AO5UZ)~8Nm`RlR%O)<;Og6WX!X!HeK0rDATAsbeLir!sR~%zi z2y*n<0&jwDS9Izxkb7o<--u~Z}=d1WHnpetscrZ ztj=C&=eAqUf=&7km7DIxWuC6}*|!&OMZR;?_!d4inoFch-^tePhgGGu&Ie!u>db)y z7%^tc@NSFauLIE6n6gW&%EhvJI=^L{BLTXc@fKLqqm2I&@G4Rl37AvD#aBhD!Y&+! zW_<64{s1$1+2t1$5|+h3;2r2b7E%By*Xqbr|P^+wHZ;daMAf~ zMLK0?PCMB|H%HLUg-Xc?kOpv-N^b}Ax_0Ob&yeFiN}F1vCDCc8>R`rt-2 zHtR>6@n-hwX2YUR9@IMJ2>2c=RRk({1sqS!L@lK?lBH9$&)q z9+pju#J(9+gxd0Eeh&NkI=*W`MYwPs;=)RGfGdEaD*zWD|7?Wq zzK%N1`MG!<>iE9`8^6Q#W$S;%$N)Kf_N#RA_=7;g$)COf(Zb2+pb~Dn34wvz1VdIs zi~)~;%auF9MLDDs)@^1}et#inaExQ9j zo>zq-M~3ht47T?UOzmSo^!1$q~MzRjiASQCa@KNIi4IDRU{P>|G z-yat<6}mQ8aPOAVH2LgM?Q+dV5ox01CzJ*?7005K1guGUn(+K zPW!W=jY)0W24XOTBmBOHV(5ltI@WDv>Yq8jZcHWu%Xy6?v6uuhq;4;7#qd6+iJ06m zRmEh-Gz(K2rWKerV%m=B5T?_PO$p>4RC!iiG1NlJKA3P2Lr5pb<|d>H`2AE}A%M|20$)&%ivd>7IXi;Z1KYlwF|=tADc z4{6w&-N@?}{YShE_=`&yd}<(gUpG>OKb*$a^bpv$dJvlttDf(WZdmktha_WB@D6E) zh2>q6ghl&zNj)r5-X)F9NHFI8LE}BxJ1gR;v%epHbt)Lf8k`3!j(u5fqR1DWw=$j$)LP;(L7w{?<%9>)xN#$6{8066MWj zHEK(Z8g74&wo6tee7QmssH{^%l!?8VF-!k5Wm{&et&{LD92_EAu+rq zpf0nG!nbhotZ)dQy~BGbapkh|8zvZTK8)b6sj=i?VixJcNIl-mqZ+dfBUR}FPcwG; iZ5VmTyIdMcVzH<+O30+eC=&PogJ<#6dV5=wy8i>gg1h_x delta 20431 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 UZ8KC%+y6`6^k3vV*ql}SKYTFAx&QzG diff --git a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources index 22f4d5f57..326c9358d 100644 --- a/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/files-events/dist/plugin.wasm.sources @@ -1 +1 @@ -6caa94db0c866c0c7f86011dbf837b3f9edb1c891276a4d435a8d02e8f64e0cb +d433104828c588baf00dece077a087dc7f4cd78097952adca5b6049e366ca944 diff --git a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm index 865a9b9571f28eaba1141ffa61b426055fdb31d4..704fc1115db673501ba2070404adc7938e4bda3b 100644 GIT binary patch delta 4957 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 diff --git a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources index b9928c8e8..f792701c7 100644 --- a/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/grouped-views/dist/plugin.wasm.sources @@ -1 +1 @@ -8d9c6bcd572b6fb54dd42c79d66d4971e55ec3dc072fc769f5634212c2927e22 +4093e1d4791c3786f4b67fd4ef76928fc17048470f858824a29aafd71855c77d diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm index fec17dc6c6389cb06628efcba4bf62694e4db40e..a6cc6567e94c987f67f2157402ebee05586acee0 100644 GIT binary patch delta 5909 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 YaN?w&$nHHqq2#|S58}nT_Zp7+H>k`D>;M1& diff --git a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources index e62ffb8cd..02efff3b3 100644 --- a/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hello/dist/plugin.wasm.sources @@ -1 +1 @@ -2af65b7db7edce8450f0ca829adb3b79fcb64b1ec3d39b785fb9df9423815e7b +aeb6d2f0a589731a1b7f71c9c71232b72d2535f0f555c4fbdb7101861b906d77 diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm index 2a468a118f99963dca91bce1097e9ad427a919f7..d7f853d242c2347b989ff8ddd54da9dbac4f908a 100644 GIT binary patch delta 4725 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 Kz*j|L%KreF%!O?L delta 4719 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 diff --git a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources index 9cfccba7f..28d8e8fb7 100644 --- a/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/hover/dist/plugin.wasm.sources @@ -1 +1 @@ -40f80dcba2f370d02e2d5ad9609b21500fc42fc7c7202b439485a9cbf7baf727 +b723a8826d256fb57f04bbe8aa4b5300c9777f79d889bed2770f6f2bd7a65aa3 diff --git a/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/indexer/dist/plugin.wasm index 5bd99d2cda135a1cd58c696104ede477746275a3..9ead8900ad8a0bb97d13a18f2cac0fd71b563c70 100644 GIT binary patch delta 4987 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 delta 4970 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 delta 4964 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 diff --git a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources index b77911fb8..c5e877c0e 100644 --- a/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/inlay-hints/dist/plugin.wasm.sources @@ -1 +1 @@ -5723b90f8a742f912b315209fc597e3ea7f428a2a234cf4ea5b4620454206d71 +f03ff9487293925027c5681800633d2e0a71ef908db9ea037dff5e4e0a48458e diff --git a/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/jobs/dist/plugin.wasm index 7fe3f427764772d7ef11dadc245afb1ef50b0ce8..fef4c6628a56d008c1d1215a569aafa2ed44f67f 100644 GIT binary patch delta 6167 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 HXx#q*a6d5Q delta 6176 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

E(iFLue7&^Ye7ihA87%)~&O__-3M;_^bdl$O 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 literal 0 HcmV?d00001 diff --git a/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources new file mode 100644 index 000000000..dea43dfa8 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/language-features/dist/plugin.wasm.sources @@ -0,0 +1 @@ +639a25cbc3aaab3080bf2894bc4710f45a1043a8a0f0f7f8c41ac20253f8bc98 diff --git a/crates/ide-plugin/tests/fixtures/language-features/plugin.toml b/crates/ide-plugin/tests/fixtures/language-features/plugin.toml new file mode 100644 index 000000000..0b77ae880 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/language-features/plugin.toml @@ -0,0 +1,31 @@ +id = "test.language-features" +name = "Language features fixture" +version = "0.1.0" +engines.openidea = "^1.4" +wasm = "dist/plugin.wasm" + +[[contributes.providers]] +kinds = ["formatting", "signature-help", "document-highlights", "document-links", "code-lenses", "selection-ranges", "folding", "colors", "document-symbols", "implementation", "type-definition", "gutter-actions", "injections"] +languages = ["plaintext", "text", "sql"] + +[[contributes.providers]] +kinds = ["completion", "hover", "diagnostics"] +languages = ["sql"] + +[[contributes.commands]] +id = "run" +title = "Run fixture" +icon = "run" + +[[contributes.commands]] +id = "state" +title = "Fixture state" + +[[contributes.snippets]] +language = "sql" +path = "snippets.json" + +[[contributes.menus]] +menu = "editor.title" +command = "run" +when = "editorLangId == sql" diff --git a/crates/ide-plugin/tests/fixtures/language-features/snippets.json b/crates/ide-plugin/tests/fixtures/language-features/snippets.json new file mode 100644 index 000000000..f19ee3f95 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/language-features/snippets.json @@ -0,0 +1 @@ +{"Select":{"prefix":["sel","select"],"body":["SELECT ${1:column}","FROM ${2:table}$0"],"description":"Select rows"}} diff --git a/crates/ide-plugin/tests/fixtures/language-features/src/lib.rs b/crates/ide-plugin/tests/fixtures/language-features/src/lib.rs new file mode 100644 index 000000000..db7190a63 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/language-features/src/lib.rs @@ -0,0 +1,144 @@ +use openidea_plugin::language_features::*; +use openidea_plugin::{ + Diagnostic, Error, Invocation, Location, Plugin, Position, Range, Severity, TextEdit, register_plugin, text, +}; +#[derive(Default)] +struct Fixture { + last: Option, +} +fn range(a: u32, b: u32) -> Range { + Range { start: Position { line: 0, character: a }, end: Position { line: 0, character: b } } +} +impl Plugin for Fixture { + fn run_command(&mut self, id: &str, _: &Invocation, arg: Option<&str>) -> Result, Error> { + match id { + "run" => { + self.last = arg.map(str::to_owned); + Ok(self.last.clone()) + } + "state" => Ok(self.last.clone()), + _ => Err(Error::NotFound(id.into())), + } + } +} +impl LanguageFeatures for Fixture { + fn format(&mut self, d: &Document, r: Option, _: FormatOptions) -> Result, Error> { + Ok(vec![TextEdit { + range: r.unwrap_or(Range { start: Position::default(), end: text::position(&d.text, d.text.len()) }), + text: "formatted".into(), + }]) + } + fn signature_help(&mut self, _: &Document, _: Position) -> Option { + Some(SignatureHelp { + signatures: vec![Signature { + label: "query(value)".into(), + documentation: Some("Query".into()), + parameters: vec![Parameter { label: ParameterLabel::Text("value".into()), documentation: None }], + }], + active_signature: 0, + active_parameter: 0, + }) + } + fn document_highlights(&mut self, _: &Document, _: Position) -> Vec { + vec![Highlight { range: range(0, 1), kind: "read".into() }] + } + fn document_links(&mut self, _: &Document) -> Vec { + vec![ + DocumentLink { range: range(0, 1), target: "next.txt".into() }, + DocumentLink { range: range(1, 2), target: "https://example.com".into() }, + ] + } + fn code_lenses(&mut self, _: &Document) -> Vec { + vec![ + CodeLens { range: range(0, 1), title: "Run".into(), command: "run".into(), argument: Some("lens".into()) }, + CodeLens { range: range(0, 1), title: "Foreign".into(), command: "foreign".into(), argument: None }, + ] + } + fn selection_ranges(&mut self, _: &Document, _: Position) -> Vec { + vec![range(0, 1), range(0, 5)] + } + fn folding(&mut self, _: &Document) -> Vec { + vec![FoldRange { start_line: 0, end_line: 2, kind: "region".into() }] + } + fn colors(&mut self, _: &Document) -> Vec { + vec![ColorInfo { range: range(0, 1), red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0 }] + } + fn color_presentations(&mut self, d: &Document, _: ColorInfo) -> Vec { + vec![format!("red-{}", d.language), "#ff0000".into()] + } + fn document_symbols(&mut self, _: &Document) -> Vec { + vec![ + DocumentSymbol { + name: "Outer".into(), + detail: "module".into(), + kind: "module".into(), + range: range(0, 5), + selection_range: range(0, 1), + parent: None, + }, + DocumentSymbol { + name: "Inner".into(), + detail: "value".into(), + kind: "variable".into(), + range: range(1, 2), + selection_range: range(1, 2), + parent: Some(0), + }, + ] + } + fn implementation(&mut self, d: &Document, _: Position) -> Vec { + vec![Location { path: d.path.clone(), range: range(1, 2) }] + } + fn type_definition(&mut self, d: &Document, _: Position) -> Vec { + vec![Location { path: d.path.clone(), range: range(2, 3) }] + } + fn gutter_actions(&mut self, _: &Document) -> Vec { + vec![GutterAction { + line: 0, + icon: Some("run".into()), + tooltip: "Run fixture".into(), + command: "run".into(), + argument: Some("gutter".into()), + }] + } +} +impl Injections for Fixture { + fn injections(&mut self, d: &Document) -> Vec { + if d.language == "sql" { + return vec![]; + } + let Some(start) = d.text.find("[[") else { return vec![] }; + let Some(end) = d.text[start + 2..].find("]]") else { return vec![] }; + vec![Injection { + range: Range { start: text::position(&d.text, start + 2), end: text::position(&d.text, start + 2 + end) }, + language: "sql".into(), + }] + } +} +impl openidea_plugin::providers::Providers for Fixture { + fn completion( + &mut self, + d: &Document, + _: Position, + _: Option<&str>, + ) -> Vec { + vec![openidea_plugin::providers::CompletionItem::new( + &format!("{}:{}", d.language, d.text), + openidea_plugin::providers::CompletionKind::Text, + )] + } +} +impl openidea_plugin::hover::HoverProvider for Fixture { + fn hover(&mut self, d: &Document, _: Position) -> Option { + Some(openidea_plugin::hover::Hover::new(format!("{}:{}", d.language, d.text))) + } +} +impl openidea_plugin::diagnostics::DiagnosticsProvider for Fixture { + fn diagnostics(&mut self, d: &Document) -> Vec { + vec![ + Diagnostic::new(range(0, 1), Severity::Warning, "outside"), + Diagnostic::new(range(4, 7), Severity::Warning, format!("{}:{}", d.language, d.text)), + ] + } +} +register_plugin!(Fixture, language_features, injections, providers, hover, diagnostics); diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm index 1c6092c273f859e4770671f11d458fed9907fe2f..2a180d2c1c6bfc2d9948869a0706c00a0ff0fccf 100644 GIT binary patch delta 6022 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 J@K{e${|VC3(m?6-7kFc%^76T4ZdN%Lewd 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 LUouyI0kQuDFS)>3 diff --git a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources index 1612f2bb1..36c651b29 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/misbehave/dist/plugin.wasm.sources @@ -1 +1 @@ -185557c699fc00796eb487f50c688c3e91d8895bd05d4185b492352c187f9d98 +741d2909f021ec4c2f572edba0d83bcee4843a1aae4d8a79ea0367419a667640 diff --git a/crates/ide-plugin/tests/fixtures/misbehave/src/lib.rs b/crates/ide-plugin/tests/fixtures/misbehave/src/lib.rs index 59db88875..18234bec2 100644 --- a/crates/ide-plugin/tests/fixtures/misbehave/src/lib.rs +++ b/crates/ide-plugin/tests/fixtures/misbehave/src/lib.rs @@ -5,9 +5,9 @@ wit_bindgen::generate!({ inline: " package fixture:misbehave; world misbehave { - include openidea:plugin/plugin-syntax@1.3.0; - export openidea:plugin/providers@1.3.0; - export openidea:plugin/file-actions@1.3.0; + include openidea:plugin/plugin-syntax@1.4.0; + export openidea:plugin/providers@1.4.0; + export openidea:plugin/file-actions@1.4.0; } ", path: "../../../../../wit", diff --git a/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/path-events/dist/plugin.wasm index b77075715b4904d3cc646229c29e2b3dd0da9027..ac24fadae69e545107b884c8363326c228a28d39 100644 GIT binary patch delta 19323 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 delta 19036 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 diff --git a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources index 0d0841088..de92d6fac 100644 --- a/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/private-dir/dist/plugin.wasm.sources @@ -1 +1 @@ -18957534fc011510894544b61e154a9d0972c27ab1872d9d2accd01864984fa0 +e14bfd9a7f61a7ff44f656a148ed133e973c66edfb0859bbff4d4cda9663eb3a diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm index d270f47729159dcdffc8fb9380c0b5d3e8a28731..883742c9d9457bad2f80e5ed6d7f570575f38636 100644 GIT binary patch delta 4909 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 MM@blaWDZIE58B6-X#fBK delta 4956 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 PWfT98mN566Ok)2JrK_~K diff --git a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources index da42ec0b8..9a638a03a 100644 --- a/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/semantic-tokens/dist/plugin.wasm.sources @@ -1 +1 @@ -3562bc253b0b4f0fa1cb15b9c8a7470a09719b28008aa8f5f459bbaba8eda598 +1c2f975248675b935adac4ac096d23d3f45ca44a46e9e4a2ee578bb69bb35356 diff --git a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm index 49f377ecb307ae8011ba264d40e31c01c36947e0..82c1b2f4471b2a52bb070ae86bed7d3565fad734 100644 GIT binary patch delta 5313 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 ITa2Us1$$s;5{L_uhXHUvm`NF!}QAZV&- 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 diff --git a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources index 7b3b8525e..655dad638 100644 --- a/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/settings-pages/dist/plugin.wasm.sources @@ -1 +1 @@ -f28b40f3708ec4f828d04f7cec29208b66d0ab6158b26c60c0bd989cdc74f9b4 +2d566981f7f3aa78fbd6188e2bbdb17e33c01f6863e9b9a0b656f541371374bc diff --git a/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/storage/dist/plugin.wasm index 6dad1e8a64946c95ce072bed05348b3fbc55edfc..cbfc1524ca4e7557953e347746df04906874f674 100644 GIT binary patch delta 4739 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 delta 5395 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 delta 5028 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 diff --git a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources index 02312108a..8eed9241d 100644 --- a/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/tables/dist/plugin.wasm.sources @@ -1 +1 @@ -28e1d58c8ab50574e184cb401050a6fd2755c9398cd3a3720dc376f67461a44b +c4740acdb3c805e13bf9a0c2e2c2a830336a1085285c39de3dce73a301f70150 diff --git a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm index 176b52e9921cbf2775cfe798c874656bb8d4bc97..aa61e499841b1e68cb3454a16096fb7e780a81f1 100644 GIT binary patch delta 4682 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 diff --git a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources index 7c51baa74..a3dd95e7c 100644 --- a/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-content/dist/plugin.wasm.sources @@ -1 +1 @@ -e80605523d9aaa1c88c83deac439a39a739541bdb327e279352441547de3c232 +315d592adb776916170a6c2293379e7a7a086b9ae513dcac4aefad925d91df5d diff --git a/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/web-log/dist/plugin.wasm index 5671f3313d7abc2bcba37f1cdd114e8eceb75d28..82022e0ed099e6b2cf7126481e148dfdccb81c74 100644 GIT binary patch delta 4742 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�Z3B=s%l}_vTUhyRI)qd*c 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 Vyw*>S?}L6g`2X@obK`L6{{WYG-lzZo delta 4882 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 diff --git a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources index c59478b01..22ad70b65 100644 --- a/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/web-pty/dist/plugin.wasm.sources @@ -1 +1 @@ -009f7e069920bcf0e7699b57cd41954dc1dcb620977e081c24ec58ef0a9809d2 +c98b94057e62a575d6c4b6c4c92e1e88e101bcef9138683e47b0bebf262e3f63 diff --git a/crates/ide-remote/Cargo.toml b/crates/ide-remote/Cargo.toml index eb9828c7d..cd69e8b50 100644 --- a/crates/ide-remote/Cargo.toml +++ b/crates/ide-remote/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true [dependencies] ide-core.workspace = true +dunce.workspace = true ide-ssh.workspace = true ide-run.workspace = true serde.workspace = true diff --git a/crates/ide-remote/src/lib.rs b/crates/ide-remote/src/lib.rs index 2d19e4ad3..7a42a0132 100644 --- a/crates/ide-remote/src/lib.rs +++ b/crates/ide-remote/src/lib.rs @@ -16,3 +16,5 @@ pub use deploy::{ pub use mapping::{Excludes, Mappings, ResolvedMapping}; pub use service::{DeployedPath, DeploymentRoot, RemoteService}; pub use target::{RemoteRuns, RemoteStart}; + +pub mod plugin_target; diff --git a/crates/ide-remote/src/plugin_target.rs b/crates/ide-remote/src/plugin_target.rs new file mode 100644 index 000000000..cec24b904 --- /dev/null +++ b/crates/ide-remote/src/plugin_target.rs @@ -0,0 +1,185 @@ +//! Shell-free argv construction for plugin-owned run targets. +use ide_core::remote_settings::PluginRunTarget; +use ide_core::{IdeError, IdeResult}; +use ide_run::{RunConfig, wrap}; +use std::path::Path; + +/// Validates a bounded target before replacing an existing registration. +pub fn validate(target: &PluginRunTarget) -> IdeResult<()> { + let label = |s: &str, max: usize| !s.is_empty() && s.len() <= max && !s.chars().any(char::is_control); + if !label(&target.id, 128) || !label(&target.name, 256) || !label(&target.program, 256) { + return Err(IdeError::invalid("invalid run target id, name or program")); + } + let argv = |args: &[String]| args.len() <= 256 && args.iter().all(|a| a.len() <= 16384 && !a.contains('\0')); + let mut names = std::collections::BTreeSet::new(); + if !argv(&target.args) + || !argv(&target.workdir_args) + || target.commands.len() > 64 + || target.commands.iter().any(|(name, args)| { + !label(name, 256) || wrap::program_name(name) != *name || !names.insert(name) || !argv(args) + }) + || serde_json::to_vec(target).map_or(true, |v| v.len() > 256 << 10) + || target + .container_root + .as_ref() + .is_some_and(|r| !r.starts_with('/') || r.contains(['\0', '\n', '\r']) || r.split('/').any(|p| p == "..")) + { + return Err(IdeError::invalid("invalid run target arguments or container root")); + } + Ok(()) +} + +/// Wraps argv, keeping the local cwd and environment for the approved wrapper. +/// The wrapper decides whether to forward environment into its container. +pub fn prepare(root: &Path, target: &PluginRunTarget, program: &Path, config: &RunConfig) -> IdeResult { + validate(target)?; + let cwd = wrap::run_dir(root, config); + let root = dunce::canonicalize(root).unwrap_or_else(|_| root.to_path_buf()); + let cwd = dunce::canonicalize(&cwd).unwrap_or(cwd); + let relative = + cwd.strip_prefix(&root).ok().filter(|p| !p.components().any(|c| matches!(c, std::path::Component::ParentDir))); + let mapped_cwd = target.container_root.as_deref().zip(relative).map(|(container, rel)| { + if rel.as_os_str().is_empty() { + container.to_owned() + } else { + format!("{}/{}", container.trim_end_matches('/'), rel.to_string_lossy().replace('\\', "/")) + } + }); + let mapped = target + .container_root + .as_deref() + .map(|container| crate::target::argv_command(config, &Default::default(), &root, container)); + let command = mapped.as_ref().map_or_else( + || { + let p = Path::new(&config.command); + if p.is_absolute() && !p.starts_with(&root) { + wrap::program_name(&config.command) + } else { + config.command.clone() + } + }, + |m| m.program.clone(), + ); + let name = wrap::program_name(&config.command); + let override_args = target.commands.iter().find(|(n, _)| n == &name).map(|(_, a)| a); + let mut args = override_args.unwrap_or(&target.args).clone(); + if let Some(cwd) = mapped_cwd { + args.extend(target.workdir_args.iter().map(|a| a.replace("{cwd}", &cwd))); + } + if override_args.is_none() { + args.push(command); + } + args.extend(mapped.as_ref().map_or_else(|| config.args.clone(), |m| m.args.clone())); + let mut result = config.clone(); + result.command = program.to_string_lossy().into_owned(); + result.args = args; + result.cwd = Some(cwd.into()); + result.extras.target = None; + // A wrapper's -c/-e are its own flags, not interpreter flags to inspect. + result.kind = "shell".into(); + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + fn config(command: &str, args: &[&str], cwd: &str) -> RunConfig { + serde_json::from_value(serde_json::json!({"id":"test", "name":"Test", "kind":"php", "command":command, "args":args, "cwd":cwd, "env":{}, "detected":false, "isTest":false})).unwrap() + } + #[test] + fn prefix_override_cwd_and_literal_argv() { + let target = PluginRunTarget { + id: "ddev".into(), + name: "DDEV".into(), + program: "ddev".into(), + args: vec!["exec".into()], + workdir_args: vec!["--dir".into(), "{cwd}".into()], + container_root: Some("/var/www/html".into()), + commands: vec![("composer".into(), vec!["composer".into()])], + }; + let tmp = tempfile::tempdir().unwrap(); + let base = dunce::canonicalize(tmp.path()).unwrap(); + let root = base.join("project"); + std::fs::create_dir_all(root.join("src")).unwrap(); + let program = base.join("approved/ddev"); + let php = base.join("tools/php"); + let run = + prepare(&root, &target, &program, &config(php.to_str().unwrap(), &["x.php", "$(touch bad); a b"], "src")) + .unwrap(); + assert_eq!(Path::new(&run.command), program); + assert_eq!(run.args, ["exec", "--dir", "/var/www/html/src", "php", "x.php", "$(touch bad); a b"]); + assert_eq!(run.cwd.unwrap(), root.join("src")); + let outside = base.join("outside"); + let run = prepare( + &root, + &target, + &program, + &config(r"C:\tools\composer.exe", &["install"], outside.to_str().unwrap()), + ) + .unwrap(); + assert_eq!(run.args, ["composer", "install"]); + let in_project = prepare(&root, &target, &program, &config("/tools/composer", &["install"], "src")).unwrap(); + assert_eq!(in_project.args, ["composer", "--dir", "/var/www/html/src", "install"]); + let run = prepare(&root, &target, &program, &config("composer", &["install"], "../outside")).unwrap(); + assert_eq!(run.args, ["composer", "install"]); + let mut target = target; + target.container_root = None; + let run = prepare(&root, &target, &program, &config("php", &["x.php"], "src")).unwrap(); + assert_eq!(run.args, ["exec", "php", "x.php"]); + let run = prepare(&root, &target, &program, &config("echo a; touch bad", &[], ".")).unwrap(); + assert_eq!(run.args, ["exec", "echo a; touch bad"]); + } + + #[test] + fn command_overrides_match_both_path_separators_on_every_platform() { + let tmp = tempfile::tempdir().unwrap(); + let target = PluginRunTarget { + id: "ddev".into(), + name: "DDEV".into(), + program: "ddev".into(), + args: vec!["exec".into()], + workdir_args: Vec::new(), + container_root: Some("/container".into()), + commands: vec![("php".into(), vec!["php".into()])], + }; + for command in ["php", "/usr/bin/php", r"C:\tools\php.exe", r"\\?\C:\tools\php.exe", "C:/tools/php.cmd"] { + for container_root in [None, target.container_root.clone()] { + let target = PluginRunTarget { container_root, ..target.clone() }; + let run = prepare(tmp.path(), &target, Path::new("ddev"), &config(command, &["x.php"], ".")).unwrap(); + assert_eq!(run.args, ["php", "x.php"], "{command}"); + } + } + } + + #[cfg(windows)] + #[test] + fn plain_and_verbatim_roots_map_project_commands_and_cwds() { + let tmp = tempfile::tempdir().unwrap(); + std::fs::create_dir(tmp.path().join("src")).unwrap(); + let root = dunce::canonicalize(tmp.path()).unwrap(); + let verbatim = std::fs::canonicalize(tmp.path()).unwrap(); + let target = PluginRunTarget { + id: "ddev".into(), + name: "DDEV".into(), + program: "ddev".into(), + args: vec!["exec".into()], + workdir_args: vec!["--dir={cwd}".into()], + container_root: Some("/container".into()), + commands: Vec::new(), + }; + let command = root.join("tool.exe"); + for project in [&root, &verbatim] { + for cwd in [root.join("src"), verbatim.join("src")] { + let run = prepare( + project, + &target, + Path::new("ddev"), + &config(command.to_str().unwrap(), &["arg"], cwd.to_str().unwrap()), + ) + .unwrap(); + assert_eq!(run.args, ["exec", "--dir=/container/src", "/container/tool.exe", "arg"]); + assert_eq!(run.cwd.unwrap(), root.join("src")); + } + } + } +} diff --git a/crates/ide-remote/src/target.rs b/crates/ide-remote/src/target.rs index 97577e0d9..1bf971216 100644 --- a/crates/ide-remote/src/target.rs +++ b/crates/ide-remote/src/target.rs @@ -109,6 +109,22 @@ fn interpreter<'a>(program: &str, target: &'a RunTarget) -> Option<&'a str> { /// Builds the remote command of `config`; `root` (the project) maps onto `remote_root`. pub fn remote_command(config: &RunConfig, target: &RunTarget, root: &Path, remote_root: &str) -> RemoteCommand { + target_command(config, target, root, remote_root, true) +} + +/// Maps paths and interpreters while preserving argv, including a command containing spaces. +/// Unlike SSH/container shell configurations, plugin wrappers never accept a shell line. +pub fn argv_command(config: &RunConfig, target: &RunTarget, root: &Path, remote_root: &str) -> RemoteCommand { + target_command(config, target, root, remote_root, false) +} + +fn target_command( + config: &RunConfig, + target: &RunTarget, + root: &Path, + remote_root: &str, + allow_shell: bool, +) -> RemoteCommand { let root_str = root.to_string_lossy().into_owned(); let map = |s: &str| -> String { if root_str.is_empty() { @@ -129,7 +145,7 @@ pub fn remote_command(config: &RunConfig, target: &RunTarget, root: &Path, remot }; let env = config.env.iter().map(|(k, v)| (k.clone(), map_text(v))).collect(); let command = config.command.trim(); - if config.args.is_empty() && command.contains(char::is_whitespace) { + if allow_shell && config.args.is_empty() && command.contains(char::is_whitespace) { return RemoteCommand { program: String::new(), args: Vec::new(), diff --git a/crates/ide-sdk/src/apply.rs b/crates/ide-sdk/src/apply.rs index 7ad8e2009..14c689878 100644 --- a/crates/ide-sdk/src/apply.rs +++ b/crates/ide-sdk/src/apply.rs @@ -1173,6 +1173,29 @@ fn add_maven_options(args: &mut Vec, options: &[String], additive_profil args.splice(0..0, missing); } +/// The interpreter target or project default; an explicit local choice returns an empty id. +pub fn run_target<'a>(config: &RunConfig, settings: &'a Settings) -> Option<&'a str> { + let name = ide_run::wrap::program_name(&config.command); + let target = if matches!(name.as_str(), "php" | "composer" | "phpunit" | "pest") + || matches!(config.kind.as_str(), "php" | "composer" | "phpunit") + { + &settings.languages.php.run_target + } else if matches!(name.as_str(), "node" | "npm" | "npx" | "pnpm" | "yarn" | "bun") + || matches!(config.kind.as_str(), "node" | "npm" | "vitest" | "jest" | "mocha") + { + &settings.languages.node.run_target + } else { + return non_empty(&settings.run.default_target); + }; + if target == "local" { + Some("") + } else if target.is_empty() { + non_empty(&settings.run.default_target) + } else { + Some(target) + } +} + /// Points run configurations at the selected toolchains: Python configs use the /// project interpreter, npm scripts the chosen package manager and Node.js, /// cargo / go configs the selected toolchain, Maven / Gradle their runner JDK @@ -1210,6 +1233,19 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings }; for c in configs.iter_mut() { + if let Some(pm) = pm + && c.kind == "npm" + && c.detected + && matches!(c.command.as_str(), "npm" | "pnpm" | "yarn" | "bun") + { + c.command = pm.to_owned(); + } + if c.extras.target.is_none() { + c.extras.target = run_target(c, s).map(str::to_owned); + } + if c.extras.target.as_deref().is_some_and(|t| t.starts_with("plugin-target:")) { + continue; + } let owner = ide_run::scope::owner(root, c); resolve_run_command(c, root, s); let mut path_dirs: Vec = Vec::new(); @@ -1243,13 +1279,6 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings } } "npm" | "node" | "deno" | "bun" => { - if let Some(pm) = pm - && c.kind == "npm" - && c.detected - && matches!(c.command.as_str(), "npm" | "pnpm" | "yarn" | "bun") - { - c.command = pm.to_owned(); - } // The configuration's own Node interpreter, else the project's. let own = c.extras.params.get("nodeInterpreter").and_then(|p| setting_path(root, p)); if let Some(dir) = @@ -1367,6 +1396,9 @@ pub fn without_derived(config: &RunConfig, root: &Path, s: &Settings) -> RunConf c }; let mut out = config.clone(); + if out.extras.target.as_deref() == run_target(&out, s) { + out.extras.target = None; + } // Until nothing changes: some entries are derived only once others are gone // (the virtualenv's `PATH` is added only without `VIRTUAL_ENV`). An entry derived before // the configuration's own choice (an npm script's Node interpreter) is derived too. diff --git a/crates/ide-sdk/src/tests.rs b/crates/ide-sdk/src/tests.rs index ec9d12190..dcef83a8d 100644 --- a/crates/ide-sdk/src/tests.rs +++ b/crates/ide-sdk/src/tests.rs @@ -2169,3 +2169,31 @@ fn project_structure_module_jdk_language_level_and_external_libraries() { let groups = crate::libraries::project_libraries(root, &settings, &[]); assert!(groups.iter().any(|g| g.name == "servlet-api" && g.kind == "library" && g.roots.len() == 1)); } + +#[test] +fn plugin_interpreter_targets_route_all_php_node_command_families() { + let mut settings = Settings::default(); + settings.languages.php.run_target = "plugin-target:php".into(); + settings.languages.node.run_target = "plugin-target:node".into(); + settings.languages.php.interpreter = "/local/php".into(); + settings.languages.node.interpreter = "/local/node".into(); + for (command, kind, expected) in [ + ("php", "php", "php"), + ("composer", "composer", "php"), + ("vendor/bin/phpunit", "phpunit", "php"), + ("node", "node", "node"), + ("npm", "npm", "node"), + ("node_modules/.bin/vitest", "vitest", "node"), + ("npx", "node", "node"), + ] { + let mut config: ide_run::RunConfig = serde_json::from_value(serde_json::json!({ "id":"run", "name":"run", "kind":kind, "command":command, "args":[], "env":{}, "detected":true, "isTest":false })).unwrap(); + crate::apply::apply_to_run_configs(std::slice::from_mut(&mut config), Path::new("/project"), &settings); + assert_eq!(config.extras.target.as_deref(), Some(format!("plugin-target:{expected}").as_str())); + assert_eq!(config.command, command); + assert!(config.env.is_empty()); + assert!(crate::apply::without_derived(&config, Path::new("/project"), &settings).extras.target.is_none()); + config.extras.target = Some(String::new()); + crate::apply::apply_to_run_configs(std::slice::from_mut(&mut config), Path::new("/project"), &settings); + assert_eq!(config.extras.target.as_deref(), Some("")); + } +} diff --git a/crates/ide-vcs/src/cli.rs b/crates/ide-vcs/src/cli.rs index 3a73f2704..56b20e938 100644 --- a/crates/ide-vcs/src/cli.rs +++ b/crates/ide-vcs/src/cli.rs @@ -381,10 +381,46 @@ fn run_cancellable_raw_stdin( args: &[&str], stdin: Option<&[u8]>, cancel: &CancellationToken, +) -> IdeResult { + run_capture(root, args, stdin, cancel, usize::MAX) +} + +/// Neutralizes repository clean/process filters for a read of working-tree bytes. +/// Git still applies them with --no-ext-diff and --no-textconv. +pub(crate) fn filter_overrides(root: &Path, cancel: &CancellationToken) -> IdeResult> { + if crate::execution_allowed() { + return Ok(Vec::new()); + } + let config = run_bounded(root, &["config", "--null", "--list"], cancel, 4 << 20)?; + let mut overrides = Vec::new(); + for record in config.stdout_str().split('\0') { + let key = record.split('\n').next().unwrap_or_default(); + if key.starts_with("filter.") + && (key.ends_with(".clean") || key.ends_with(".process") || key.ends_with(".required")) + { + overrides.push("-c".into()); + overrides.push(format!("{key}={}", if key.ends_with(".required") { "false" } else { "" })); + } + } + Ok(overrides) +} + +/// Cancellable capture with bounded stdout and stderr; stops git on overflow. +pub(crate) fn run_bounded(root: &Path, args: &[&str], cancel: &CancellationToken, max: usize) -> IdeResult { + let out = run_capture(root, args, None, cancel, max)?; + if out.code == Some(0) { Ok(out) } else { Err(failure(args, &out.stderr, &out.stdout, out.code)) } +} +fn run_capture( + root: &Path, + args: &[&str], + stdin: Option<&[u8]>, + cancel: &CancellationToken, + max: usize, ) -> IdeResult { if cancel.is_cancelled() { return Err(IdeError::cancelled()); } + let overflow = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let mut cmd = command(root)?; cmd.args(args) .stdin(if stdin.is_some() { Stdio::piped() } else { Stdio::null() }) @@ -404,22 +440,32 @@ fn run_cancellable_raw_stdin( _ => None, }; let capture = |mut pipe: Box| { + let overflow = overflow.clone(); std::thread::spawn(move || { let mut bytes = Vec::new(); - pipe.read_to_end(&mut bytes).map(|_| bytes) + (&mut pipe).take(max.saturating_add(1) as u64).read_to_end(&mut bytes)?; + if bytes.len() > max { + overflow.store(true, std::sync::atomic::Ordering::Release); + std::io::copy(&mut pipe, &mut std::io::sink())?; + } + Ok(bytes) }) }; let stdout = capture(Box::new(child.stdout.take().expect("piped stdout"))); let stderr = capture(Box::new(child.stderr.take().expect("piped stderr"))); let status = loop { - if cancel.is_cancelled() { + if cancel.is_cancelled() || overflow.load(std::sync::atomic::Ordering::Acquire) { terminate_tree(&mut child, KILL_GRACE); let _ = stdout.join(); let _ = stderr.join(); if let Some(w) = writer { let _ = w.join(); } - return Err(IdeError::cancelled()); + return Err(if cancel.is_cancelled() { + IdeError::cancelled() + } else { + IdeError::invalid(format!("git output exceeds {max} bytes")) + }); } if let Some(status) = child.try_wait()? { break status; @@ -434,6 +480,9 @@ fn run_cancellable_raw_stdin( }; let out = Output { stdout: join(stdout)?, stderr: join(stderr)?, code: status.code() }; session.finish(status.success(), &String::from_utf8_lossy(&out.stderr)); + if out.stdout.len() > max || out.stderr.len() > max { + return Err(IdeError::invalid(format!("git output exceeds {max} bytes"))); + } Ok(out) } diff --git a/crates/ide-vcs/src/compare.rs b/crates/ide-vcs/src/compare.rs index d2404185c..6e1bc738b 100644 --- a/crates/ide-vcs/src/compare.rs +++ b/crates/ide-vcs/src/compare.rs @@ -135,19 +135,7 @@ impl Repos { let mut compare = || -> IdeResult<(String, Vec)> { // Worktree comparisons must not run repository-defined clean/process filters // in Restricted Mode. External diff and textconv are disabled below as well. - let mut overrides = Vec::new(); - if !crate::execution_allowed() { - let config = cli::run_cancellable(repo.root(), &["config", "--null", "--list"], cancel)?; - for record in config.stdout_str().split('\0') { - let key = record.split('\n').next().unwrap_or_default(); - if key.starts_with("filter.") - && (key.ends_with(".clean") || key.ends_with(".process") || key.ends_with(".required")) - { - overrides.push("-c".to_owned()); - overrides.push(format!("{key}={}", if key.ends_with(".required") { "false" } else { "" })); - } - } - } + let overrides = cli::filter_overrides(repo.root(), cancel)?; let run = |args: &[&str]| { let mut full: Vec<&str> = overrides.iter().map(String::as_str).collect(); full.extend_from_slice(args); diff --git a/crates/ide-vcs/src/lib.rs b/crates/ide-vcs/src/lib.rs index 9e59c7db3..9c1c12106 100644 --- a/crates/ide-vcs/src/lib.rs +++ b/crates/ide-vcs/src/lib.rs @@ -1013,6 +1013,30 @@ impl Repo { let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); refs::stashes(self) } + /// Working tree against a revision, optionally restricted to a file. Bounded and cancellable. + pub fn working_tree_patch( + &self, + path: Option<&Path>, + against: Option<&str>, + max_bytes: usize, + cancel: &CancellationToken, + ) -> IdeResult { + let _scope = crate::ExecutionScope::enter(self.execution_allowed.load(std::sync::atomic::Ordering::Acquire)); + let rev = against.unwrap_or("HEAD"); + if rev.is_empty() || rev.starts_with('-') || rev.contains('\0') { + return Err(ide_core::IdeError::invalid("invalid revision")); + } + let rel = path.map(|p| self.rel_path(p)).transpose()?; + let overrides = cli::filter_overrides(self.root(), cancel)?; + let mut args: Vec<&str> = overrides.iter().map(String::as_str).collect(); + args.extend(["--literal-pathspecs", "--no-pager", "diff", "--no-ext-diff", "--no-textconv", rev, "--"]); + if let Some(rel) = &rel { + args.push(rel); + } + let out = cli::run_bounded(self.root(), &args, cancel, max_bytes)?; + Ok(out.stdout_str()) + } + /// Unified diff of a file: index↔worktree (`staged == false`, untracked files diff /// against /dev/null) or HEAD↔index (`staged == true`). Empty when unchanged. pub fn diff_text(&self, path: &Path, staged: bool, encoding: impl Into) -> IdeResult { diff --git a/crates/ide-vcs/src/write.rs b/crates/ide-vcs/src/write.rs index 64958f0fb..21ae5f937 100644 --- a/crates/ide-vcs/src/write.rs +++ b/crates/ide-vcs/src/write.rs @@ -146,7 +146,12 @@ fn converted_blame( let current = crate::diff::normalize_eol(text); let mut result = uncommitted_lines(current.split_inclusive('\n').count()); let Some(base) = r.file_bytes_at(path, Some(revision))? else { return Ok(result) }; - let out = run(r.root(), &["--literal-pathspecs", "blame", "--porcelain", revision, "--", rel], None)?; + let args: &[&str] = if crate::execution_allowed() { + &["--literal-pathspecs", "blame", "--porcelain", revision, "--", rel] + } else { + &["--literal-pathspecs", "blame", "--no-textconv", "--porcelain", revision, "--", rel] + }; + let out = run(r.root(), args, None)?; let physical = crate::blame::parse_porcelain(&out.stdout_str()); let raw = crate::text_encoding::decode_raw(&base, encoding); let raw = crate::diff::normalize_eol(&raw); diff --git a/deny.toml b/deny.toml index 418b97b38..56636f0de 100644 --- a/deny.toml +++ b/deny.toml @@ -11,7 +11,7 @@ ignore = [ [licenses] allow = [ "MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-2-Clause", - "BSD-3-Clause", "ISC", "Zlib", "0BSD", "CC0-1.0", "Unlicense", + "BSD-3-Clause", "ISC", "Zlib", "0BSD", "CC0-1.0", "Unlicense", "MIT-0", # File-level copyleft, used unmodified as dependencies. "MPL-2.0", "Unicode-3.0", "Unicode-DFS-2016", "CDLA-Permissive-2.0", "OpenSSL", diff --git a/plugins/README.md b/plugins/README.md index 333cd1df4..af88cd494 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -37,7 +37,7 @@ sdk/rust/openidea-plugin/ # the Rust SDK (host API, Plugin trait, mock host) crates/ide-plugin/tests/fixtures/ # test plugins: hello, jobs, language-toy, misbehave, semantic-tokens, hover, commit, inlay-hints ``` -The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.3.0`, world +The plugin API is defined in [`wit/`](../wit/) (`openidea:plugin@1.4.0`, world `plugin`). ## 1. Quick start @@ -137,24 +137,27 @@ work), but 0.1 code no longer runs: rebuild it against the 1.0 SDK. ### Contribution points -| Point | Declaration | Code needed | -| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `commands` | `id`, `title`, `category?`, `when?` | to run them | -| `themes` | `path` (theme JSON), `editor-scheme?` | no | -| `iconThemes` | `id`, `label`, `path` (icon theme JSON, see §5) | no | -| `languages` | `path` (`languages/.toml` format, see §6) | no | -| `runDetectors` | `id`, `name`, `files`, `command`, `args?`, `test?` (§6.2) | no | -| `providers` | `kinds`, `languages` (editor providers, see _Providers_) | yes | -| `secrets` | `name`, `label`, `description?` (see _Network_) | to use them | -| `menus` | `menu` (`editor.context`, `projectView.context`, `editorTab.context`, `viewItem.context`, `tools`, `commit.message`), `view?`, `command`, `when?` | no | -| `keymaps` | `command`, `key`, `mac?` (`Ctrl+Alt+C`; a chord: `Meta+K Meta+C`) | no | -| `statusBar` | `id`, `text`, `tooltip?`, `command?`, `align?`, `priority?` | no | -| `settings` | `schema` (JSON file) or `pages` (API 1.3), `title?` — see _Settings_ | no | -| `views` | `id`, `title`, `icon?` (an icon name or a package SVG), `location` (`left`, `right`, `bottom`, `services`), `web?` (_Web views_), `group?`, `when?` (API 1.3) | without `web` | -| `customEditors` | `id`, `title`, `patterns` (file names), `priority` (`default`, `option`), `layout?`, `web?` | without `web` | -| `fileIcons` | `id`, `patterns` (file names), `icon` (SVG), `iconDark?` — whatever icon theme is in use | no | -| `fileTemplates` | `id`, `name`, `extension`, `path`; API 1.3: `fileName`, `variables = [{ name, prompt }]` — New menu | no | -| `commitChecks` | `id`, `title` — a before-commit check in Commit Options (see _Commits_) | yes | +| Point | Declaration | Code needed | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `commands` | `id`, `title`, `category?`, `when?` | to run them | +| `themes` | `path` (theme JSON), `editor-scheme?` | no | +| `iconThemes` | `id`, `label`, `path` (icon theme JSON, see §5) | no | +| `languages` | `path` (`languages/.toml` format, see §6) | no | +| `runDetectors` | `id`, `name`, `files`, `command`, `args?`, `test?`, `framework?` (API 1.4; §6.2) | no | +| `runConfigurations` | `files`: marker files for the optional `run-configurations` export (API 1.4) | yes | +| `mcpTools` | `name`, `description`, `inputSchema` (package JSON Schema path), `readOnly`, `destructive` (API 1.4; see _Plugin MCP tools_) | yes | +| `snippets` | `language`, `path` (named JSON snippets; API 1.4) | no | +| `providers` | `kinds`, `languages` (editor providers, see _Providers_) | yes | +| `secrets` | `name`, `label`, `description?` (see _Network_) | to use them | +| `menus` | `menu` (`editor.context`, `projectView.context`, `editorTab.context`, `viewItem.context`, `tools`, `commit.message`; API 1.4: `editor.title`, `mainToolbar`, `problems.context`, `searchResults.context`), `view?`, `command`, `when?` | no | +| `keymaps` | `command`, `key`, `mac?` (`Ctrl+Alt+C`; a chord: `Meta+K Meta+C`) | no | +| `statusBar` | `id`, `text`, `tooltip?`, `command?`, `align?`, `priority?` | no | +| `settings` | `schema` (JSON file) or `pages` (API 1.3), `title?` — see _Settings_ | no | +| `views` | `id`, `title`, `icon?` (an icon name or a package SVG), `location` (`left`, `right`, `bottom`, `services`), `web?` (_Web views_), `group?`, `when?` (API 1.3) | without `web` | +| `customEditors` | `id`, `title`, `patterns` (file names), `priority` (`default`, `option`), `layout?`, `web?` | without `web` | +| `fileIcons` | `id`, `patterns` (file names), `icon` (SVG), `iconDark?` — whatever icon theme is in use | no | +| `fileTemplates` | `id`, `name`, `extension`, `path`; API 1.3: `fileName`, `variables = [{ name, prompt }]` — New menu | no | +| `commitChecks` | `id`, `title` — a before-commit check in Commit Options (see _Commits_) | yes | A `viewItem.context` menu needs `view`, the id of one of the plugin's own views. It adds the commands to the right-click menu of the items in that @@ -287,23 +290,27 @@ installing (or when a new version asks for more). Low-risk capabilities are listed; high-risk ones are called out as equivalent to running code on the machine. Users can revoke optional capabilities in Settings › Plugins. -| Capability | Risk | Unlocks today | -| ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- | -| `project.read` | low | `Project::root`, `read_file`, `find_files`, `symbols`, `host::projects`, `publish_diagnostics` | -| `project.write` | medium | `Project::write_file` — creates and replaces text files inside the open projects | -| `fs.read:` | high | `host::fs::read_file`, `read_bytes`, `stat`, `read_dir` — files outside the projects, under the grant's directory | -| `fs.write:` | high | `host::fs::write_file`, `write_bytes` — files under that path; doesn't imply reading | -| `editor.read` | low | `TextEditor::document`, `selections` | -| `editor.write` | medium | `TextEditor::apply_edits` | -| `vcs.read` | low | `Project::vcs_status` (branch, upstream, ahead/behind, changes), `vcs_remotes`, `host::pending_commit`, before-commit checks | -| `vcs.write` | medium | `Project::vcs_fetch` (trusted projects only), `host::set_commit_message` | -| `storage` | low | `host::storage` (64 MiB per plugin, counting UTF-8 bytes of keys and values) | -| `events` | low | file opened/saved, editor focused, repository changed events | -| `events.input` | medium | `action-performed` events (which actions you run, and how) | -| `process:` | high | `Project::run_process`, language servers, debug adapters, runs | -| `net:` | medium | `host::fetch` to that host (`*.example.com`: its subdomains) | -| `secrets` | medium | `{{secret:}}` in request headers, `host::secret_is_set` | -| `clipboard` | medium | `host::copy_to_clipboard` and the clipboard API in the plugin's web views | +| Capability | Risk | Unlocks today | +| ------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `ide.commands` | high | Run any IDE action or other plugins' commands as if you chose them, including running programs, committing and pushing. API 1.4; declared own commands are exempt. | +| `database.sources` | medium | API 1.4: manage only the plugin's own project database sources, write their secure credentials and request connection; trusted projects only. | +| `project.read` | low | `Project::root`, `read_file`, `find_files`, `symbols`, `search_text`, `host::projects`, `publish_diagnostics`, `problems`, editor opening and decorations | +| `project.write` | medium | `Project::write_file` — creates and replaces text files inside the open projects | +| `fs.read:` | high | `host::fs::read_file`, `read_bytes`, `stat`, `read_dir` — files outside the projects, under the grant's directory | +| `fs.write:` | high | `host::fs::write_file`, `write_bytes` — files under that path; doesn't imply reading | +| `editor.read` | low | `TextEditor::document`, `selections`, `host::open_editors` | +| `editor.write` | medium | `TextEditor::apply_edits` | +| `vcs.read` | low | `Project::vcs_status`, `vcs_remotes`, `vcs_log`, `vcs_blame`, `vcs_diff` (new reads require trust), `host::pending_commit`, before-commit checks | +| `vcs.write` | medium | `Project::vcs_fetch` (trusted projects only), `host::set_commit_message` | +| `storage` | low | `host::storage` (64 MiB per plugin, counting UTF-8 bytes of keys and values) | +| `events` | low | file opened/saved, editor focused, repository changed events | +| `events.input` | medium | `action-performed` and `input-mode-changed` events | +| `process:` | high | `Project::run_process`, language servers, debug adapters, runs | +| `net:` | medium | `host::fetch` over HTTPS (also loopback HTTP); `*.example.com` matches subdomains | +| `net.http:` | medium | Unencrypted HTTP to that host; API 1.4, same wildcard rules as `net:` | +| `ipc:` | high | Local Unix sockets or Windows named pipes on the project machine; API 1.4 | +| `secrets` | medium | `{{secret:}}` in request headers, `host::secret_is_set`, `secret_set`, `secret_delete` | +| `clipboard` | medium | `host::copy_to_clipboard` and the clipboard API in the plugin's web views | `process:` capabilities are also derived from what the contributions start: a language's `language-servers` and `debugger` commands and a run detector's @@ -358,6 +365,49 @@ are always available. Calling a gated function without its capability returns ## 3. Writing code (Rust SDK) +### API 1.4 + +API 1.4.0 is planned for the next OpenIDEA release. Use +`engines.openidea = "^1.4"` and the matching SDK. Released 1.3 plugins keep +working. New in 1.4: + +- [Permissions](#capabilities): `net.http:` for unencrypted HTTP, + `ipc:` for project-host sockets/pipes, `ide.commands` for IDE/other + plugin commands, and `database.sources` for owned database connections. +- [Network and secrets](#network-and-secrets): scheme-aware HTTP grants, + socket resources, write-only secret set/delete and desktop OAuth loopback + redirect resources. Settings › Plugins can select/reapprove a process + executable globally or per project. +- [The API](#the-api): window status overrides, `command-runner.execute`, + per-plugin Output channels (`append`, `clear`, `reveal`). +- [Editor and project snapshots](#editor-and-project-snapshots-api-14): open + editors, open/select/reveal, editor and tree decorations, text search, + Problems snapshots, VCS log/blame/diff. +- [Events](#events): file-closed, settings-changed, input-mode-changed and + `vim.mode`; owned run-exited, terminal-exited and content-tab-action events. +- [Runs, terminals and tests](#runs-terminal-sessions-and-test-results-api-14): + `run.start`, keyed terminal sessions/write, `run-configurations` and + `test-results` exports, wasm content tabs and replies. +- [Run targets](#plugin-provided-run-targets-api-14): approved plugin wrappers, + project/PHP/Node defaults, path/cwd mapping and per-command overrides. +- [Language features](#additional-language-features-api-14): formatting, + signature help, highlights, links, code lenses, selection ranges, folds, + colors/presentations, symbols, implementation, type definition and gutter + actions; [injections](#embedded-languages-api-14) route plugin providers to + embedded virtual documents. +- [Snippets](#snippets-api-14) and [menus](#additional-menu-locations-api-14): + numbered placeholders and editor title, main toolbar, Problems and search + result context contributions, with optional command icons. +- [Database sources](#database-sources-api-14): non-secret owned settings, + write-only credentials, connection/introspection and uninstall cleanup. +- [MCP tools](#plugin-mcp-tools-api-14): manifest schemas, optional + `mcp-provider` export, trust/confirmation rules and `mcp.stdio-command`. + +Small runnable API examples: [blame annotations](official/blame-annotations/), +[container wrapper](official/container-runner/), [test runner](official/test-runner/) +and [TODO finder](official/todo-finder/). These demonstrate extension points; +see [official and community plugins](#81-official-and-community-plugins). + `openidea-plugin new` writes this for you. `Cargo.toml`: ```toml @@ -438,45 +488,73 @@ then run `openidea-plugin check`. ### The API -| Rust SDK | WIT | Notes | -| ------------------------------------------------------------------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `Plugin::run_command(id, ctx, arg)` | export `commands.run` | `ctx` is the window: `ctx.project()`, `ctx.editor()`. An error shows _" failed: …"_; the plugin keeps running. `"msg".into()` is `Error::Failed`; `Cancelled` and `Denied` don't count as the plugin failing. | -| `Plugin::handle_events(events, ctx)` | export `event-handler.handle` | Batches of subscribed events, oldest first, per window. | -| `Plugin::activate` / `deactivate` | export `lifecycle` | Per instance: after a crash or idle reclaim a new one is activated. | -| `TextEditor::document()` → `{version, text}` | `editor.text-editor.document` | `selections()` are those when the command ran (0-based lines, UTF-16 columns). | -| `TextEditor::apply_edits(version, edits)` | `editor.text-editor.apply-edits` | One undoable change, only if the document is still at `version` — else `Error::Stale` and nothing changes. `transform_selections` retries once. | -| `Project::root()`, `read_file(path)` | `workspace.project` | Absolute or project-relative; `..`/symlinks escaping the root are rejected; UTF-8 only, ≤ 16 MiB; unsaved editor contents win; a missing path is `Error::NotFound`. | -| `Project::exists(path)`, `stat(path)` | `workspace.project` | `exists` covers files and directories, hidden and ignored included, without reading content; `stat` gives kind, size and modified time (`Error::NotFound` for a missing path). | -| `host::projects()` | `workspace.projects` | Every open project (for scheduled work without a window). | -| `Project::vcs_status()`, `vcs_remotes()`, `vcs_fetch()` | `vcs` | `vcs_remotes` returns `(name, url)` pairs; fetch runs git, so only in trusted projects (call it inside a task). | -| `host::pending_commit()`, `host::set_commit_message(revision, text)` | `commit` | The commit being prepared in the calling window and its message; see _Commits_. | -| `host::subscribe(kind)` | `events.subscribe` | For the session, across idle reclaim (an event activates the plugin). Or declare `on-event:<kind>`. | -| `host::begin_task(title, cancellable)` → `Task` | `tasks.begin` | Shown with the background tasks; `report`, `is_cancelled`; the call may run 60 s instead of 5 s while it is open; dropping it ends it. | -| `host::storage::{get, set, delete, keys}` | `storage` | Survives restarts; the instance's memory does not. Deleted on uninstall. | -| `host::set_status_item(id, item)`, `remove_status_item` | `ui` | `text`, `tooltip`, `command` (one of yours, run on click); at most 8; kept while the plugin is idle. | -| `host::set_context(key, value)`, `remove_context` | `ui.{set,remove}-context` | Sets `plugin.<id>.<key>` for `when` clauses in the window the call came from (`true`, `3.5`, `"text"`; `ContextValue`). Key 1–64 of `A-Za-z0-9_-`, 100 per plugin; gone when the plugin stops. | -| `host::notify(level, title, body)` | `ui.show-notification` | Balloon in the window the command or events came from (every window otherwise). | -| `host::copy_to_clipboard(text)` | `ui.copy-to-clipboard` | Copies `text` to the system clipboard. Needs `clipboard`. | -| `host::open_url(url)` | `ui.open-url` | Opens an `http://` or `https://` URL in the user's default browser. | -| `host::pick(title, items)`, `pick_many` | `ui.pick`, `ui.pick-many` | Picks key(s) from `PickItem`s, or `None` when dismissed. From a command/event or code-action resolution in its editor window (else `invalid`); waiting is excluded from the budget. | -| `host::input(prompt, value)` | `ui.input` | A one-line text box pre-filled with `value`; `None` when dismissed. Same rules as `pick`. | -| `host::input_multiline(prompt, value)` | `ui.input-multiline` | A multi-line text box pre-filled with `value`; `None` when dismissed; Cmd/Ctrl+Enter confirms. Only from a command or event of a window. | -| `host::confirm(title, body)` | `ui.confirm` | OK / Cancel; `true` on OK. Same rules as `pick`. | -| `host::locale()` | `ui.locale` | The IDE's UI language as a BCP 47 tag (`en`, `fr`, `zh-CN`); no capability. See _Translations_. | -| `host::log(level, msg)` | `log` | IDE log, tagged with the plugin id. `eprintln!` output is logged too and shown when the plugin crashes. | -| `Project::find_files(glob, limit)` | `workspace.project.find-files` | Project-relative glob (`**/*Mapper.xml`); honours `.gitignore`; at most `limit` paths. | -| `Project::symbols(query, limit)` | `workspace.symbols` | New in 1.3; `project.read`. Fuzzy in-memory search (no LSP): name, camelCase kind, container and UTF-16 location. Queries ≤ 256 UTF-8 bytes; limits capped at 1000. Snapshot notes below. | -| `host::publish_diagnostics(path, items)` | `diagnostics.publish` | Replaces this plugin's problems for the file (editor squiggles and the Problems view); an empty list clears them. | -| `host::fetch(request)` → `{status, headers, body}` | `net.fetch` | See _Network_. | -| `Project::run_process(program, args, stdin)` → `{code, stdout, stderr}` | `process.run` | Only the pinned program of a `process:` grant; runs in the project root of a trusted project; 4 MiB of output; killed when the call is cancelled or times out. | -| `host::run_command(cmd, project, stdin)` → `{code, stdout, stderr}` | `process.run-command` | API 1.3: same limits as `run_process`, with append-only environment variables; needs the pinned `process:<program>` grant. | -| `host::start(cmd, project)` → `Child` | `process.start` | API 1.3: `poll(max_bytes)`, `write(text)`, `close_stdin()`, `wait(timeout_ms)`, `kill()`; at most 8 handles; dropping one kills its process. | -| `host::refresh_view(view)` | `views.refresh` | The view (or custom editors) render again; call it when the content changed. | -| `host::reveal_view(view)` | `views.reveal` | Brings one of your declared `[[contributes.views]]` to the front in the calling window. | -| `host::post_view_message(view, text)` | `views.post-message` | To the script of the view's `html` body. | -| `host::open_terminal(title, program, args)` | `views.open-terminal` | A terminal tab in the invoking window running the approved `process:<program>` in the project root: output that keeps coming, interactive sessions. | -| `host::open_location(path, line, column)` | `views.open-location` | Opens a file inside the project in an editor of the invoking window, the caret at `line`/`column` (0-based, UTF-16): a view's links to code. Needs `project.read`. | -| `host::setting(key)`, `setting_bool`, `setting_string`, `setting_number` | `settings.get` | The user's value (project over global) or the schema's default. | +| Rust SDK | WIT | Notes | +| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Plugin::run_command(id, ctx, arg)` | export `commands.run` | `ctx` is the window: `ctx.project()`, `ctx.editor()`. An error shows _"<title> failed: …"_; the plugin keeps running. `"msg".into()` is `Error::Failed`; `Cancelled` and `Denied` don't count as the plugin failing. | +| `Plugin::handle_events(events, ctx)` | export `event-handler.handle` | Batches of subscribed events, oldest first, per window. | +| `Plugin::activate` / `deactivate` | export `lifecycle` | Per instance: after a crash or idle reclaim a new one is activated. | +| `TextEditor::document()` → `{version, text}` | `editor.text-editor.document` | `selections()` are those when the command ran (0-based lines, UTF-16 columns). | +| `Project::{upsert_data_source,remove_data_source,data_sources,set_data_source_credential,connect_data_source}` | `database` | API 1.4: owned project sources; requires `database.sources` and trust. See _Database sources_. | +| `host::mcp_stdio_command()` | `mcp.stdio-command` | API 1.4: invoking project bridge argv, `None` while off. No URL/token. | +| `McpProvider::call(tool, arguments, ctx)` | export `mcp-provider.call` | API 1.4: declared tools under plugin grants and command budget. See _Plugin MCP tools_. | +| `host::open_editors()` | `editor.open-editors` | API 1.4: invoking window's path/language/dirty/active snapshots; requires `editor.read`. Commands/events of a window only. | +| `host::open_editor(path, selection)` | `editor.open` | API 1.4: open/select a project file (`project.read`) or plugin-dir file; same path rules as `views.open-location`. Commands/events of a window only. | +| `TextEditor::set_selections(ranges)`, `reveal(range)` | `editor.text-editor` | API 1.4: selections or scrolling in this window's already open editor; no capability. Commands/events only, UTF-16 coordinates. | +| `host::set_decorations`, `clear_decorations` | `decorations.{set,clear}` | API 1.4: project-scoped editor annotations cleared on plugin stop. Requires `project.read`. | +| `host::set_file_decoration`, `clear_file_decorations` | `decorations.{set-file,clear-file}` | API 1.4: project-tree badges/styles. Requires `project.read`. | +| `Project::search_text(query, limit)` | `workspace.project.search-text` | API 1.4: cancellable Find in Files with unsaved documents and project excludes, at most 10,000 matches. Requires `project.read`. | +| `host::problems(path)` | `diagnostics.problems` | API 1.4: project Problems reports grouped by file/source, optional file filter. Requires `project.read` and an invoking window. Empty for remote projects. | +| `Project::vcs_log(path, limit)` | `vcs.log` | API 1.4: newest first, at most 1000 commits, optional file history. Requires `vcs.read` and trust. | +| `Project::vcs_blame(path)` | `vcs.blame` | API 1.4: attribution including unsaved editor text. Requires `vcs.read` and trust. | +| `Project::vcs_diff(path, against)` | `vcs.diff` | API 1.4: disk working tree against HEAD/revision, optionally one file. None outside Git; fails above 4 MiB. Requires `vcs.read` and trust. | +| `TextEditor::apply_edits(version, edits)` | `editor.text-editor.apply-edits` | One undoable change, only if the document is still at `version` — else `Error::Stale` and nothing changes. `transform_selections` retries once. | +| `Project::root()`, `read_file(path)` | `workspace.project` | Absolute or project-relative; `..`/symlinks escaping the root are rejected; UTF-8 only, ≤ 16 MiB; unsaved editor contents win; a missing path is `Error::NotFound`. | +| `Project::exists(path)`, `stat(path)` | `workspace.project` | `exists` covers files and directories, hidden and ignored included, without reading content; `stat` gives kind, size and modified time (`Error::NotFound` for a missing path). | +| `host::projects()` | `workspace.projects` | Every open project (for scheduled work without a window). | +| `Project::vcs_status()`, `vcs_remotes()`, `vcs_fetch()` | `vcs` | `vcs_remotes` returns `(name, url)` pairs; fetch runs git, so only in trusted projects (call it inside a task). | +| `host::pending_commit()`, `host::set_commit_message(revision, text)` | `commit` | The commit being prepared in the calling window and its message; see _Commits_. | +| `host::subscribe(kind)` | `events.subscribe` | For the session, across idle reclaim (an event activates the plugin). Or declare `on-event:<kind>`. | +| `host::begin_task(title, cancellable)` → `Task` | `tasks.begin` | Shown with the background tasks; `report`, `is_cancelled`; the call may run 60 s instead of 5 s while it is open; dropping it ends it. | +| `host::storage::{get, set, delete, keys}` | `storage` | Survives restarts; the instance's memory does not. Deleted on uninstall. | +| `host::set_status_item(id, item)`, `remove_status_item` | `ui` | `text`, `tooltip`, `command` (one of yours, run on click); at most 8; kept while the plugin is idle. | +| `host::set_window_status_item(id, item)`, `remove_window_status_item(id)` | `ui.{set,remove}-window-status-item` | API 1.4: window-only override; removed on window close or plugin stop; removing falls back to the global item. Click runs its command in that window. | +| `host::execute_command(id, arg)` → `Option<String>` | `command-runner.execute` | API 1.4: requires high-risk `ide.commands` for IDE actions and other plugins; this plugin's own commands are exempt. Stable IDE action or `plugin.<plugin-id>.<command-id>` in the invoking window. IDE actions return none; plugin commands return their result. Unknown ids are not-found; at most 8 nested calls, no re-entry into a waiting instance. | +| `host::ipc_connect(name)` → `Connection` | `ipc.connect` | API 1.4: `write(bytes)`, `read(max_bytes, timeout_ms)`, `close()`; requires `ipc:<path>`, at most 8 connections, closed on drop or plugin stop. Runs on the project server in remote projects. | +| `host::secret_set(name, value)`, `secret_delete(name)` | `secrets.{set,delete}` | API 1.4: requires `secrets` and a declared name. Never returns the value. | +| `host::begin_loopback(path)` → `Loopback` | `auth.begin-loopback` | API 1.4: `redirect_uri()`, `wait(timeout_ms)` → raw query or none on timeout. Desktop-only, even for remote projects; no capability. At most 2 listeners; one-shot, closed on timeout, drop or stop. Waiting excludes the call budget. | +| `host::output_append(channel, text)`, `output_clear`, `output_reveal` | `output.{append,clear,reveal}` | API 1.4: read-only Output tabs; 32 named channels per plugin, each a 1 MiB UTF-8 ring buffer. Reveal requires the invoking window. | +| `host::set_context(key, value)`, `remove_context` | `ui.{set,remove}-context` | Sets `plugin.<id>.<key>` for `when` clauses in the window the call came from (`true`, `3.5`, `"text"`; `ContextValue`). Key 1–64 of `A-Za-z0-9_-`, 100 per plugin; gone when the plugin stops. | +| `host::notify(level, title, body)` | `ui.show-notification` | Balloon in the window the command or events came from (every window otherwise). | +| `host::copy_to_clipboard(text)` | `ui.copy-to-clipboard` | Copies `text` to the system clipboard. Needs `clipboard`. | +| `host::open_url(url)` | `ui.open-url` | Opens an `http://` or `https://` URL in the user's default browser. | +| `host::pick(title, items)`, `pick_many` | `ui.pick`, `ui.pick-many` | Picks key(s) from `PickItem`s, or `None` when dismissed. From a command/event or code-action resolution in its editor window (else `invalid`); waiting is excluded from the budget. | +| `host::input(prompt, value)` | `ui.input` | A one-line text box pre-filled with `value`; `None` when dismissed. Same rules as `pick`. | +| `host::input_multiline(prompt, value)` | `ui.input-multiline` | A multi-line text box pre-filled with `value`; `None` when dismissed; Cmd/Ctrl+Enter confirms. Only from a command or event of a window. | +| `host::confirm(title, body)` | `ui.confirm` | OK / Cancel; `true` on OK. Same rules as `pick`. | +| `host::locale()` | `ui.locale` | The IDE's UI language as a BCP 47 tag (`en`, `fr`, `zh-CN`); no capability. See _Translations_. | +| `host::log(level, msg)` | `log` | IDE log, tagged with the plugin id. `eprintln!` output is logged too and shown when the plugin crashes. | +| `Project::find_files(glob, limit)` | `workspace.project.find-files` | Project-relative glob (`**/*Mapper.xml`); honours `.gitignore`; at most `limit` paths. | +| `Project::symbols(query, limit)` | `workspace.symbols` | New in 1.3; `project.read`. Fuzzy in-memory search (no LSP): name, camelCase kind, container and UTF-16 location. Queries ≤ 256 UTF-8 bytes; limits capped at 1000. Snapshot notes below. | +| `host::publish_diagnostics(path, items)` | `diagnostics.publish` | Replaces this plugin's problems for the file (editor squiggles and the Problems view); an empty list clears them. | +| `host::fetch(request)` → `{status, headers, body}` | `net.fetch` | See _Network_. | +| `Project::run_process(program, args, stdin)` → `{code, stdout, stderr}` | `process.run` | Only the pinned program of a `process:` grant; runs in the project root of a trusted project; 4 MiB of output; killed when the call is cancelled or times out. | +| `host::run_command(cmd, project, stdin)` → `{code, stdout, stderr}` | `process.run-command` | API 1.3: same limits as `run_process`, with append-only environment variables; needs the pinned `process:<program>` grant. | +| `host::start(cmd, project)` → `Child` | `process.start` | API 1.3: `poll(max_bytes)`, `write(text)`, `close_stdin()`, `wait(timeout_ms)`, `kill()`; at most 8 handles; dropping one kills its process. | +| `host::refresh_view(view)` | `views.refresh` | The view (or custom editors) render again; call it when the content changed. | +| `host::reveal_view(view)` | `views.reveal` | Brings one of your declared `[[contributes.views]]` to the front in the calling window. | +| `host::post_view_message(view, text)` | `views.post-message` | To the script of the view's `html` body. | +| `host::open_terminal(title, program, args)` | `views.open-terminal` | A terminal tab in the invoking window running the approved `process:<program>` in the project root: output that keeps coming, interactive sessions. | +| `host::register_run_target(project, target)` | `run-targets.register` | API 1.4: project-scoped, read-only argv wrapper; requires its process grant, executable pin and project trust; remote projects run on the server. | +| `host::unregister_run_target(project, id)` | `run-targets.unregister` | Withdraw this plugin's target; remembered prompt answers remain. | +| `host::start_run(config, args, debug)` | `run.start` | API 1.4: start an owned configuration in this window's Run tool window; returns an id, followed by `run-exited`. Approved `process:` program; commands/events only. | +| `host::open_terminal_session(key, title, cmd)` | `views.open-terminal-session` | API 1.4: keyed, pinned terminal in this window, append-only env; returns its operation id and later `terminal-exited`. | +| `host::write_terminal(key, text)` | `views.write-terminal` | API 1.4: write only to this plugin's still-running terminal in this window; trusted project, commands/events only. | +| `host::open_content_tab(tab)` | `views.open-content-tab` | API 1.4: the shared content editor protocol, without a web view; actions arrive as `content-tab-action`. | +| `host::reply_content_tab(request_id, reply)` | `views.reply-content-tab` | API 1.4: reply to an owned action in the originating window. No capability; commands/events only. | +| `RunConfigurations::configurations(root)` | export `run-configurations.configurations` | API 1.4: optional `plugin-run-configurations` world; SDK registration `run_configurations`. Full project list, refreshed on configuration listing. | +| `TestResults::parse(config, chunk)` | export `test-results.parse` | API 1.4: optional `plugin-test-results` world; SDK registration `test_results`. Bounded output batches for `framework = "plugin"`; failures use built-in parsers. | +| `host::open_location(path, line, column)` | `views.open-location` | Opens a file inside the project in an editor of the invoking window, the caret at `line`/`column` (0-based, UTF-16): a view's links to code. Needs `project.read`. | +| `host::setting(key)`, `setting_bool`, `setting_string`, `setting_number` | `settings.get` | The user's value (project over global) or the schema's default. | `Project::symbols` reads the current in-memory index: results may be partial while indexing, and locations can lag unsaved edits. Normal saves and filesystem @@ -505,17 +583,185 @@ plugin ends its children, as does closing the window that started them. `run-command` receives an optional string argument: `executeAction("plugin.<plugin id>.<command id>", "arg")` from another feature, or nothing when run from Search Everywhere / a shortcut. `ctx.selected_paths()` lists the files and folders a Project view or editor tab menu ran the command on (the argument is the first of them); it is empty otherwise. No capability is needed. +### Database sources (API 1.4) + +Declare `database.sources` (medium risk). All five calls require it and a +trusted, open project; it grants no access to user-created or another plugin's +sources. The `database` interface imports `workspace.project` and `types.error`: + +| Rust SDK | WIT | Result | +| --------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------- | +| `Project::upsert_data_source(&PluginDataSource)` | `database.upsert-source(project, source)` | Create/replace an owned source | +| `Project::remove_data_source(id)` | `database.remove-source(project, id)` | Remove an owned source and its credentials | +| `Project::data_sources()` | `database.sources(project)` | Only this plugin's private ids | +| `Project::set_data_source_credential(id, password)` | `database.set-credential(project, id, password)` | Write directly to the Database secure store | +| `Project::connect_data_source(id)` | `database.connect(project, id)` | Ask this project's window to connect/introspect normally | + +`database.data-source` (`PluginDataSource`) has `id`, `name`, `driver`, optional +`host`, `port` (`u16`), `database`, `user`, `url`, and `options` (string pairs). +Driver ids are `sqlite`, `postgres`, `mysql` (MariaDB too), `sqlServer`, +`clickhouse`, `oracle`. For SQLite, `database` is the file path (absolute or +relative to the project), or use a `sqlite:` URL. Other options are the +Database Advanced tab's non-secret driver properties. A URL selects URL-only +connection settings; its driver must agree. URLs containing passwords or +credential query parameters, and password/token/secret options, are rejected. +Never put credentials in any settings field: use `set-credential` instead. +There is no credential read function, including for credentials just written. + +```rust +let project = ctx.project().ok_or("Open a project first")?; +project.upsert_data_source(&openidea_plugin::PluginDataSource { + id: "local".into(), name: "Local service".into(), driver: "postgres".into(), + host: Some("127.0.0.1".into()), port: Some(5432), + database: Some("app".into()), user: Some("app".into()), + ..Default::default() +})?; +// If needed: project.set_data_source_credential("local", password)?; +project.connect_data_source("local")?; +``` + +Ids are plugin-private, 1–128 bytes; the host allocates the actual connection +id. At most 64 sources per plugin/project; names at most 256 bytes, 128 option +pairs, each value/optional connection field at most 16 KiB. Credential input is +at most 64 KiB. Unknown/unowned ids return `not-found` without revealing whether +another owner's source exists. The host checks stored ownership even when a +user source happens to use the generated id format. + +Owned sources persist in `.openidea/data-sources.toml` with `pluginOwner` and +`pluginSourceId`; updates preserve unrelated entries, comments and formatting. +Plugin edits require the normal `[[data-source]]` table format; inline table arrays +are rejected. No password is written there. The Database tree refreshes on source changes +and labels owned sources with their plugin id. Owned sources stay project-scoped; +duplicating one creates a user-owned copy. Disabling retains them; +uninstall removes them and their credentials, including in closed projects +remembered by the host. A remote disable also retains its sources; a later +uninstall is applied when that server synchronizes with the desktop. + +`connect` is valid only from a command/event of that project's window and +uses the usual Database connection, password prompts and introspection. Waiting +for this window is excluded from the plugin call budget (like `ui.pick`), with +a 180-second window-response timeout and cooperative cancellation. +Source persistence and credential storage run where the Database subsystem +runs: on the project server for remote projects. A host without that subsystem +returns `invalid("Database subsystem is unavailable on this host")`. + +### Plugin MCP tools (API 1.4) + +Tools extend the IDE's built-in MCP server. Declare a self-contained JSON Schema +file in the package (local `$ref` fragments are supported; network/filesystem +references are refused): + +```toml +[[contributes.mcpTools]] +name = "find_notes" +description = "Find notes in this project" +inputSchema = "find-notes.schema.json" +readOnly = true +destructive = false +``` + +Names match `[a-z0-9_]{1,40}`. The registered name is +`<plugin-id with every non [a-z0-9_] character replaced by _>__<name>`; the +complete name must fit 64 bytes or installation/check fails. Descriptions +(1–4096 bytes) identify the originating plugin in the server listing. Schema +files are at most 256 KiB, validated at install and `openidea-plugin check`, +and must describe an object. Inputs are checked against that schema at call +time (at most 1 MiB of JSON); text results are at most 4 MiB. If two live +plugins normalize to the same tool name, both ambiguous tools are withheld. + +Implement the new optional `mcp-provider.call(tool, arguments, ctx)` export +(`tool` is the local name, `arguments` JSON text, `ctx` a borrowed invocation). +The result is text, or JSON encoded as text if the tool wants structure. +`plugin-mcp` includes the base plugin world; export-only `mcp-tools` composes +with it. In Rust: + +```rust +impl openidea_plugin::McpProvider for MyPlugin { + fn call(&mut self, tool: &str, arguments: &str, ctx: &Invocation) + -> Result<String, Error> + { + // Validate/interpret this tool's arguments, then call ordinary host APIs. + Ok(format!("{tool}: {arguments}")) + } +} +register_plugin!(MyPlugin, mcp); +``` + +Enabled tools are listed on each request, so disabling/uninstalling withdraws +them immediately. They execute in the owning plugin's actor with its own +grants, fuel and command wall-clock budget; an MCP client grants no additional +permissions. `readOnly = false` tools are hidden and uncallable in Restricted +Mode. Every `destructive = true` call asks the project's window for confirmation +through the same UI as built-in run tools; rejection, cancellation or a missing +window fails closed. A tool has the project's invocation context, but is not a +user command/event: APIs explicitly reserved to commands/events remain invalid. +The built-in server currently serves local projects; remote project tool calls +retain its documented “not supported for remote projects yet” response. + +The new host interface `mcp` has `stdio-command() -> option<list<string>>` +(`host::mcp_stdio_command()`). It returns only bridge argv for the invoking +project, such as `["/path/openidea", "--mcp-stdio", "--project", "/project"]`; +never the HTTP URL or bearer token. It returns `none` while the server is off, +without a project window, or on a remote host without its own running server. +It needs no additional capability. + +### Editor and project snapshots (API 1.4) + +Editor lists come from the invoking window's existing workbench reports; language +and dirty state come from the open documents. Selection and reveal operations +require an editor belonging to that window, even when another window has the same +project. Ranges use zero-based lines and UTF-16 columns. Selection changes accept +1–1000 ranges and do not change document text. Opening a file acknowledges a UI +request; the usual editor handles the actual open. + +`Project::search_text` takes `TextQuery { pattern, regex, case_sensitive, +whole_word, include, exclude }`. Globs follow Find in Files' masks. It uses the +same search engine and unsaved-document overlay as the built-in MCP tools, with +the project's exclusions; only files inside this project's root are returned. +Results contain `{ location, line }` and `limit_hit`; limits are capped at 10,000, +zero returns no matches. Line previews are capped at 4096 UTF-8 bytes and the +result budget is 4 MiB (`limit_hit` also covers that cap). Closed-file text is +read with the editor's encoding defaults, at most 16 MiB per file; a matched file +that cannot be materialized also sets `limit_hit`. Cancellation or the +call deadline interrupts the search. `host::problems` reuses the Problems view's +reports, including LSP and plugin diagnostics; a remote project currently returns +an empty list because it does not forward Problems reports. + +Decorations are plain text, with fixed styles mapped to theme tokens: +`highlight`, `search`, `added`, `modified`, `removed`, `warning`, `error`, `info`, +`underline`, `strikethrough`, `muted`. Unknown styles are ignored. Editor items +have `{ range, style, hover, after }`; tree items have `{ badge, style, tooltip }`, +where a badge has at most two Unicode characters. Paths must resolve inside an +open project; absolute paths select that project, relative paths use the invoking +project. Sets are isolated by plugin and project and shared by all windows of +that project. Setting replaces a set's items for one file; clearing removes that +set in all projects. Each plugin has at most 50 editor sets and 50 tree sets, +10,000 editor items per file across its sets, and 4 MiB total metadata. Disabling, +unloading or crashing the plugin withdraws them; idle reclamation preserves them. + +The new Git reads run on the project machine, require a trusted open project and +`vcs.read`, and disable repository-configured helpers, textconv and external diff +programs. `vcs_log` returns commit ids, author/email, Unix seconds, subject/body +and parents. `vcs_blame` uses the IDE annotate backend: unsaved changed/inserted +lines are uncommitted; `line` is zero-based and dates are Unix seconds. +`vcs_diff` uses disk text (including staged and unstaged changes) against HEAD or +`against`, and stops with an error if output exceeds 4 MiB. Without a repository, +log/blame return empty lists and diff returns `None`. + ### Events -| Kind | Capability | Delivered | -| ------------------ | -------------- | ------------------------------------------------------------------------- | -| `file-opened` | `events` | a file opens in an editor | -| `files-changed` | `events` | debounced created, modified and removed paths in open projects (API 1.3) | -| `file-saved` | `events` | after the file is written | -| `editor-focused` | `events` | the focused editor changes (path, or none) | -| `vcs-changed` | `events` | a repository's state changed (commit, checkout, fetch…; its root) | -| `action-performed` | `events.input` | an IDE action ran: id, title, source (keyboard, menu, toolbar…), shortcut | -| `scheduled` | — | the `on-schedule` period elapsed | +| Kind | Capability | Delivered | +| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `file-opened` | `events` | a file opens in an editor | +| `file-closed` | `events` | API 1.4, `Event::Other`: JSON path string | +| `settings-changed` | `events` | API 1.4, `Event::Other`: JSON array of this plugin's changed setting keys (effective values) | +| `input-mode-changed` | `events.input` | API 1.4, `Event::Other`: JSON `{path, mode, sub}` on focused Vim mode changes; `ctx.editor()` includes the document and current selections | +| `files-changed` | `events` | debounced created, modified and removed paths in open projects (API 1.3) | +| `file-saved` | `events` | after the file is written | +| `editor-focused` | `events` | the focused editor changes (path, or none) | +| `vcs-changed` | `events` | a repository's state changed (commit, checkout, fetch…; its root) | +| `action-performed` | `events.input` | an IDE action ran: id, title, source (keyboard, menu, toolbar…), shortcut | +| `scheduled` | — | the `on-schedule` period elapsed | Events are batched: a running plugin gets them within a fraction of a second, an idle one collects them for 2 s before it is woken, so frequent events @@ -631,8 +877,7 @@ The referenced icon path is package-relative, up to 256 bytes; both SVGs use the references are rejected during installation. Missing or unknown icons retain the navigation glyph. A marker's optional target still navigates on click. For several cross-file targets, publish document-index mappings; the existing -chooser lists every matching definition. Command clicks are deferred; use a -code action on the same line. +chooser lists every matching definition. Command clicks are available through the API 1.4 `gutter-actions` kind. Document-indexer markers (`Edges.markers`) use a declared icon when their key starts with one of its `markers` prefixes; the longest matching prefix wins, @@ -805,12 +1050,139 @@ shows it. The host asks the language server and the plugins together, under the edit-path budget, and shows both; the editor asks again after edits and scrolling. Labels are cut at 256 bytes and tooltips at 1 KiB. +#### Additional language features (API 1.4) + +Declare the following kinds under `[[contributes.providers]]`, with +`engines.openidea = "^1.4"`, implement `language_features::LanguageFeatures`, +and register `register_plugin!(MyPlugin, language_features)`: + +| Kind | SDK method | Host policy | +| --------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `formatting` | `format(doc, range, options)` | Settings › Languages & Frameworks › Plugin Formatters selects a plugin per language; it takes precedence over automatic formatters. Explicit external-formatter actions still select that tool. Format Document, Selection and format-on-save share this choice. Uses the 5-second command budget. | +| `signature-help` | `signature_help(doc, at)` | Used when LSP returns no signatures. Parameter labels accept literal text or UTF-16 offsets within the signature label. | +| `document-highlights` | `document_highlights(doc, at)` | Appended to LSP highlights; the LSP wins duplicate ranges. Kinds are `text`, `read`, `write`. | +| `document-links` | `document_links(doc)` | Appended to LSP links, in the same Ctrl/⌘-click UI. Targets are absolute paths, paths relative to the project root, or HTTP(S) URLs. Other URI schemes are ignored. | +| `code-lenses` | `code_lenses(doc)` | Rendered beside LSP and built-in code-vision lenses. Clicks invoke only the returning plugin's declared command. | +| `selection-ranges` | `selection_ranges(doc, at)` | Extend Selection prefers valid plugin ranges, innermost to outermost, then falls back to tree-sitter. | +| `folding` | `folding(doc)` | Merged with tree-sitter folds, deduplicated by start/end lines. Applies to any declared language; `syntax-provider.folding` remains available for a plugin's own languages. | +| `colors` | `colors(doc)`, `color_presentations(doc, color)` | Color Preview shows swatches and the native picker. A presentation replaces the original range; multiple presentations open a chooser. Channels are finite floats in 0–1. | +| `document-symbols` | `document_symbols(doc)` | Structure and breadcrumbs use these only when LSP returns no document symbols (including an empty list); otherwise LSP symbols win. Tree-sitter is the final fallback. | +| `implementation` | `implementation(doc, at)` | Merged with LSP Go to Implementation, deduplicated by location. | +| `type-definition` | `type_definition(doc, at)` | Merged with LSP Go to Type Declaration, deduplicated by location. | +| `gutter-actions` | `gutter_actions(doc)` | Buttons beside line markers. Clicks invoke only the returning plugin's declared command, with its optional string argument and the editor window/project context. Icons accept the same names, aliases and declared marker SVG paths as line markers. | + +The optional WIT interface is `language-features`, in worlds +`plugin-language-features` / `editor-language-features`. Its own `document` +record contains path, language, version and unsaved text. All functions are +present in the interface from its first release, but the host calls only +kinds declared by the plugin. No additional capability is required for the +supplied document; host imports retain their normal permission checks. +Except formatting, these calls use the editor-provider soft/hard budgets. +Missing/unavailable LSP services permit plugin results; other LSP errors +remain errors. + +`document-symbol.parent` is an index in the returned flat list. A parent +must precede its children and contain their ranges; invalid parents become +roots, and outline depth is bounded to 64. Symbol/fold/highlight kinds are +strings so later host versions can recognize new ones. Unknown kinds use +neutral UI defaults. Invalid document ranges are dropped. Each list is +limited to 10,000 items; signature help retains at most 100 signatures and +100 parameters each, presentations at most 100 strings. Text is bounded to +64 KiB per string. Format edits must be valid and non-overlapping. +Disabling a selected formatter reports it as unavailable rather than silently +formatting with another provider. Format-on-save uses the editor's existing +Format on save setting. Native tests can obtain the current unsaved snapshot +with `MockHost::provider_document()` and call the traits directly. + +#### Embedded languages (API 1.4) + +Implement `language_features::Injections`, declare `kinds = ["injections"]` +for the host language, and register `register_plugin!(MyPlugin, injections)`. +The optional `injections` interface has worlds `plugin-injections` / +`language-injections` and returns `{range, language}` entries. Ranges use +host-document UTF-16 coordinates. The editor refreshes them after edits +(300 ms debounce) and highlights each slice using the embedded language's +CodeMirror parser and current color scheme, over the host tokens. + +Completion, hover, diagnostics and additional language features use embedded +languages' **plugin** providers. A virtual document keeps the original path, +version and newlines, replaces text outside all ranges of that language with +spaces, and changes the language id. One space is emitted per UTF-16 unit, +so emoji outside a range do not shift its positions. Point requests ask the +embedded language at the caret before the host language. Document features +filter results to their injected ranges, and diagnostics outside those +ranges are dropped. Completion replacement/additional edits are confined to +the embedded ranges. Ranges are half-open; the end belongs to the host. + +At most 1,000 non-overlapping injection ranges and 32 embedded languages +are routed per document. Earlier ranges in document order win overlaps. +Documents over 2 MiB are excluded. Invalid ranges, self-injections and invalid +language ids are ignored. Injections are not applied recursively. The same +server-side plugin host handles remote editing. **Embedded LSP forwarding is +future host-side work**, not part of 1.4, and needs no WIT change. + +### Snippets (API 1.4) + +Contribution-only or code plugins can package read-only live templates: + +```toml +[[contributes.snippets]] +language = "sql" +path = "snippets/sql.json" +``` + +```json +{ + "Select rows": { + "prefix": ["sel", "select"], + "body": ["SELECT ${1:column}", "FROM ${2:table}$0"], + "description": "Select rows from a table" + } +} +``` + +`prefix` and `body` each accept a string or an array of strings (body lines +join with newlines). Numbered `$1`, `${1:default}` and final `$0` placeholders +use the existing linked live-template fields, in numeric tab order; `\$` +escapes a literal dollar. They appear in completion, Tab expansion and +Settings › Editor › Live Templates, grouped by the plugin name and read-only. +Disabling or removing the plugin removes them. Plugin locales translate +`description`; prefixes and bodies remain code. Installation and +`openidea-plugin check` validate every entry and package path. Limits: 1 MiB +per file, 1,000 named entries, 32 prefixes per entry, 64 KiB per body, 4 KiB +per description. No additional permission or runtime export is needed. + +### Additional menu locations (API 1.4) + +`[[contributes.menus]]` also accepts `editor.title` (the editor tab bar's +actions area), `mainToolbar`, `problems.context` and `searchResults.context`. +The existing `when` clauses apply alongside command enablement. Toolbar +icons use the command's optional API-1.4 `icon`, then the plugin manifest +icon. A command icon may be a built-in icon name or a package SVG path, +validated and bundled at installation. Context menus also support the +existing submenu grouping. + +Commands receive the usual invoking window/project/editor context. For a +problem or search match, `selected-paths` contains its file and the command's +string argument is JSON `{ "path": "…", "line": 0, "column": 0 }`; lines +and columns are zero-based and columns are UTF-16 units. Editor-title and +main-toolbar actions use the focused editor context. + ### Network and secrets -`host::fetch` makes an HTTPS request (plain HTTP only to `localhost`) to a host -the plugin was granted with `net:<host>`; redirects to another host are -refused and responses are limited to 8 MiB. Requests are made from the -machine the project is on. +`host::fetch` requires `net:<host>` for HTTPS and loopback HTTP. API 1.4 adds +`net.http:<host>` for unencrypted HTTP to other hosts; it does not imply an +HTTPS grant. Both use the same wildcard rules. Each redirect is checked before +connecting to the destination; HTTPS → HTTP also requires `net.http:<host>`, +even on loopback. Responses are limited to 8 MiB. Requests run on the project +machine. A manifest using `net.http:` or `ipc:` declares `engines.openidea = "^1.4"`. + +`ipc:<path>` grants a local byte stream: an absolute Unix socket path (the host +expands `~/`, `$TMPDIR/` and `$XDG_RUNTIME_DIR/`) or `\\.\pipe\<name>` on Windows. +Only a grant's last path segment may contain `*`, for example +`ipc:$TMPDIR/discord-ipc-*`. It never grants TCP. A remote project's connection +runs on the server. Reads and writes honor cancellation and the call budget; +reads return empty bytes on timeout or EOF, and each operation is capped at 1 MiB. Secrets declared under `[[contributes.secrets]]` are entered by the user in Settings › Plugins and kept in the system keychain. The plugin never reads @@ -818,6 +1190,27 @@ them: it writes `{{secret:<name>}}` in a request header and the host fills it in on the way out (`secrets` capability). `host::secret_is_set(name)` tells whether the user has entered one. +API 1.4 can also store or delete declared secrets with `host::secret_set` and +`host::secret_delete`; the plugin still cannot read them. An OAuth flow uses +`begin_loopback("oauth/callback")`, `ui.open-url` with its `redirect_uri()`, +`wait`, an authorized `net.fetch` code exchange and `secrets.set` for the token. +The listener binds only `127.0.0.1` on a random port on the **desktop**, including +for remote projects, serves "You can close this tab", then closes. There are +no OS URI callbacks or `on-uri` activation. Validate OAuth state in the plugin. + +Window status overrides, command execution, loopback creation/waiting and +output reveal are valid only in a command or event from a window. The Vim +context key `vim.mode` is `normal`, `insert`, `visual` or `replace`, and empty +when Vim is disabled. Subscribing to the three new event kinds, including +`on-event:<kind>` activation, requires `engines.openidea = "^1.4"`. + +Settings › Plugins lets the user choose the executable of a `process:<name>` +grant globally or for the current project. The native chooser's selection is +approved again and pinned by path and content hash. Project choices override +the global choice for runs, process handles, terminals and log panes. Resetting +a choice verifies its fallback, or revokes the grant if that is unavailable. +The plugin continues to call its declared program name. + ### Views and custom editors A view is data: a toolbar, a body — `Tree`, `List`, `Form`, `Table`, @@ -1197,6 +1590,84 @@ also ask the sandboxed page to open a tab, exercising the Wasm-to-page bridge; `Log Probe: Open From Code` opens one from the plugin's code. This extends the web bridge without changing released WIT signatures. +### Runs, terminal sessions and test results (API 1.4) + +`host::start_run("tests", &["--verbose".into()], false)` runs this plugin's +`runDetectors` entry or a configuration returned by its optional export, in the +invoking window's normal Run tool window. Both the short id and the full +`plugin:<plugin>:<config>` id work. Appended arguments follow the configuration's +arguments. The id is allocated before asynchronous preparation (including before-launch +builds); a later startup failure still produces its completion with a null status. +The IDE owns Stop, Rerun and target handling, and verifies the approved +program at launch and on rerun. Debug uses the debugger of the source file named in the configuration's arguments +(or the debugger explicitly chosen on its saved configuration). Without one it +returns `invalid`. Local Debug and Rerun recheck the owned program pin; debugging +these configurations on a run target is currently invalid. Starting from activation or a provider is invalid. + +An optional configuration provider declares marker files: + +```toml +engines.openidea = "^1.4" +[[contributes.runConfigurations]] +files = ["package.json"] +``` + +Implement `RunConfigurations` and register `run_configurations`. Each returned +`PluginRunConfiguration` has `id`, `name`, `program` (a declared and granted +`process:` name), `args`, optional project-contained `cwd`, `test` and optional +`framework`. The host bounds the list to 1000 entries / 4 MiB and keeps the last +valid list after an export failure. Entries without an approved program are not +offered. Configuration exports run without a window or prompt context. + +For a static detector, `framework = "plugin"` is an API 1.4 manifest field. For +either kind of configuration, it enables the optional `TestResults` export +(registration `test_results`). `parse(config, chunk)` receives the short config +id and sequential chunks of output, not necessarily complete lines. The plugin +retains any partial line between calls. Test ids are scoped to a run; a suite or +test's parent must already exist. Locations use absolute project-contained paths, +zero-based lines and UTF-16 columns. Passed/failed durations are milliseconds. +The host uses the editor-provider soft/hard budget per batch and bounds returned +events to 10,000 / 4 MiB. A missing, failed or invalid parser switches that run to +the built-in parsers; common framework commands retain their existing rerun-failed +filters. Arbitrary frameworks have no failed-test selection protocol in this +export and therefore cannot supply a custom rerun-failed filter. + +`host::open_terminal_session(key, title, &Command::new("tool"))` opens a normal +Terminal tab in this window's project root. Reopening the same key focuses its +running tab and returns the same id; after exit a new call starts a new operation. +The host rechecks the program pin and trust at PTY spawn. Environment entries +have the same append-only validation and collision errors as `run-command`. +`write_terminal(key, text)` requires the owned live session and project trust, +like writing to a process child. Closing the tab stops the operation. Idle wasm +reclamation retains running sessions; disabling the plugin or revoking its grant +ends its PTYs. `views.open-terminal` keeps its released behavior. + +Completions go only to the owning plugin, in its launching window, even without +a subscription and even when its wasm instance was reclaimed. A terminal emits +exactly one completion from its PTY exit. The following kinds are also subscribable +and accepted in `on-event:<kind>` activation declarations with API 1.4: + +| Kind (`event::other`) | JSON data | +| --------------------- | ----------------------------------- | +| `run-exited` | `{id, config, status, killed}` | +| `terminal-exited` | `{id, key, status, closed}` | +| `content-tab-action` | `{requestId, key, action, content}` | + +`status` is a signed exit code or null (for example a signal); `killed` indicates +an IDE Stop, and `closed` indicates the terminal tab was closed before process +exit. Use the SDK's `Event::run_exited`, `terminal_exited` and +`content_tab_action` accessors to decode these extensions. No `events` capability +is needed to receive the completion or content action of an operation you own. +Remote plugins and their programs run on the server; the UI requests and replies +are forwarded to the originating desktop window. + +`host::open_content_tab(&ContentTab { .. })` reuses the web content editor below, +with the same content, action, reply and diff behavior. Its key is scoped to the +plugin and window, independently of web views. Save and toolbar actions arrive +as `content-tab-action`; answer with `host::reply_content_tab(request_id, reply)`. +The host accepts replies only from that plugin and window. Content and diff text +are limited to 1 Mi UTF-16 units, with at most 16 actions and one Save action. + ### Plugin-backed content editor tabs (API 1.3) A web view can open an editor tab on content the plugin supplies, with @@ -1724,6 +2195,11 @@ It lists two kinds of plugins: and publishes its index, which `plugins-index.yml` merges into the release's within the hour. Its README has the rules. +The four `blame-annotations`, `container-runner`, `test-runner` and `todo-finder` +packages are small API 1.4 examples, with native tests and checked-in components, +rather than feature plugins. Their fake executables and protocols are for adapting +the examples to a real tool. + Pull requests that add a plugin to this repository are for official plugins. A plugin that needs a new extension point brings the extension point here, as its own pull request, and the plugin to the registry. @@ -1753,7 +2229,8 @@ the API itself follows _Evolving the plugin API_ below. ## 10. Evolving the plugin API Released versions are frozen: 1.0.0 shipped with OpenIDEA 0.1.36, 1.1.0 -with 0.1.37 and 1.2.0 with 0.1.38. wasmtime decides what stays compatible: it resolves interface names +with 0.1.37, 1.2.0 with 0.1.38 and 1.3.0 with 0.1.39. API 1.4.0 is planned +with the next release. wasmtime decides what stays compatible: it resolves interface names by semver (a component importing `openidea:plugin/x@1.0.0` links against a host defining `@1.1.0`, and the host finds its exports the same way), but function types must match exactly — a record with another field count, or a @@ -1879,3 +2356,57 @@ one dismisses the balloon and runs the command in the receiving window with its normal editor and project context. No capability is needed. Invalid or undeclared actions reject the whole notification. Labels are limited to 256 bytes and arguments to 64 KiB. Plain `host::notify` behaves as before. + +### Plugin-provided run targets (API 1.4) + +`host::register_run_target(project, target)` registers or replaces a read-only, +project-scoped target owned by the plugin. `host::unregister_run_target(project, +id)` removes it. `PluginRunTarget` has `id`, `name`, a granted `process:` `program`, +`args`, `workdir_args`, optional `container_root`, and `commands` (pairs of tool +names and replacement argv). Targets appear in Settings › Run Targets and every +run configuration's Run on selector, labelled with their plugin. They survive +idle instance reclamation; stopping, disabling, reloading or uninstalling a plugin +withdraws them. Registrations and execution for remote projects are on the server. +A missing target fails the run rather than silently using the local tool. + +For each launch the host checks project trust and the wrapper's current process +grant and executable pin, including per-project executable choices. Debugging on +plugin targets returns `invalid`; language servers are not run in these targets. +There are at most 32 targets per plugin/project, 64 unique command overrides and +256 arguments per prefix (16 KiB each, 256 KiB metadata per registration). + +The host constructs argv directly, without a shell: + +- No override: `program + args + workdir_args + command + command arguments`. +- Override matching `wrap::program_name(command)` (basename without `.exe`, `.cmd` + or `.bat`): `program + override + workdir_args + command arguments`. The override + replaces **both** the prefix and the command name and includes the tool itself. + +DDEV, for example: `program = "ddev"`, `args = ["exec"]`, +`commands = [("composer", ["composer"])]` runs `php x.php` as +`ddev exec php x.php` and `composer install` as `ddev composer install`. + +Local absolute tool paths outside the project use the tool basename. +When `container_root` is set, project paths in argv map under that root. Each +`workdir_args` element may contain `{cwd}`; for example `["--dir", "{cwd}"]`. +The host substitutes the configuration's working directory mapped under +`container_root`. It omits **all** workdir arguments when there is no container +root or the working directory is outside the project. Without `workdir_args`, +the wrapper manages its container directory itself. The wrapper process always +starts in the configuration's **local** working directory, so tools such as DDEV +can find their project. Configuration env and env files reach the local wrapper; +no generic env-forwarding flags are added. Container env is forwarded only if +the wrapper's own conventions allow it. + +Registration never changes a selection. The first registration prompts the +project's windows: “Use <name> for running PHP / Node / Composer in this project?” +with **Use** / **Not now**. The first answer is remembered on the host per +project + plugin + target id. Use sets the project's default run target and its +PHP and Node run-target choices; Not now leaves settings alone. Interpreters +also have a Run in target choice in Settings (Default run target, Local machine, +or a named target). The `languages.php.runTarget` / `languages.node.runTarget` +settings store a selector id, empty for the default, or `local` for local execution. Code-created PHP/Node runs, +Composer/npm scripts, PHPUnit and Node tests use that choice through the normal +Run flow. Explicit per-configuration targets take precedence. Choosing Local +machine explicitly overrides defaults. Language servers, tool discovery/version +probes and external analysis helpers remain local to the host. diff --git a/plugins/official/ai-completion/dist/plugin.wasm b/plugins/official/ai-completion/dist/plugin.wasm index 90c83adc3e9a46bc43598e7aa9bff108354b6591..f0ecc3db1ac06648fe86c09013cf9718d94e8ca8 100644 GIT binary patch delta 5743 zcmc&&dsLK1mhXG3yG5D?ROC(QZqNXVf+$h&)hf7g*7cDr$tF&GGCtxM59&@#qJnV@ z8jNVG<ukz<9U4K?q=CX0AqGdqL@}a92O}DziS|aK%*dQEW^Yw@BSz2JKlk)G_4{ts zy|?bGzOVY8TWQ?*p3zYW(BD=C*)&FCK8?F^J&gld8jTCsLmG3m-_n?+8w&lM8+Bv! zQ15Iuya23!pCq2_!y;@GjFH4J&uF1>o$(nO9mYf&zcKdcOiXNM_Cif3TateS&Bgi0 zkeYw@FV|Z(9R5sWSf;Vod9cxg`SrDGKL4;Sx_kC-TRqb~CGP_dQB>oW4Um(YJ;ZlS zZoyu1vY^V%QQ=wK?!n$l!s%XYwq5N$nXIw~C()P_Jb*@f@E{tm1wTh)c!(S)hR_V} zlEi{-e+r2<kV`Q+i{@$aA%Vk4W+ION35lmMB(xWeBSZVrxFA$UV5b^ShxVsuZ5YkF zwTuaqLa&FV(s(+wyHkYm53r)xIo{q43=jVDS*~V|sgrd@v29+-{Kx1YO2&1hog??m z?uHwRZR71`=jp>60e88d-}`hvg9Em<Bd<Y~?WNC$+U_3-bngEA2tb`}*3mK0=yV>P zuZQc-*e^yRwAyB!dJb3EojXrW55etaPW_F$%-C2W?FhWPA~#o~;p<+7U|YnkTx_)4 z0&aylt8R_blKj7Z90%BH=Xn{}%{IBkh!rKyc`a#-_9?m7gcW<ZIi6Owwi=<%X>aXC z7VdGL`}=5Sv)}V~Cj4`$0gm$$EewVRaYGAH#>mD}we44GTZ75Rq%~<{mgkZY*2d!K zu7J=eOhM3<%;qV<P>(D2h+l#sKRRlIT}d{1B()};N$-)Yv-3|=Aq;nUu);1brNXVw z5gXi2dS%em$s@6=L|hpTJ;>e?p&tQl3@b`EWkLjQDCJQSca`$AOz43tO8L}G*hkg` zj)q7+FcSiJ`e^XSvNG|T(J&_fu8XQH_*P4HZ+#UabWq2urofAEog1dYzZo7Ve@#@d zW#YA|u!N#2nzCU$Kr>IA384g;Ghqxgi*++$o-bVAlmijO6PW|MbkHI``vc?vwDA6m z;R9&kpD%_f*t(am(P9K2{uYE0%PVieDTG=czl7r4B*DzzTLKrbaG#j66dXw6UGiXZ z?23<71pn@ipgR&4opr~RTB-!=ml~7K`hai9gXvgU!~dBF{U!EY4vVAgHBvI3Y0O$S z+@u|`Y=RLzk+j!{qszfSrN_^_3*)h3uZURz&jW7W$LIYKuA-wxOjrpMD6#y{`S3QD z*KpG+$ixjF^I5CFBI$xvun}C`vKq!<ehr_u8qx#u)s|LB(`YDq-}CC#5D#DTYpbD8 zk6edxguhE=V`5f~<Uwmk2_%UD@54iE+RIHPFjAWJRso!6GAI)ZVKmVG#oN{abZOfs zm1P<=9*5dQ*?P(gwDF@hI1X*%gAK6rDQtZBCYXiQ4!&m-q+`J+{N^TjJ*MTb)Rw5x zc=cJp>Yg(xGr{T-i#YsQ=09u;r1;h<Urb<a5LsIwA4sEU-U`c}w&VQmFh}m#v>n#t zu8;Zr9gq`Q;m+!r_7lZS%;QQOf4Bp}DKlXo!a~d~=W9QNnG)MRgx_MpUjEWY@Z*yK z;8{gb@bu{+B55a`mWaY~=_bWK&s{v7DgJ(vE-DuzcSBD^`#v6N2NQq47?ROZ&TEQc zl4o+Cq`$DkM4nOr8I&4MsTnMGmOv)p_PyeVJ@C4W1J5d>gHk7o%jkemW#8QkD@cWS zb03V-;$|nW`2@ycb&UvcLJ$?4llQ2gRBU(hX%(<A=#Z+Q4ymvns^PaPXp>r=TuG;_ zmXEE3bb?iGI9ds2qHb2gtfvc4uYzP^*jNRzP%92p!4^FhIr)DchF2t(ABHSK(YXee z>$^9o?c*@i0^VAn92eEW4oq~luc1+c!@cP`B7+)-8VmDEShUtUKIa(i=i+OR!Ayet z$6zc$#&KBd=hkD<nIv2Dad?Rq2A+Uaf;UdU0D|2oU;@GY6A(=hQV&B3vg+X#g7SJu zA^5JIj)RLQo`k4iuVGq?Z-x;(cDclqli&x$^ZPI8&F11qzJR|Htojn>5VU>?ABB1a zeaIO0yw`lIi<h2)xx^HD8hR28I}Hg0b529QpgOO+8SU(K+;JL$sbD=WyH3O8Ft6%r zX3-LBd$?SDMg#OCZM*?Y1l0|)T6pI(lwlVie}-zp#k0@AN}6<?fp>_S{S{0hsQ(HQ zqTFgt`c$l71|07Gc|GP`&MNVqXJHTtOgRgYp4gZ=rDCAL&1cbDU3~pndNI}ULuct_ z?BeIoLXkHuT9d(A$Co!k7A;(CRBY@VB=_-p6gaNkbB{Gjxv7)MFvskDAG!Fnb1;N7 z6rF<)9;x;v)oJIUj23R5S78Xh0DmBA{RJ3EaPk72YR`(FMQdVXA8p3VzlQgSY0yRJ zOECK)TqGEIN$L6TOR$8f)=Q8`VEG$d2G^!0#hBCtjWnmbtO{q)Wf)G>TbJp)cwGt| z-C>u15MTRBP{)-a-_U2x#U0;3J;B>ol=jjqaEPc`&B}~z&B~0htI!`@;y<oJl&rwH z*C2+V@S2jSy#_~#TKO#u_3=ay`1~6XN0VD_KoswK9SYhlc+9p>uB&76!*wvVSACF0 z-_gkE?_eq^%KHv(63o7#wj{ONqCI<QQqd=XYKJ~hzrb%gIR3&-<-*FFs_NMHkVXoI zeh(+WRqXQv$d6^`Td?%$mTkL5mEz(@Z$WB1!(+h<xD9KG;iKCyiQvI)6~kjLX`Z<6 zf?OG=H-A*tzVZ`YTIzm;5}KNKM;-pXcht(|JFvA~{bP}NzXejCb9-pfSf!8TA3>0E z)%)G|Cj3`1Yim(4qkV6BGlDWBVf@QG5I|PYzEOPbU06!b{`V-UF5df|iq_<NDq2nV zRKmNo!dV~8tq?aJz<$8O3cm9p^fK5h)Q3lFt*#Ik9zvK_uG~eOOE=8P40{6{7X@0p z3-q4esmEZyrX5ni8Y{oBa>g9dTqS1$k9t0sU_IkKYAyV-9#3Gd;Py^fjfH|w^T8ol zB*ac1Y?W7nzZoz>l>1^l5rT*s13LEeZ6WwFHdM>YaChz-iUw>}gb7;|2J>;D=!?z! z-5m2JM}wR2D_EiIiy-^j_=lkwPP;4}g`osLWwcX~ZOZRJ&rQ~<F$GxTJTI#@ZVpq7 zxfvM5$A_VjWM6x7YTK`-ygEIB_?6CfW#OK!y=-wEHtrKn8`p~G!?6?}85|gep;5W} zRg@nY?ALxRDcsL9qA(SU_KPJ^xCeNVp5it*nhY<JDGK7R(K9-##P6fArxv~z8)9&_ z4jkMRkHIRylwEo@(~X!Q#2wyRPXbhU9iG7Nh(}+#YAL~*cr?&mxIP|x!1uhx4Z#UG zh%V9N6L1({Znap~3nx*zRr5Q&F$9aMg-;@;>&UsH6nuu44aFt=N<XZn^wY#Df5L<j zI1u>EHR#XRWMF5$cqFB_MWweJ-!hUs$vvR{C-H-lDD)7QCDGy`evw48n+VWacf6~L zB9Xr9MF)7&C=4S5o~#O*bht~!11CmNOxpPEQQmqmKbd4phx}2J|4)ACz<>BrMbb8& z`t-}hP;?yN<)bj<35z6;w}QNX`l8z_p;qk5#EYSrUs=3tGBT3RohD0PY?+47%JcTw zbafIgO~-H_PaS^6uTIBq#1l9JCxTbE_b;BdgSGEuE8&gVX!f#s554{on`p?!V}4|U z*ph>Xby!r%Uw#wq5^*t>(R=0N#W<5iG*_yE_B!F${=d~x$&;5*t<<_PqSnpmwWuSb zcxMUDp#Njy>N0GQ_{nm7!#6hf;A5(p3C9|d&Lcf|&Un#5p0NfkSa?v(UV}k0QnDPp z*?n}%IymMj7;&-?6XfZ1*N+IW;pR^0I4DZCVk*5RMe{cN6N4u4?nl^HM{(d4yKot9 zJ}d_8#!rKMa~5S!ox31=(cHP>;eI?nm^7XH2PT^Lphgp;(P&N?12r!aZYJ8?f!-=& z?&D<O5AWk}vAm7^#lczp#ed;nQTvjJe1JUx<6h>Ye#XC$5x0NFXAw28RtB*{e$+@g z<Hw>2e)40~R^mn@3)fR)rmHrLPl;o}I{B<~_ngEGBQtA>X=N8Skl@oU>;-q@g~xSe z2{iLUS2mTPvMZym7jNmxCK1Sf51-zRQBTMHyt^CAq-R$*rKLv@8%NZlASojpK`g|V z$m7o_6)c)jZ-=KvvkY3yjb{IaL|!s01!fDQZj+d3Va?Qh<0-LhG?8z|GBYD`$Fpn# z!JTK7W#MtGCsEcoHo%u$O8i~+!s)Ygd|huA>mzAut?3m-OlRVxt>Wi6b{_rY;_L;9 z3$hnR_vCqTEQ~j#F+Fkp(#xwch^O^tW}2GRn^CihukOuy$$p&_icOS4F|yZ{I3s&* z_QLEQd`LVq3u7WXDXqTPhf%YP2lQ3(N$<-B6ZK|a){mgHFQc{@|F*AEOL3&mp*w`X zOJdX><bO<3!UvO-@XaK}9-XY%$0oBBV#!Np)KcT6$t*$IBpYr+dfW1X!+GHdX7El8 z^>*NRVn4P^b|l@+Nd8kl7Oo{y%exre)u3}12I;h>NF&6Q6y}hgbQ$1gMa3$Gn7eMc zYnTd;|8N#1>q+0Pp>}}MaAg47BsKhOWql+=Xd+KaX4DJiV@I$)1n-PcHM)HSOO)%8 z$)0{8zcWGxN^Bd*sE^g3?@MEG1Q*g|VQ`;xMJ1=RSfaAhSucMQ_SI$Q<h+q%h+Mds vPmW_Dv{alf?Gq=`nXjyz#-U7h7`4OH-YLUW9A^$wI{&|%R88V#I+pyu`clLw delta 5735 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 diff --git a/plugins/official/ai-completion/dist/plugin.wasm.sources b/plugins/official/ai-completion/dist/plugin.wasm.sources index 12a94dd9e..c715f2339 100644 --- a/plugins/official/ai-completion/dist/plugin.wasm.sources +++ b/plugins/official/ai-completion/dist/plugin.wasm.sources @@ -1 +1 @@ -7dfbea1faeae8930b462fe80d431c83c49aeafa45ac7acd36f7d7f05fac3aaa4 +acb5f0a97061616d5bdcf7193d8fbe45c27818663dc0a9eab2e8203589492919 diff --git a/plugins/official/blame-annotations/Cargo.lock b/plugins/official/blame-annotations/Cargo.lock new file mode 100644 index 000000000..5c2101816 --- /dev/null +++ b/plugins/official/blame-annotations/Cargo.lock @@ -0,0 +1,336 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "openidea-plugin" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "openidea-plugin-openidea-blame-annotations" +version = "0.1.0" +dependencies = [ + "openidea-plugin", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/plugins/official/blame-annotations/Cargo.toml b/plugins/official/blame-annotations/Cargo.toml new file mode 100644 index 000000000..b221d2723 --- /dev/null +++ b/plugins/official/blame-annotations/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "openidea-plugin-openidea-blame-annotations" +version = "0.1.0" +edition = "2024" +publish = false + +# A plugin builds on its own (target wasm32-wasip2), outside any IDE workspace. +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openidea-plugin = { path = "../../../sdk/rust/openidea-plugin" } + +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 +panic = "abort" diff --git a/plugins/official/blame-annotations/README.md b/plugins/official/blame-annotations/README.md new file mode 100644 index 000000000..56bd9552e --- /dev/null +++ b/plugins/official/blame-annotations/README.md @@ -0,0 +1,21 @@ +# blame-annotations — API 1.4 example + +Run **Example: Show Blame Annotations** in a trusted Git project with a text +file open, or click its code lens. `vcs.blame` becomes `decorations.set` after +text at each UTF-16 line end (up to 2,000 lines). The host withdraws the set +when the plugin stops. The lens performs no Git work itself. + +Grants: `project.read`, `editor.read`, `vcs.read`. This example annotates on +demand; it intentionally has no background refresh or attribution cache. + +Build from the repository root: + +```sh +plugins/build.sh official/blame-annotations +cargo test --manifest-path plugins/official/blame-annotations/Cargo.toml +cargo run -p ide-host --bin openidea-plugin -- check plugins/official/blame-annotations +``` + +Install the built package folder through Settings › Plugins › Install from +Disk. See the [plugin guide](../../README.md#api-14) and +[official/community policy](../../README.md#81-official-and-community-plugins). diff --git a/plugins/official/blame-annotations/dist/plugin.wasm b/plugins/official/blame-annotations/dist/plugin.wasm new file mode 100644 index 0000000000000000000000000000000000000000..e587d742478ac06d4ec127d1ae0adafd9fd77d5c GIT binary patch literal 187372 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 E2Mtuz^8f$< literal 0 HcmV?d00001 diff --git a/plugins/official/blame-annotations/dist/plugin.wasm.sources b/plugins/official/blame-annotations/dist/plugin.wasm.sources new file mode 100644 index 000000000..f3af33fc5 --- /dev/null +++ b/plugins/official/blame-annotations/dist/plugin.wasm.sources @@ -0,0 +1 @@ +aaa3cdd1595ba8aeabae6472f7f1ef635bd3115662d64d9284152747edfb6b16 diff --git a/plugins/official/blame-annotations/plugin.toml b/plugins/official/blame-annotations/plugin.toml new file mode 100644 index 000000000..dcd31b019 --- /dev/null +++ b/plugins/official/blame-annotations/plugin.toml @@ -0,0 +1,18 @@ +id = "openidea.blame-annotations" +name = "Blame Annotations (API Example)" +version = "0.1.0" +description = "Small plugin API 1.4 example; see the package README." +authors = ["OpenIDEA contributors"] +license = "Apache-2.0" +engines.openidea = "^1.4" +wasm = "dist/plugin.wasm" +activation = ["on-command:*"] + +[capabilities] +required = ["project.read", "editor.read", "vcs.read"] +[[contributes.commands]] +id = "annotate" +title = "Example: Show Blame Annotations" +[[contributes.providers]] +kinds = ["code-lenses"] +languages = ["*"] diff --git a/plugins/official/blame-annotations/src/lib.rs b/plugins/official/blame-annotations/src/lib.rs new file mode 100644 index 000000000..937576e47 --- /dev/null +++ b/plugins/official/blame-annotations/src/lib.rs @@ -0,0 +1,74 @@ +//! A command does repository work; its lens only offers the command. +use openidea_plugin::language_features::{CodeLens, Document, LanguageFeatures}; +use openidea_plugin::{Decoration, Error, Invocation, Plugin, Position, Range, host, register_plugin}; + +#[derive(Default)] +pub struct BlameAnnotations; +impl Plugin for BlameAnnotations { + fn run_command(&mut self, _id: &str, ctx: &Invocation, _arg: Option<&str>) -> Result<Option<String>, Error> { + let project = ctx.project().ok_or("Open a project first")?; + let editor = ctx.editor().ok_or("Open a text file first")?; + let text = editor.document()?.text; + let lines: Vec<_> = text.lines().collect(); + let items = project + .vcs_blame(&editor.path())? + .into_iter() + .take(2000) + .filter_map(|b| { + let line = lines.get(b.line as usize)?; + let at = Position::new(b.line, line.encode_utf16().count() as u32); + Some(Decoration { + range: Range::new(at, at), + style: "muted".into(), + hover: Some(b.summary), + after: Some(format!(" {} · {}", b.author, b.commit.chars().take(8).collect::<String>())), + }) + }) + .collect::<Vec<_>>(); + host::set_decorations(&editor.path(), "blame", &items)?; + Ok(None) + } +} +impl LanguageFeatures for BlameAnnotations { + fn code_lenses(&mut self, _doc: &Document) -> Vec<CodeLens> { + vec![CodeLens { + range: Range::default(), + title: "Show blame annotations".into(), + command: "annotate".into(), + argument: None, + }] + } +} +register_plugin!(BlameAnnotations, language_features); + +#[cfg(test)] +mod tests { + use super::*; + use openidea_plugin::{BlameLine, mock::MockHost}; + #[test] + fn annotations_use_utf16_line_ends_and_require_vcs_permission() { + let mock = MockHost::install(); + mock.open_editor("/project/a.txt", "text", "😀x\n", &[]); + mock.set_vcs_reads( + vec![], + "/project/a.txt", + vec![BlameLine { + line: 0, + commit: "1234567890".into(), + author: "Ada".into(), + date: 0, + summary: "First".into(), + }], + None, + ); + BlameAnnotations.run_command("annotate", &mock.invocation(), None).unwrap(); + let items = mock.decorations(); + let item = &items.values().next().unwrap()[0]; + assert_eq!(item.range.start.character, 3); + assert_eq!(item.after.as_deref(), Some(" Ada · 12345678")); + drop(mock); + let mock = MockHost::install().with_capabilities(&["project.read", "editor.read"]); + mock.open_editor("/project/a.txt", "text", "x", &[]); + assert!(matches!(BlameAnnotations.run_command("annotate", &mock.invocation(), None), Err(Error::Denied(_)))); + } +} diff --git a/plugins/official/checkstyle/dist/plugin.wasm b/plugins/official/checkstyle/dist/plugin.wasm index 35b84c97ca37ea9db23283f6f795130bdf8a5dc4..d365a3c2360a2261881af270fdcfa74ebeab892d 100644 GIT binary patch delta 22923 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 delta 22762 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 diff --git a/plugins/official/checkstyle/dist/plugin.wasm.sources b/plugins/official/checkstyle/dist/plugin.wasm.sources index e3440fcf3..ef8ba4ca4 100644 --- a/plugins/official/checkstyle/dist/plugin.wasm.sources +++ b/plugins/official/checkstyle/dist/plugin.wasm.sources @@ -1 +1 @@ -adeef6ba69276d3ac8e9ba349ca6e787befa6532ccac69bc35a381c0428af189 +7d09b4e0e3edfbed92be8efaba47d468b7707d6164abf45b0bec495ac454f4dd diff --git a/plugins/official/container-runner/Cargo.lock b/plugins/official/container-runner/Cargo.lock new file mode 100644 index 000000000..e65b12d0e --- /dev/null +++ b/plugins/official/container-runner/Cargo.lock @@ -0,0 +1,336 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "openidea-plugin" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "openidea-plugin-openidea-container-runner" +version = "0.1.0" +dependencies = [ + "openidea-plugin", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/plugins/official/container-runner/Cargo.toml b/plugins/official/container-runner/Cargo.toml new file mode 100644 index 000000000..1767d21da --- /dev/null +++ b/plugins/official/container-runner/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "openidea-plugin-openidea-container-runner" +version = "0.1.0" +edition = "2024" +publish = false + +# A plugin builds on its own (target wasm32-wasip2), outside any IDE workspace. +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openidea-plugin = { path = "../../../sdk/rust/openidea-plugin" } + +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 +panic = "abort" diff --git a/plugins/official/container-runner/README.md b/plugins/official/container-runner/README.md new file mode 100644 index 000000000..1f8cb6b2e --- /dev/null +++ b/plugins/official/container-runner/README.md @@ -0,0 +1,33 @@ +# container-runner — API 1.4 example + +Put this directory on PATH **before installing/granting** the plugin, so +`process:openidea-example-wrapper` resolves to the included executable. Run +**Example: Register Container Wrapper** in a trusted project. It registers a +project run target, opens a keyed terminal, writes an Output channel and sets +a window status item. The owned `terminal-exited` event updates that window. +Disable withdraws the target; terminal/output lifetimes follow the host rules. +The fake wrapper just executes argv locally and exits its demo terminal. + +To adapt it to DDEV, declare `process:ddev`, change `program` to `ddev`, +`args` to `["exec"]`, and use command overrides such as +`("composer", ["composer"])`. For Lando declare `process:lando` and supply its +CLI's command prefixes. Only approved executables run; selecting one in +Settings › Plugins reapproves/pins it. No shell command strings are evaluated. +For mapped working directories set `container_root` and `workdir_args` using +your wrapper's documented argv convention (for example `--cwd {cwd}`), or let +the wrapper manage cwd. Environment forwarding also belongs to that wrapper. + +Build from the repository root: + +```sh +plugins/build.sh official/container-runner +cargo test --manifest-path plugins/official/container-runner/Cargo.toml +cargo run -p ide-host --bin openidea-plugin -- check plugins/official/container-runner +``` + +Install the built package folder through Settings › Plugins › Install from +Disk. See the [plugin guide](../../README.md#api-14) and +[official/community policy](../../README.md#81-official-and-community-plugins). + +On Windows, supply an equivalent approved executable; the included demo script +requires a POSIX shell. The host API itself is cross-platform. diff --git a/plugins/official/container-runner/dist/plugin.wasm b/plugins/official/container-runner/dist/plugin.wasm new file mode 100644 index 0000000000000000000000000000000000000000..13f0d8ac4f8b57433659af52b135cb7fd75ef3f0 GIT binary patch literal 161182 zcmdSC3!GhbdH1_+`!aj)nXEv7V6d*eleUw$1kxUwQ0jS|HHTtQ`;qpgeLbK0eo72c zG6_kT35e&!WRwKs9TgQ3F$xu`M$xLp3pFSp-YTtDQK_PWwG|cbwrV-w-}C>kwb$OW zCjorhPmht=>+-)m|L6WZ|H}$C&RsYjghAuxFr03L7uD<MZ{EIT^ZDUiP@CJeaogq~ z7~8&M*P3VUxOlrBHnwcPWaG9ix*XfIar>st+qP{!KM3m2+_;4co+d$U^X%-7**FNo zopa%AGPZNu#n0NZea)8h=O+B+C7WmGw(QtG*WS2u%bMb*vuWebjnCM!ZOg7Lo9Ch= zXl&ZCeb?qocf~;*&PK`BE!)qZOU~c8YvWuJjPZQ)cC`@)wN@4eD|YMz?DIEoJi~Qy z>aOSP+&nk++ozm%%Gz2`Z-s#4;A&hHPB+7?Ko%!zey*`=$Bu2gw(L}g7hbq=`}qu^ z&CqvUJhx`cuFV(5K^VnR81@GihciK#gi}$(m=cwVf?5(BKeu_;nr8#%j%Tl_)DP>e z$A-c3VE`}Q;-QNgOTuYdZ|t1iF|&Eot~hM13WLWDmw)z-*{yRsH*VVOtHt#WgNlk; zjsMB8lAr~7c5K?XOPDdRlS;!u7+w&BTcU6)z=z4)=55aeeM#_`Kf7@At_yaY|G)Z` zPw~J2{I7>WHVjO^pogYbpAd@sbZLCt_(^z9>Kiu+pt;(vExWdDo~zI8xOjHc=DFtF z1v_SUZMt|D2v1#+H9I%YK6A(Hg`3a!Sg#M#Iu0*tw4Vv5tr0}VZ!No>+jxoHO>91Y z%dQ=>u<NFa=Xeq<Y&ndA_9dI<)@-_9<MwA+rLnn97i>QNVq@gmj$IdQo`oDD_a&RR z!+y0^Hw;c1MuEL7IIPk5op8D)e%}$z&4oKdxJ#6I*6hX$=aP-H&zcM3&D$?=Ty0DS zSGRg$@YvyIsRU|@cAEp7dkzvRRvhcIVjxxwVa0}qvwicnImgriUDY@41WD(IF~lxk z+lyI%-?%MIMh<Mp#k+Q1yvx(IRX;8Q1BGbxrRdK*ENv51Kcqm=b^6dn;Y>KE{^NM- z=I0djk2hTiVPMSTH25{UHqTzTW&6f$Yvvd?Y!Z$Z=u5V2es+PDaf7Pl{XSIhhUq2* z#6*?tO%cI~&?X9ZCgE(u1b6$!3pdXVQYq?4%+B93yGAsNCZvKkZrQ$hcFpXL9lPSD z*AR0EO>6dIV5it-Sfs8#m-GkT3@->pnK4nui&CrZN;d*<)|}n^tSxg4J?yl8BMiF3 zY|wAK7<1bABa`sv&K=vfZG6VI&9O<dm*WtG2FDk2TogIw8QXSj+8Q$GFx>eZ*dW@1 z!UXe)N`UAS!r+)^Z=Bn5#+DtY>e1BNQ-0@^->WUHk0Z@vWmk<S%Qie4u+P7E)+v|3 zXnhQiiq23*;TG{)^6U#XZ#M!TGk5Vb<~Gf4dB)~7MMXN*c&*b-f^P2A3wK~(c5L6W zX^lPbFdW+$52Y5pH3?hG!{FEgR<kg^;IjJhuxPumIu)qzjl${Ulc2L@I}D9Vp546h z!k8w<9gCMwM7-n5p4>1lijLd1ac-CNj2rP9Z{d(Dz(ltMV7%iRUE?iYO)b%51t$&T zgz(v6U6_%|9Sd4X&@D(c0Lm%op-o%YJbM<K61Kyg&lL|*F>)1QKP=+*U2~maJPB5e zZo*Zb==Vm^Rild<cWl}ItToTvcJbT=;gWDmvtKPug2#=nRuVN_+ImbF99Iy<;bP8{ z<%Ie%@K9$E*QO?gq~!~!fV;2-MFw9XTL=}@IUnSNalemE+qRtQp87scJYpY@d02fw zx)FWw)c3KXzEtBFV4}uaw7rIL(lB>bM=|C{v1L0QoEQenJk#4Cwruz9Ken+doKC#m z2!sGQbcw2BFt*I@*nS}t52M!O!r(Dw1C>|4nO|!>Hk_7YLK9)#xgWT7i2CMBx9kcl z;v+2TnXmY`#_^%#Y@*b|CN6j(GEZfXgKvG^tZ{<XT~fYv>`djAVeo4UE4$aecDH^k zm_25w<Hmmvr%j#q7Q*DPyEPJ);v_gmSSq`QGr)AR2-x=4cf7K`jE;{ThPpB3dtL<r z%4YqDhPpm>xNEU$vdE|hc=p$i()n*Ziq6HuN9erI%)c3a@F&dxmmkL{2)`FT_PlU! zxc9th@7{o`Ko{I5d--2*Ud&(a>bkS5rS7WoH#o07a96WCS2Z{<pmw;2KS6L_!<P$+ z_JZ@8Mb)B9e(7^}U;yk1&Qoi93@V;Qy9G>HM}>A9RIX3nRYBo)%r(DfkF~_lo*;Ut z_1L5OvM2u8>}Kbot~q~p$IgmV97IR0d<0iI2#@B|A3xNycN~J~=<Y%3PXvucBaKDk zlc2dEc&hkXqkXiXxxgRxqHPhE_dJ8KBbRb+XwVvA*4Hi@!Ua6MV>XBudMj&B7%Ex# zBSAFMGOb-PRKDsRELP{Fp*n7`-hl|BBdW=@j~%L9sut@xh>ol$*Z$gK)g2H)w76MV zJ8VaS$l-dx2COv`F%q|nc$6c{nok+DMkD>`{!S2%u6ZOkCrC!ud076=VjUjM>j_30 z^0j?yAG94*L+;AjidQpKq)^ah5gr8X5nR=`Vrxejzmcp|F+OWYS*^t?l`t)+B;cZ> zdhlyUIR8N>{$l)vcsy)Pw4zoro{(0F!btyH5=E`39;MNED~`h;it4R2ZY`xO|64+E zWu?kLtyW7f!mvi<<F5D{BrVysxHUdDrV_0sOOhmPjnhTkiemd8KVxIZ)Pk@T)@pG; zu^PP-q_%1;YV(L!wXyivWy`{4%Z?obz%Ux4HzM|05FdAZt2GW7v{keJYVnfsu|9OI zu@LyjTW!Xc#N(}{5pi1FVnFS2{Qj6*I-u)X!VAjAI5MF^GXAJd5>5Q&Ul`YxM#<Rt zQl8eL@$qpW1JsD70{!8CHiCA$&7eEuZNbEqzzX<(jek}Hcxv%@)Cz^}aZ+J%tyMc= zNo#CuB3_;(%j0-yD*+Fb=6|i)i8Rql>hX!S6CYCp6Juj>EClisCsBI^W8i=4#{L)S zl8#PFNPvXfwlRcYY5hPnxi`$`=MM+%y+Lk&LS5~9-K}<WrT_ieRqnp7mks@C>d#XB znSY<FJ^ya^d-t{O_ZYoAR)3c1&vE*5y#Ab^Kg;#!J-+#qbn_v9bFcrk9`5!xFTBGw z+EvLLU*(|rk$-uQ|Ls|uSGbo4uXVrk^S9YhzG{CD{=~KO`7gNNuiMl6f_CG^s9C>o z^MyNRpA!WqZz9F{=R0@I?fUcKkiW<+xXjB+G62EU?<`(?+m>f;-t?SJ+k8H7r4neD zY}~au$a@c?<>~)6T*&fSYe*uUzisoZhr!(i|JUSj;YB+?b3wbqMaj_?ke)%x7i<ug zZzB^B+!2j^0U6H2_SVP$pHKMjuLwUKeK!1Ly!UnCH^VE#*MzSQUmIQ({zLdc{Py_1 z`0L?!<4;BZ6yFzqBm9@}-@{*ozYMR6_D3&^u8!`A-WvTNJQ&>){Y~_y=)=+B=*`is z(MO`cjD8s29Q}24d-S&GhUhP%w?sb*ua7<$eIWX1bX)X>=$h!A(c7a#(L18oMAt>% z58o5r6um3@tLVMayQ2?9?~C3Ty+67!x;FYm^rPs<(NChEMn8-GHF{-yZTyD#V0=yd z`S>5=L-F6npNaoI{zm-4_^a^`;_t=Zj~|R%|8RTb3o)6;XNKXPXeye@gDjkkf~E8P zFR(wo@DIk_!#@}gS3XbA^ZWMQIv>oWe|H%-LEAsP%RPME^X*|4u8xv!l!bY)E=sZ> zi`J73xho3ey(_KVJl)!hSJ~yF-3lUu2MD4`+Dm9}?>f|sf06Ve4_&ic>xAt*v@Sz+ zSpa+GegWI7t&5uNEY6~=HW_u6+CZ%LEM6T4(|`NPFW+$2_iwrC%>BA=PQU2ddp>;d z?iao8MSJ(}JBN`|)BQ{In;xVpSTW%8a0ZaF7`)|s&Ki#xY`i*Zu8*4TDp;?#c{<}p zyQDzak49jz7w0d3{bAtEA4`>e273D0BjEX_CE<91(VN1py@u-~Yvi4oEJ=SPI1O73 zMhzMRPp!ARUY4b=1QP+M%NiL)Qm{nbgW*ix$l>`sOy8gadUt);ZK|nJ|MdPKrOz7o z9Y87`-5B=9vLrujo%NE*AYK>58ILA|U|m3~*3*&^J+=BRdP0^hwrTNWzQxr+V|~!b zc(ghQ)|V|VAJJl`-(uvDP_{Tui=QuBOxFi##-r6ibA8ZsS86-`xR9B5@+7@oSG|cp z7?1rBk`Wym1{_ifWGu<=fAH0>uWRTNc_Lg-WJ|=X6B&0!56jCQl0O))`5tN`dPw_2 zs{0`=^=My`EzQ5{hO~4|kem@D8IK0gKB~oI`Ykqmi^uvFmuAQ2KXfe~>s#cJZ!yV{ zuVq=1EnOF-{2a3`>VQ-U9d|awB!js`@@QRjCfvq=r?SR0+%=!Ak51>w#JcD-o?OL~ zbbWL(PsZ0pll*knMXRVtGZ}a7b<s)uwAMw-dDu|DjciQ)HnOJrZAeF)%MYsO92%rO z4F39P1CJgIX87CGpUzCWPrNNKCZnCbB;O1BW`H;u?N&Qnt&VoBH}xg*xH|8oH4<-L zip4`}$)4I}$xHe2^k*u|Q`f5`GXI&OFmI+m_3!MdlaL&&<nz}Ymi#Yq)zTL@@e?TN z`uJeTAc1fBI%LZM-WdS=Ms%NpoU#L;14DYq!|obU*$Z^JT4m$&e2-R$Ybk+IqB$l2 z9A7s*k4)$h!VV+nFE{vm6J9iVREp-6q40h$fV^HR&G#{i4^c-&A7ev(jMK-pu8$?2 z56815C1=AV;s@v@et<3D2O;lDumQiQ-D?<kM(1#6g~?zmF_}m;kzePCKyeZGg^z+w z-eIV2w^w2h=YxVx?5V)Zc?y}=p;i6(U?h42IqN{K2PDZ~d$qWMTd0wKP<0F>8t8*K z8ST=rE=}kAa1L?>?BdxO!ICxh>@1$RSCw3U=~r~!0cd_b&?Acs!;n7T8VT}brkg}l zf~M1(aPq{sj9;Y)jENB>)F1LhM|w6HUEtINe0ZL0Wnea*KAJP>s|=z}J8P=ojbVDF zGlp4f=pHgx-{(o*Nnh*BS||OwyjxzrGacH0mzf5v+#9!7?u|1m_l}<nB#O5Ui4-fr z*Y!=IH~;N7EaC&ntRBhz!m${3elQQ_nw8Fv{G%f|KgpwTehROz7xrC~@Zq>71sw;y zq@(#kuNg#7Sh$EDJ;9K78VCh?D8H-k8}h{ahFmqfap4={(wIBy7(DmBVGRGZvk>19 zCvLzu42OI}?uUHC5<Nj5J;ako_uLQy!R@`r@Mw5>>mXiA*ZEQ4#n{I1;(cZ`!brRn zKJ%}{Hje@?gdlJ-gv)~TdeeCWxH_sY{evVNE1cphaEg;E6J-`MMET@4Vg-EDV5VE+ z_q3UAJ)YtM-ylqXtX@%M2|(nL+=3)OS?0Srldt*(If*QBioMs!O;@6|FO@ROsffZ} z7wzP(zAoD4RVgapnplUfPV#$9r5|#-J;@(HYg}y>kwTNv`90j2r>Vl~=;`a@bErTG z+AdW-h0e{IPT#w?>DvuYNq$$U(%rRbBUdJ$81Nx!WM@~tc(hH9d+Z@aPh(Je_)_^> zotbT}4(jWJI`)Yx^Rf+~e9&-PbFH$uWUx6Dcwx0W-fFkAPStySNyvyX!Uy!*S{Lo( zclu?FYUg_4EW^<gPO<3QJO-N;gcuIZPP>;SK7?Cmb?e^L)t!dUYW}`fXzVbboav$k z@~6%8qVYi1DZir7+e_LOw?w-t7D$LHZeK`-tl&99+rY7$*E9F-vlui?-)vMOn<OT? zRyQ!2MA5=*cirg`>ju5plzSF0H3x#C^FiC9;PQ>`n&}<xO;9S3_I+gV3Q*RT+z;;X zyz$C2)ZHXM@`5~(5Cq2Bfo)guo6dj(2QqirO!`I#sFR1Z7nxzlT7j!w`I?Ias4?zI zu3^}A+4`8EjHq31GjCVN`1s~{Oh%JHp@5UelMz+qnOGGSFcnUmp5CB7L6Wnqp{H4r zuPyB8<2>r9Vs5)H8F*^8-$WX-z(TgQ7de6$vb(W<lup2CfNMn(pg^9Z{iOOK{$9Hc zjGP2A5@95rN&nJ-!Vu$K4v@A|<~mn~I_X=>OI(i0^vmWXMPSsy(*9x?HVP2onF2&< zK0u6&ODx6E1HD-YHs0l0#7d#G!t~vCD?oeod~$miEz@mAQ<?D&ml;BfqU*(Ah{yH( zH=jI?lcpESX`rD9HI0Ep5W5BSA5*Knrb=>GI8lLuE0GXE6irc@Tj^7-TbTZpA8C=~ zaG4&uEP2~74i39PJQWT7gX}vIIpca$pqI|6>pa-7Vxqx%!K7Aac@m{|&GceV=h$;Z zFi7`KeRR1_ZIS0m`TTPZnd!@oOhM}IgW?|Xp!0!3o!C`&%-V^NPY+F0K$q_x0WyU3 zRU`%@My03dBKSYwJJSoLi`wsx`U>W*pwBIE<7BkV@h(ZX&UyrjXRD*->tnGsZ!^U^ z_H5O9B;1~ztl-C<1!U&jGYH&-?bgl=@pk$qPu^8dKQ5m!_UxFm50RnV+Fq@%6>5c6 zi1h05>xjI$S{<FXKAvJ|8GeO0C|Vs)Ijumc4TV<l8ekZciN|c>F+0o$p(jn~=@rQm zdO15CeJGwYVaMUZm90q-kf>exuU|B7R7hreL%h^(|92|lgE$@JH4CJH6NbAZ!LRyP zd)~Q1zwJpzIij-HZK~of+Zd0<I~vr0*3qz94DwZ@Kw3NM;hh*nr(A>l@q+mtBW{p7 zn13Qr1_jj62F@rqk7UMhr6jZNB+~>F<Sm3LU4T)HC`#oKK6cQm>Ihtp=oUgOgk_Z< z^db<n+gDfY_q~W$-SG}gg8(JCL;Q4lO_`cH^4lH5StK^h*P1t|fE{e38d0J$u=+9M zSI<wM$;Q%`8DqrMJ$)vhv^udwoCIRle4;rUB#E7Z_4V#8`S;3-DZ@^L4m41S<-$jn zH84Y#&>Z-cbcJQs3-t`V7@)P@09gr)t_sQm##{5X+q(fH<(ly^usFTRm;%cp5HYdP z%z*62Dec{TeN+WXXmZf>T+7K3S|;Z<(@R{-#%6K{LS30I8iKsi`5RkFW3N{6M0-|> zF9Ra%azY_}sl&FCCSq0_yvt(~;9VaoM*&b=A9Oiu*&1n3Qu|6GD0dXg9{?8F$w0<d z;X%G%BG7FN%E$0sR$CJUUOXlcj~XnGZNuG&xo+xO9{#*073gF)Kipo*yH2HCT9m^v zwsJVeR&XD`@=N*qGi3S2m9a8qK$^%u(Ma&`na(&Ec3T?RRbdy`YIS@?*zM$(yP+^c zyBL5s`i`&n9Uts>tZbC;#x++o@03KhL4x#uYqJbeh*!!;=oEtS3$VD0Aom=Hv!^(e zZJ^jl+&<kEdzveDF2$BrNU*#h!3D0^`L5V`6gx>Eh@uvn#ksU<K?$hB^UqbDKoy=K z(7sGE5!D-bB#Qw5s1U!;V@R`HXqNP=+z0YWVp-HlIe4D=JR=%H#E2uin-s()ncKOd z5C|YTuXS8B+3j@jo{SfVdb=WY-DMSIt}2i@vOCAm{r<qTF|0fnE>}lakfC0#ivwYI zT<`Wd=8kqd-oDg;BBJf+<+g64?Ie6zuc-$ebS$8f%zdx$0@zG;5tHKL_mXUUJ#N0m zmdp8Rt>=}+qKcMqsN{W{*9*N@$IInIab<qh$zlo1;Vc8aDD)aC9-USn7w+X#Q?8OO zluI|c4X^tpPHgoW=gn}!;5wG#$M;WvA~4r6Y)1x<<{S0Y4eSci?LNbr^3=vuNi+Tl zX54GWiguPl&G1T4K}_z+CcIN{&?4ro*(+ixGj>H}3&f>ce_btNX=b5vAMD#nuX5`O zjn2XL0s2$;;cf#6K^FMiml14ZF~Bnm?U}_BEV_X*brY15eL?5C=sfXLU5?}9`9W!( z?(R-=rTKfBmo3#lO})83Z?m2yd2jKkt4~H};>=jk7<$YVFciNTg-$C9$z*qfmE>oz zGT|8kBrJTNu<aiQJO+tWJH4#7I(mG!;ogH-D#|BltdGv;2fUmIj1a?(_V0`va4pC$ zGI88#R2oi*X?!!-GS`eVkQdNWusXtlS7U_j)y6_KM!LUA=+g&2L1fFuqQx6qKB6%g zze&hbR*x62o{gwZ2B3*dcvv(g2~o?&YKu39Dq1kGNgDH&yNp!RHr^`=+f(LDlV$3L zj-Xk9vPS+f=8+glQPo5e20<w26~@<}4s+cml<oH4*ZabA5VxY~n)W=7LnZ#BlIAPz zZY@7JZ)fs@^ETz~9?uU9dk#M^>{Hy`2DAm-h`T7Sq~mATn<V`luo;)<**rQ0>EP*9 z<Ix352wGR?(tJZ1mpbEabZdBPh;D$F#%t$j^r&|Mw?K40Kir*1W01tUb|y$bPDW4f zQH@-plw;E@*2mEEAccq6#Jq!c<c;NQXo&n@1Qq$^omM?Ooi6c(LsTC+uV#LnQV#L1 zQp367qG$`-*UU8y*d+Gw#?XV-*A6z~r>~1}e7Ia4ox5JiK)ci|-ZovjS-f5DDKK5? z9!{EDRT=EtoYWVBtzT8nyp*n#)$~%WS|AWiR^{6jkbFxh?T)%8hGtl>pBK<Ky4%DM z&`f<ffd*<O5XAS0aM@KNi7Z+&864S?w8MhNOgkXLMRk+G(bdrh3#ywwK!hDECWA<1 z(U{5L=*G|o3mP+hAaN2#FDT4~YYm=g%6(V`e&><A_HRo+N)F*&cO~EdNK!ZvIDt$5 zLQS;aIv4~EOn_uj9$FeQhJd=cgd`<oEtjugWt(z)2<0SPZlb4K0}wYGAc4Zi&mu); z?Tp-nMOKnu%UTjrNpUa`EZ5bT0t!{9A)KA>6gXHGNTX0wx+P4%V-@BVS@%t{&-NKd zQVwV(g+xva!6FZrgu!%t$W2BQtKq-!{`&p5&STK-P^(>oEA$DQn|;aj<L(^^snT|5 zLHpNDG5WIe^M`4rM_R@&RpJU(4cg6MrNW$`-C`8!U-vKHZ<iXI(17E^yioc+dpO}A zrth)a<pX78_R^c}1tX+#dbzaBlJ=6K)&Y0Fw5*oC&^__xU+nH;>bxyV@IZD~ypVo! zSGaWlGU>#LI53AI{H@sKQxO&vbQb?!OomtD73GgBa$WmgU`RbEL)?XNx(TK(nj(=F zH+!{dpr3D^tDF&R6v>*arP}(%Tl&0fNjbvW4P80JgRNb<d-@^%x8qMm^&B<uFdH<` z6!_3+J0}J)w(RpMjg<-JpH?Of2XcmlIgsHK&i5;jrr#`D6D;$r$h5u9oVusS`KN1w z<8eG63NOmnJW+Gf=_k#1iEw_g20{8>ce^^CoZkCKERfO*kV&)?X~o4!S;)mnGwHhw zKe5*A$tj_eBtK`y8g8BK5gl6L&jdq6dcUfsx4H(X>xn|5MG{{EaJm3cZ}@Kso`)-@ z_$5|cMAC7}#zzTwAxS?W7A*>Oy0ONM0Hay}2Sl6eKyJi$EWG=S4fb6`>}MReG50`q z$+angUbNL%vKQyU6MM;1jjx$xFdyJr!<ckMHxeE!d(W*2yu~M4OzFZ9I#f^L3`OZ* zYO4LRiefoDm>EuQac#q^{$KjH8l{KLdBGwFYQ@cKM(LYW#Vz|J9#DUQJRU&<**>G{ z3lrT@ez@?!+W&lP*en#CvNCmV1kB$Z$#vqq{=clgCIIVFhd9)oLUwpM=AW(!T1>ot zIgloA<Ui*Zqs_m1{?4%AmHV7PS9v95l$8M~^-&j3o^Dl7FL+XlexMw29IdYjmdnd& zoDnP+taOYJ$Z!-QP?uWR>Ad@Gv<yGiZC>(Q4&`oyE=XwRd;Ita5@4)+Z+FbUZ#t?l zbY9UK7gN2KxkD&Yn;@+#VYE-<9qhxkJ*KvKX$^?){hCZrcBLT&(64dW`DZ}}08(`} zsIf}tin!GIIub`*GB<mrewl$KDjnT*d$?<FnU2u4u{`6q%&J7P;~*b*y<Y|E&8yqG z%K_l%b;r;qxnIP&c*`O#X$3zoVAm#YHVvnj%9XkK9J{W(6gB!p4`(1bRQ_vIIGR+0 z8dLm0427f6;_3XbO7Us@ux9aGe!!OEU}CUK*Ao~RCg&N8qGB+3dIqLNrRI3tX$czE zXbtmWrzv&7M4YNiz<M}Zl*_osFJ?X9a(C5~9kORQp-tBw>};~Cv4FEiXKa{@3l7Hd z@k-n{SWzW_5~fNB#K`<y>VEn>cOM?ezj#%GWS*|PvE!k^zY^x(!_8weyeVqXrxgxk z>4!|s5XvOlBmz3M4#9DO7Ili%ya&|C51U`(_->^Vm&7!}yb)D+YviAN&0*Xbo-pnY z7@;f&>Dg4!ep~e%Zy{ES)Q#D}y|@mzOjbYZN{Ohu=mAJ1(OPwY|L?%Ov;dPb17Lpa z(SZr49UaV8A53L^D`1**0OxK8=cU9N4$gf(%TZr|&(AzMK#fmL;~W8>V|_p+U==`# z=lTi^jP48N6^QOeiCzmWZgR5AYaB#=+j_FqeYE$QQn(lrTO`?MkOK49h-*B9+-O=O zzea)T^_rmh>w-nD2d;ugaL-EFgbAf)E?pm!UQ&j!XL`xnvB#7BvTIdh7r94%c1llA z60bVlr3@vBYCMS+Gd~ZAp?dInED1DP92AuBsGjEqEgmuG#x&{OYDVo!)!7Yv>bemE z%5Dk~0uv>MoCtvpCqgBD`Xa;_6uVki{3(TAQhhq45bHWKgVC&OT~qo60Yn{DH5@G9 z#>Mq6gf{(vggwzrYu%<e3MCEW4=HIX7zNNAMFlPRDJpPXsZi>_LZRRM=oC7+C$G@g z_VszydNyT!LC3?_*vJ8#oeZE5$KJHrpcWUuj<OZQJ_}}FMyvo!&IU5zeOGxLnd!nz z)`TldYcF?KBneV?rN~eP5mtGDC>aClrXsFyKgl9Wk8hXJa;sbhC&~Lnz_YT6az~*` zjYsIk{++linlcYNw*`7f@?vd96$F%C?B6@78D3-%RlL}j9QR+ri~Z4PFSatE(u>^y zfnAc_g;wUqRtxq$n$nFeTmW!WbpcL>eL)uLPDy+IGN1Gfk8$LE18S>qU!Q`4kYR)t z?7Q$z+*5ElOsk&b-BuXrfMfy=+UKFHv??9q+vMeB<l4-mE?H8qz{c{oJJ)q=&~<H^ zk936Y&2^=Fb6x4)T-S0?-_f9amjx6dS*b?ETm;&yRG+^0A;(!muQ;NM6_S<fBl5eh z&o#k@afO&@(zAOl$HRuRmw@?sAq!Kqk6ABVFI{Q^46`IC<r7a~O>)0x6LKupwaF_k z;{xVz!R%mAvj{iH528o>pkYtY;gQ^X`UA%uB7_lxQEeb<773$nYqV=v6g+#PrllkS zPQ50m8Q{)d$NPt4?11V_coR}OUU2n*<MsIw{)`-L?;mR8Pw5{j&+Lr<m=m4;Aime< z2J>PzIQ!obNarx?=&aE{xKeWTjZ83?TrV8{OQDG<D%YX#5ee7_K>f<21eN*pf`^3f zML;#|IZ$O~E1;V5Rfr+5;(Vdnq0U+3!V^oC2f)1hQG(eL0$P^EF|E9?<*z;JmIn~< zU|))G8IYlF%N!N<!GQW6`_i4P%)_~+1Ds=5IFm-8UfX$-(C8onR|<PUqetqJjVak@ z5-J%!N0&~7H}p|rf;|c6xDe7oWpeF`dKXg-tj#gGMxqtA@&V}>&pO_rL@*NPP(r!^ z=nDitV(1-y{UR%8N&H?G=UuvmXztoxI8PH{9Pl&?VZ2@_^}e_hM)Nc!jHRbp5k|?r zb2a+{`Tf2?nqMd*5#h?1o=_gZ)LXy2X(4gLhSE316q#DlAB!^`&8s3RNh!vrYd2`= zE_fNKH<l{gO9fv=VO@I+jp6KOK1np_Udj>@t_TB&-!^_)X9O1*Z^WmscU&^5B+Jv> zGrY^E>xw+31>v%p8%$N2u;Pq)?~-g|Z>-D*jFtHSu6#a#E6WF9_+b|Su?W{W=oX6z z{pJd3uo+`;95u>b$iJ*Q6ATv+vDDH_iaiRX=(=Q^#ZlXLMhbMf^vaRYMJC0B%88H1 z7dZ6z0*53m2Ji*12wx<@Amx}(q&$4YnX|nRNu*vYsKf)X)h6`dxD!5TdPlqY2pw%` zEk_%E8FI8wF-My`^@z2N%N8uCY_AM}#%7hwB`X&*_Z{(e`}b+mm2$feR6gkjF%>;A zt1Di+0m*gz#!x|D`)G^r7xZsIj9+}|Ld5pdtbgNdLE6)wi73GD5+nvwXfRn;{|>1F z!zL5^c`U+e8%6qU&2))5mlaFd+sE^2VMx+`LKux0Z^U?`^s9;lNpQ2InJJBOoHVOR zdY3UgQvENRqKb%Z7S><x>;I;}a#B)7?1ELt=Z=JPF}n03Kh&rXDqsW10KpbpitIcD zTZ(L#ZLO6a4QILJd8niGOB#GT4ubYeL-?-v2RffKBZ4AD;baix-?;D9FRssl-dKzM zdGNJwz52TP-0V2hMkM)FY;_P!f9ET&_~bX={-t-{7yL1ALf*g;*1!)w@!p@)fYtrp z1Fo)oOiixkG1FlTTr>3<Lq8)3+ixENXV1(;4RAOqh6YasfPcP|S%#?WU5~!~iBKR$ zTHlxdYcMyPKlD({5^)bmH040~;kWi**HA%LGe@fT<8OwH#6eTQ`;EJAc|C>f{rz{k z_kbyAS(`Ep?R%qGvzp}`G4q;!lAEbkTPDCHhx9|t>>ab)jyKC?t;@R+z-c*|TRswN z`H0rB^2m1C&<L|dq7E#tHkiutL7lJIix@W)iv^qNVF;MSc5@-Iq|Jrxy6iK72*>h= z<+k&eN@<TZYY4DJ)GGNkB|Kw2t*t3x?}(<eYJ!MDJXH=V?I@pQ9dlHG)|=2QDF~Zu zFXlASAvSw|QZd(lLuK}>vWW(9&=luQ5^RlG6PwKao8GW#JvX#H002GQ`<f{g%@9e^ zxE4@K=|73VRYc1NoI=$#34Z~}yg8$Bx@o9oHBzpfyJykeK3cb3^wwl@wMtpdK)Qtr z>RbnGSl8V;@YLkKqM%sqm}xmst=ojOq#8rlT<rJRbL*ky%0VkaXyHzS-6zzJb*Gh# zXn5uuj0{X_YIwMp@POO1(H{+DfO@oAkm17-YHKBknh)~+j685KwK{UnPXzI^d}_zU zf-&*b&+>8AV!rg7HoF(+KeE}rFw@w|S^Lk8PLCk{rUU?8E~QO8r&g0qT$SudwHyYw z$bl5gDc<>p!^|n<arz!{5vDIPEdXx}mwdM?nUp0>r432Am<rdm51Sw>_`3b>c;tRH z&&NYk^%ZV52Jy=aKv}Jvrmr(WYBTiZ)Fta5SVF7d%;x2hZUhn-SQYD$QV2T=RJa2t zhP&Zn8b@^^RSM5qDhczR(S)g1s-V#pwx9&kuZw@wmL^-BW2hwt<{I`0(`@EdTmr`+ zC?<<cmovAmK{)NC%ZhfAB%_p^c9OuzN_!V1L-GWAF{d96jY|ZR#E=n{3}~8vfM%at zjOk7HJSv*?s*5V%A_-xnwVe`JQ1L$Jsi5YiG#HoyqPPz?Nb1vV6wg;lu#j%IJuJEz zQ(E`S)4y^WR<J-2=~q)Xg$FzurCuEwa-}>6u=<~%(ZBh>MWgS<SL&<0*e`Q1JIg9` zi4cqFwEmKnXDeTzF17w8UU|S>E^`S7JVuN(dQ#g^b*1%RTA*~rWl}#pKxw&*6(viB zkwxtXDyDBh=i2I?cRRvJTFNy&AiT8dQbeWcI}W-9`TW1fzaN?<)A}hw4Y#(ZWJ9$( z#D>euMYgoO%%XD)G(%6F4HeU>)05JBHEO}q!9bZ{!M(zb(}|`X^?DrFjc0P?2H4_v zT)bkCMLs=fhm#HHBE=Xu^hV(J;jyq@p-|{3guka{b|zXjoM6;SDDZ|BvNN+7ayO2r zqJe*UJ8sH$;qwV})BVAd%sTh9I3>x~oW)U2iK*n!r3sV#hrWnRoE8Ip7k$mpJ=T5d zJAdDx72@=UX6JbAO_xPf(^~&R7+zgI#ctuer5#O3Bc7gD?@4DF*cobm$aLw*sm`(e zVsZKf@llWGN#~gU^8&gd8<~tRl|Se^(Z#=t?5ZAipba}OM}Re<><s>dmRf-TMSpBi zhDgNOcqF(Wcp~VRtZv(G_{In=TXDjlzZZN~slTAp3fGvqkg>%ivM)n5=$nQd0^kA& z(#1Wf@_?gxA|JU=);_6qCF1_0NBuAPWgm5ZG6W=_N-RQEfX2kKBU=lUG=!f+*Logn zsUC$#jDM*T3R+TUB^B=gGZUco0$7(AQwiC0+9|1cR9p=V&(JKk#t_n`Kj?=wrtf!5 zHi=y9J8&y%C%sOAbzmzDwV=cS#9^3S%~JNqr#oAK<+G;)O<g(ARDl^Ar5IDIS)J$^ z+TF_BLabPo0<LrHVTlFov}Z<=Sx2efaMjOv;&@5%K}v%P^prz|l2PU(fRmhs!i&*K zFLEsG0=beGk#qLhf)}!|BWu9Hp|bA7HT1af3A3dAmWqlWh;GcS9*AzdGmTuBb(LQA zp$v#euPXgTd|E~~YC+M>4<(=~B32^u70*^0v6#~f+(5%p&Sh}(=3fcU0aN7QoKL(Z zauI|8y}tmIz`?=Dp!&vQ7)CWZ0OKPvAQtnrk5HxJ$9tP&@Exg2C4cIx81DanX2-ww zE86jo9g$TkPP3SHVT9<JxeRmMWYc%0h@mzf@+Td9a7;IJjEu^aLBtBUTvFlIIF&VQ zV99XiRCVm6Fi06DSr4v}i$2dkv+w>@L{l<F$I-M>?<Qf?A$5Y4l9)O0;op8AUS3g1 zBfGi25@b{saa&XBESG0_DULTybKl9Am=i8(j#raU`_U&p{_WR%`pY*x5OB6>-k8y| z2flRWM_>EWe<a7Yf8R8DHjT^agJo)Cd3u)_)kudP8T5BKq-sBS=ms$vo!pC^wgBsP z+(ah)`*{evvUv!*vUv#Uydv@<xg&Jav2A%%c*PZgR>xf}UvH|BXYB4dNhYzcyWEvz zn^eB@>DmcxwQmmx_M|7?BWIbl+~^=zi0Kk$96jUGDqQ%q3RfLVtJwN$`@=rPEVeBn z4!6tL*)9|tf-zsIPi^r`4!gEXkxP-2K|)4M*Km20keAAEIc4t;kSZAoU4SP-vqdiD zx{+I57=dkWsSZPVwj%H5hoLlE-x0FyE!CYZ(}Y_mWdYnNXl!4D&c-yXn|D;*^Fo(} zh6hO7KT`tv{mR|~f{)PSTJ9!R{Zrx}$D$T|U{rMH{>khfpCeFfg5y?@bd+DSw!kTs zZ#U+1wmB5S({AMQI8SvO1e#CA>+(I7g_q%=%G}B=FE$c96(=&xnW-N#vyoC8gD<X8 zr}PuI$)O3dN+V(EZ!PRJvaIF8=Zx2yK}ha5KQPX3*DrFfFzyt^`9b<`k}G-@f2Tk9 zH<A;CP)qzP&Op?@KkVn&Dw}!>Ur@>D0(%AOIa@&;I)7I1)XHJ8$wF*-QGp_fpqlW( zLLd~4YC89h6E3~m<6P}mT{uQ`!>{GptQ$XI*mm?{m_?Y`>|&++Ta1z7%c23r8hXwe z1xq<h%ZwIxwfs+JwE2fAfMNvd!mwkEX;3<aS!T=tyk6jD?8)F}!La!H7!14si8ioB zaOFBGSOh*Sk?h!QaU}dQ>@Pp7#P;xo%N9FrU1W=wd$lH0Qe=xUopR-PlvcCF%atvb zPFGaRtU9);W~Z6>yfF<Id1JB{QgZIF)=-Hsw6f)tEG33@(=%o0K^8MkN+)07(#hO$ z(6fR!+_}Zm$%UtDDo~IXD)56LJdoGKx&43ivU_G?EUYDb*^<vBD5jG&`}Qa9f7MS8 zUG;v=7?C}fZ>8y_?|kC@Z)=bo%flt@yp|(wditZU-0}@3kpgC+bo;<<`(M?Vn_WSX zIIp?(zVVg!U-yNVT>F7F`zh9(`CYoOwm1r0<v6vWJV*O$7B^wme*Sy}d4)taYVWI9 z?6u`Aj>aLmZsz0OKqoyNTRr{Fzkk=;-uI4AzUx~UXwFPF1AXX@H@*0q-}z$2K*yZ% zBVw`-hc>){R=S9zZ9Xl<LF?##oPKlQ#qGSalV$>UdV{$yeg?8Qplax(9O<Dr{k)n3 ziO#cdW?FzNUks^jpP(2S;$u3rR#YI7zS5@1wS`3{t9L+D>}SYoUXslok?Km)=apuH zQ`yN981T_lPMBPrAyu<8OO+erN|P6H3TT##<)>yogMgs{CA7_Lv%p6&Yr$Xbn<|i? zItxfb8F<^rXbV-0L^{0%xv4}?dR+X|;Vwxpo~Xt3DAbk)#8KN(Vt&}n7TMmiJg|*r z@GpU(&YLG_=BFw4FwiUJ)|^x44GH4H-nREuAVvZ7vx_0=t;<ThiR7425#8`sp2!Dl z(6P@c_VMOp5YDmiNo`6E<1Oyv?d^jmr+@+&;1f$}3NcxHWSsFj>4jCCY3V;W)2y%D zYxoYF+01B7Ua&xynH0{XS~XE9oPn&SyE8=!Yl`JcoP9Xy=ld$>0a1iCbD+&>vN->R z?)s`N&JVC9-R1>~65694Nwn^hEYJXZnR&o2c6(rhhUqu>DF`{wksWYh4TuS2F(GNW z7|p1IwzPkaJ@5O?w=v>eS{`n))6O>0bmwjzE@E%@5pL*o5gs03>E~Ma^a`Gy?w)Ed z{NvqIooIcgyVU7LI{b!eI@EiEdrF@50{0M?{5)Wi`7qGZ%N;=4wcf4qh~+Nkpgdms z+zx=^Kr1lePCYV`^xPIT<F{rZrv~)!Q6?fCiUDZ?ptv}Z9J1Ou<yBb~n!IkZOIt$3 zf_1lGqTx?{8$QEx)S84*3nFTMDP-bLJJZHXzb=Ep_1Cli7qYsJ@es8CM63IfMtEYO zuXPAoA?~q~<xFXm;er{V46TL?oCMqOv#d+BBwqvXLSoCbm3$Qrpi-2E$>jEA7COhf z(t8}sX;CfmA(rB_2b=rQVGnT{v4m7JbuCS8cT{l_bH?Jwc6S2jd?Fh+!=sNVaQXzd za%ET=sgo2**yC<g`aHe2WT;QSD1tsGolMYYMxRU6=aRC|(_EkFP@ldt-*vywib}w| zqs^A`-!c4mtk|3}oYP&ZpQrFJ?2=)xz<6el)k_0-Ij+Qumv&%W3yjMIBdtski7v~R zZ=2~JUo<q+J%QeqgP`LwJ|O`vP89QKdaWp;n#+2R8N$a3`Z~c3xU|Iy!Yz$hv>E4z zNd<q(yBAh_To8Z+PnpzVX~o0D5|ELGS)QGkt&l1|F?);{ORARv9S`DMDwb7y>;xi+ z#^`er6PtWezF%=Hr?W3*bR5(so=VA3&tvsK3kK#Kz*@{^^ZMN0MAiOGIQui<?9YU= zKNC<2n>_(-U`aTNpQm+MdCh>G{qajayd$qe?Az*{wVJ|;w53~0=AUNnX*T#Ybx)bq zvt?)E1fs4ldP9RAUd1I2*nMH&ds5=3?w*ryXTNsM8#CR&{&)^}>P~0aqvd*pO8td3 zTUQZu3G`LMo_*X0N)Li%oSl)UciJLUe1i?PFjd=4r-TY7QKZk=GBo9;#c5yz=rL`s zMWJ#htbm_4_uiOw`xOsPKXLQ2gfDP;9}#D{7^JyNq)((nh*I!KP$AqcW3OPTLg*~h z7CiNOviQkXT(9ClQ>em{SJ+65YCf1TYA0)T$6;=ig;yaR>5dKzjV3|CNQJbG3;SJW ze9EAdPU~_>+2!)GOKB~>2qUFLh?NRfG$Rtp*D1Ok`s~2V<FpR3%f2zgZQPggp7U`# zu==><K~aLF!Bb7*z<xr+V8YOm?iVVJ`|E39P@@T&vImbE`X{4R&RgOE-co-uKy`|L zS}eV)H(nKuaVHw%PBg}yXpB42(2jXf4Uqgyb}g{;{jsM&u9@ph@?xSvF=KZfPt_L< zp6VQ!tX@b!y&pEbvl9J_>l4rQI-<RZOl{OiUB|g!c8WVvRK%*{L_!Hx*dvFKg_6sC zNlGCUrITi*@FDH^>c`6nab*Y9>I68hU~!!cM|KVtU*(>m9(8^;&v0$lauq@%9h;rL zs1zz*20dKB!Y$`Sq{>0rAZdeZi*Uw-$|plPDBBB_>v15Qu`d461jx>IcZg~8Wq|Uy z1wtE-FBq>@3?4MWOFXIbAA71;=M9Fy$}&WYA8a-ziJ4HYSyiu}q}wWLgF06`re@pM z%n&Myoz#t~NgOT|h&x$+jpD|SL}@4KzLG_gBL@h)PI|Q}7uFmX)*Khs92eU63To;P z^l(Cl(ZB^&AW&RSoFFdb4wg4nB~~Z{1T77=7~B<ep=`j4;b=;ygV|f+1Y2y}<|Lf- zA4SpjJL7)hO@2}2P%jKs|EXnBol(1ka>1C6unx4nsaA*20{6~oP)d8*0S?TxtyK$h zdYmUT@Q@CbP|72h-CMK-;gYgD<VxJpw3e}kQKz7gjW<5sU2(ACnYw}vv^@dizDj>4 z(_hj&0jzs>vNk4IP1Nc+U{A-X6nO=tiQMfO0Tofu!%Nd49l%OWEefpSVRsTK0~Tvp zPY8v<X!!APV|{c$4$xv=EajlHFErBh-vzHm-8Y>&kEp)}tnF`$mDo?hu96Ijg09gW zrYC}?k(7g8geP5RTr=7*{TkS|loHqfT)F<HuG?4q2d3(apx3QhQ0xm&YyE^gje?~Y z<mFG)5HvVu^<63`B{c=8?GqV}pYr-AH{IWI2Sv3zZm9nR>jlNlKif^E&i7rNV||^< zn|;?Fp2x*MZm_lct%3goTswdk?lN$z<`*tj|8IT>TBVQ)_aSLqmoVk3ztTSo+fVMh zNa#K<m&O(p)cOLrJAIoVp%J*-qLNPfc0K8enF~B-LHk{y{|cgHI!g{^;h{Yo{gfTJ z0WfMekevdL^Ot7vR;7oNabU5@9@Jt?Z(8T&Sl+I}w)<e5H5RD?*6Vgrf^@$O6t{P} zTX~BrL7Be#QW*gBii)<?4G;5BNhRw!X#erR@M}&j;rJo{HemUC<q@B9seFo!-nlF1 za;Q<@q5a&xssp1w0(678#ZIi!sZ|dJmlfAPeJJ=7U`I65FF8DKbw?#xk|9dJXb-U! zN`jezv4N4M@B`*)zxS6wgNip;2B1zB`fKVmX~^j(;9Upoar#NQz>b{B$`=Z+*lWk_ z(3v0;ocMr`-fY)<{<oJMhHrU=gYFjCNwU_?cr*s>SBCw3637VM3kVTKQ^s1Hp9qJg z{zNB@5u+PVWf(xMRLY;t;-&lqW~*{G!|lpnH9w!Qtj8FKI7lm6_0c@-X2HN|j^cP{ zxGM87sL0CfNYvgwK%a^wGP?#qs7QCfl-moTB#!1DWfpnG2n89-CQRMBL%@w!6ogzS zVo8;j>~L<M9({H&Y-XYL=%vdg>qOu}x)^13u`_kbM$dnQV|Y}MzqU5eOUX)%6Ls=t zjnvpWwizo(;J8!5spkv-Dax!NK;O}_Js2R#zt&&lClBz{vpB**U-w~Mo_*Xbzm_8o zYx;_DjU`k`{z`6}wXw0W`dFiBdxfQvdPon15d=~C8Z#?;fU=*-ds@K8%!MPNIL|E} zJX*bbY#+>EMLSLCXcbRg*rA<!bF_NRh4t(lnxob0EUZ@p|E8;tgcvqa4kN}6kvm#{ ztS%j{r^By~+8+yzhU-}}KZIs)5idEdP7vZG*kbw{+zUnc1@ALG^y2aU=c%#*73+9T zDut;b{c!Xk)3kaljlDIGWqG`rpa-cBg*`mdhMx2<)t?qMI^&_^#mmK$m@tlLMf5q8 z_(f)S@YB}>q0Svst|bq5&72wrD_^X4EMpk+4`y%8n>uHevXLI05(cw4`P`yc+%4Gy z?e+(T;P*q)gjwzPL*-I<05(Q|$eU9`4&2rHRjwvns4_z0%qOqq^Os&^+pzNaoB2Wk z4fBXRwSV@r_^GH=Hou@l=@M^Nv;Bq;M*w|>T-XeB5`n!y0bFmmLZofK6csh)e-Bbv zqcR6!naZ?nQq{6FWCee$WtdhlV#kcMYmzn39&dS4H6JOd*eCBv)yml9*%Y0|Je`QB zL?>}+pDdnEW8@DU;T)Z;Y*Y|NS+pvhI8u0kPR)l!r*TLMk;wYbA1;vo&`)7SY`z^~ zfy^Nr;?{;Aw~pmw89>fQt_;n3$tGDeS%Z03p^GOhk72z`{vBIM$JSY8te$XDJ|E6X z)?l%`eo20+e-LI(^2~rRNOxsOnw}5Bv&R!~)Q6^CLc@2ChN02EAi+@APRXK)^r?`) zseY$syIS~F)4CJ5O(|?`1k78=LLZ1(SRb3tx8nvn4$6)dg$$uV_7#P$RsJ3gY~&Wp z9dk+kpc}g;r|G7T41`xe6%VLS`A+|Vwi^XnraPMb&D4Z0xG8CMRP4}2g;wHEPpkSc ztw_@j(kjSdi1aT+d*EWPx!{ewNS!c3K+QG{L-=seSb<Pz^biKa5PAfb++_G-cCISw zqu_ctwl7;?`?R*2rj3ybn!&*N{Jn4xWIIpH6}vU*9KtNa!gm7<JyO#wo2XLXU?r?q zD%^ioFTegpx8k~{H_2Oop`?Bmd4)%|92!U)E7L<^`Mb`OCFt2O;NM05dE_DpS)hj- zJ$*3l`^s3g(6{6lM)lpTu-uHISl+BA1H_?@rnFk-xL#a3Sh07o;@mb-#B#9Y$vX$j zl)X7vQ5c4iZQ_ibi{&0ftpB0Wsx^)K)-)be(RkcqOSh_=pTe3tODbz_F0`a4#&MT^ zu6g~8126q76@ZcU>e%aNeGbCwXI@e+DsPDqyl%$qqP<NuBgmLgbqXXS0pg9>PTMCY zirV5a;=IKez&C^stwEJ8N!aItsQoK?`7G7WXkVUB-_!XO8}$nnh8ulp64pp4^#36{ zig<eF8Y#f(N*OZU$c7cMREB|O!AW|zlo2XS<>D!XDf~lr7Py1!p=RDv5LRo$G5P%S z_u=ob)`$f{7^oLrdE;Hd+}5lyo6kS*`TOui?1$9uk=l_==kY~bnGdb!V`ppeSk}N? zG&%Ok^?=VJoqDN#7-i9Jw7aiHA#$_^S6y(C-Km#w)fd1;8iMr{KoGBF)bl}p<qHSY z1q-#9>@T2K`z+LwZ|JaKp(w!hi)bv>B3Ru%PGNZtSmuHJFx$yvcxMgI3<n)U3=~(l z;d-+^&eBQ)tfE^nM!OAWI_QB1%5N4kOoUWhVJTKz;nS8wxqe_MO63)K7E26ShNTzN zZ;~%7KVE2i@-*)YJK@ckYb|-^%Fn&T?cWFhI{-k-I*Fx%^c`ZFtdeqSx6h(S$9?8f z9_7RetwmFOIecJ6`y+#iLqF{WvKneUtHF#?+2UTvh}wdS2wY)jv3r0P!AHlPdh!Z5 zv?(tG1U|2M_6p(gtQ9>?7d7=#Iw}2(#zC`8yiih+CRZIzteIn`uIvi3==>C9*_Huw zgdk3vE#Z)}Yi~4@<8*ub#(32CD?M@qWe=0HtOxCsBdO`sYpz&i4KMH&7v&)Uo*YS2 zITAEu#%dCuBeAJUIHG5284dzyxGvHL7Qq5$z=vC_%9A{_NezbBU~?8_!6X?Er9j-? zXu~S@B>{*=eli>FRE1`!>N=q=r~=3ebfKVY2s$CSQ9{R%`W4~)5<0T8BcO9>LHt~) zCYDVKEL*G`>1fsw75c*o-EbJ~aYri2P>be`VCGcxDfPbwQXzv@zDSb_4<;FDPv{NK zUh!~E|Gyo955Vew6esveo*_<vmyHX4Im`tS&jroH<N?RVR;ACY9qs89%jOZfjuq0@ z)p@KoPC~6kYvki{Q5v~62I8PZ+7vc|zB)OpPchvsMqf<QBj&XVlNo$?PZ)#6Kv5dQ zm`CArq@#6504%j`?y~cB`<(T{K|K~b)qop-vWjvVCI&3eD0zf?6^iV)7b-LXj9GTQ zVCLg+IG~OSQ1JhxgZM!QNyMMLf~88_cATz_#TAih49nj`ZE_45Hb)Xc-+UDq<Hw>n zbg7n1!$Ab+Sq+=#jhKbmP~4nrPWdtfN<Fzr3;S{atBV)DkTYJWQ+E4(DUNH^BVhU& zSI3PKC5?q2uTL?uf>Ij^83oTf()gxX`MMW+&@-?pUpx`LDOS1jnFaKIkcC+^9i$(T ziO`z=So<aPH1<0IK-TnLeIypEfzd>n(N&WBy%@MQqDooE4S8W5YMe83X!@b>vTl?5 zxL!`x_JJa`s(qznc?;R1NOSsifA!5z>Aw^C@<#QRW!~hc_=YRsl2|V=K#|rSCT`Tp zZU}|*?W~54kwdxM7%gys7;cQC;WgU}VyJYdvz-8h@q(dgGGoM8Qjjsd*G;DLfUk`F zUfi5U!J7sKVO8q3KQpK#J$H&c{eM^<NQjh(vC>bQ^PvZFVC9h1wNIQ;J=M}KJW`?# zCz2#j0NxZAX~_%nj+bB#l{q=N+h)%riDmj$qcl(=4WvxuMZhwQ^lw~;WkrA}qWGT9 znvtULZn=1wAQz-4wIh$|XpMn6p&UijpX~N^0?nQ2758|2+E*ZFP*-SMU{hz(gNo54 zTLS~J$k-p=m|yR99Dl&a#qvsvSD$mQijcYb>OG6Ux^3`PUtHS<M+Uqirj(1eikZs6 zDQ0NWHi<M%@ef55nV7P0OGc$;Mg?*>`&G*y=bo|(NOp$znJi1qww?<=+H$Takr|@} zo14swjtqs%Fb1~U3B*fIpdXsT{e$v?4z4SJsmtG~72X)%aW1{FN_FfJXO6%Ph~60i ztL5-#%Aj>VQVQ6;K{Cx3qX5p!0w|HOk>KkwOL-0%o7!Aw33<`AifAO_1x2;)_!`4U zncIlk4~4{vD+z$+gS<a|r?LNsQpg<&FhC(=+R|TzAfqT`SXxkWaRhk(=LmC!*VX!G z3hB!*Bb83c4aT+fk8y!33N&@RIjQCDM2ni#ivnjm(W2%~v_LeOkXg&mT8mNAgCZXD zz>23S>rT3;*<nryUUAZe880KFk5gagRuU8^E+tAm=X`NxWXrUpBQTUG)(Heqzq+~2 z^hHYxv}B%GnCW;V(XqF2-X7Ve1v@gRRhZuBU@FdbO6V{a!;gT;vVKMV7;Ra64vEF@ z*id~Ai4YW>`eBbAv4J~?#JL;lH_lhh@Km!p{<I%#%_KH~pH+FJb3|yy7CdTgFe>ph z>hSHh5dfJv=kOquulp_%)X>(C>Al+XOCy}x<}|BL7^(nfTvv>wIP2q@A?oN%4+_!@ zCd*B}Nc<PVq16sS`g%8Lp`Uq#RVJ4>Js%qMAboJ+xDx4h-`H`-VClndD4U8?DeI%9 zN*343>~V#rWtcW~WPePg+=tyTi%I~HS&(r-{mHVRhpc&YN%3tri!DEB@Pd`Ti^fhe zsUZ3&HmcoqOneM3TY2`GmMn&DY>ub#8#R9~u_8&O9~NQI=KKc^lfli$(oe`%x#3p- znv(~S@HT%9zTxMw%9=njosAOe>P&J={7mWCH&xZ@JIfSg4Ln%QKoj84oE!xC)P`|8 znOXNZ3VZdihgWM8*;M3{L1v6vhkI=m*4q2WUBm&q3a0G+bBp(#;{6i$p1Qlut8849 z*LSf9{|Oh(a^^0NxoZ#HMRsR$%C^&*wG1Lcv<;aCH}NjaNd4q{>_PtAe77FWCueJO zU2ez3Xb^P_5T>mz+XxPx;!v?c?zs>%?hrGfpVPDs$`H1;w<K$@#yA_djlsxMFU7&& zKVkx`79WLSkv>0@-D6lR7ccJ~>oI$*R^86@TE86x2d2Zz&XLE{plrRnMEtXqHu&%C z70QyeT{c@a0o{(o^6%-TosqG{;=*%hjkhv*QD(h`GXQmANxm#kB{E@AHilQHc~*2A z#&$_|%+@Y{x=S@=LQM@}xSoY6!}ZdwG^-{VJhj1pCu?|xd*7hHge&M@{Aln+fj8L8 zB^L>y@X=VzV&75&WovuKl(>a!h1(Xm<v&KfgrT*B8x~tH1(p2RK?9TB<3{2+t>U@f zJw`ZQ1_8RFc@#|<ZZ-HGiL$I^7+oS%ikL@5<uTcDTWJ&31tK(e3^9a$_GdWsk3i!w zdS`Gb6S^3h3KW1QWnkSzd|hGZ)^0N&vyD|_%oa*-GZ;|FT}$Kfw1;KbO+(HJ2ICs% zR|3sa_E1ww;M~Od!+W1HUNgyb(n*mve8Wn4AxfosVuEU*2kC8&%{R<!^^>W^k2D2# zZ%k@J?Q#MRa5HnvyRDuXUg&gHGySA?xjL<{sEcxrSLs&su*_V0<`-Q0=Y+rw2Cb{X zp#0RQ%rGM@y46IR`=<hmu@X}NR%m!E))KRwFxA<ZLz{Z9!D<BVe7@VkR{;$9D*J)^ zZO|aK=o*x%M7N`Xdl+bWYp*pCHJL*)d~|?;Jf$|4H^K&0^gy9rhjHrwgWo!<2Y(XU zLN>M}P+M7lth;TlW<OKp@Op?CwRfzoDzwRZQK{F-*>cXUaOxz5Z4I65*c%{Z)<GSD zE)c}ntU=JN#(djNhaU2jG{Rxyt`RL2Xt6(N%n2Sfl|Ied1pTsqtVyFikfD&#&O~R) zb`?vyg=hOHq3$3`)GtP)j}kr5YHZY41to}Uff7W(p+vI*M2!w5jRBN!=!>BQ^a&-7 zj0_5y1T%&bw945Wg2sSoxM?V{Z|FIcsO^D93MQ2<GL#6>=pt&`SDKI);e+kPI&+L` zX;wllkBujd7oVDo@*A87v=*@Txfxi0Ocv$gysTNMRX0iTDIbRFCCD#S>)=atJkE%@ zPZWbRzS!pmiecr<chDXMt~6`Gsz>{tyWETP@8qmV?Xm<>KWxAoF)!l2c5CO0GX#se zQTaKE9kyekIpG<1Egwl>(L=daCySVh8Jtx?luCGS8-Ujkg8K4<dC%ZWtv8~h;-y2S z$JR%~{zUR%)CRS2pGHU4LY=uN3Z$7F^AigFMVEx*#!MCg1D%DiiX%saQOFm4)pAY` zBX+lHc;?80t=zS9yUeMqM09GY%;ZJwwbaA2(SfUlgKwKB*>tTIMIehY{BHVAdlMOY zuX}J93Dq=IikiE)^$!gWLV(I*61{ROZK;4pMT^NR;W1Oe^5)&iHS|V3B~$psl1`Z3 z9q#GJ#IbW#UC)eJofhBC4fNey^~t%h1PfV0dP{7|l3T6C0T%b1ui(0K?u+LfR;}`S zF4c;@@$n!}`$!7f`=f#Qyyjq$$^rMd2o2L5I%h-(L;dQCj^)C`ciSk(bL}zJif8+d zCVr8Ul!4XM5@$KfR5F3Ehh%ygr!_^|mMutkC_f<aDuMwxi8C@(n1Y%wqHw6CR7GJK zS-WL_^2b!$k)f`w>Oc!QY_Y!DRJu%G;<Cul&-DmCATGG2&FcHG2#?yYjUpJKLZM`k zLJlkdW8}+RUQq6`ZrNbwCEyhKyR}^B&1mLU=g&Ylgg=^Qoq_^BGK@%gOn~}PQyki_ z#1V$LMEy6nm2WgA*a%!pCAe%bUMRUv?Bz66eQvKY9sJ+p!E^*o8g>*y7JuLtV|m4S z^LzK%!3Yamkv$gQ#A*!Q%3^KwKsQ+aw6b;QrJPD$N#=NFVHk!8%K?L5pWA{9c%1f{ zLJjJED?B0$$uw%eAnWtEk`e8AABCreJe?iXo3KRml3X7mj;1ml0$NjsTe#7p%6?+J zNNPe;+rE>?wI#lA$KJR#f`(~$$M6aAMZzqezN$!WKaJ+&ey*ES@7aY+avLrSNug=k z6uz$j#&1-Hd%|b9G10_Z@~WY)xnSZX*)D;R-XZ#lq#(c&!}2+8X=u&OMI3Q3^j@@9 zPd}!n<gS)X`^mv{wkJmpdjW!xgcYUOSR)CdW8@f9U}r03z<}t%T1T0Cxe&~s5#JdT zFw@GYt*#-9!p%%|X<FV*&k@>MAjoiSxFer%(L}T%a=9FT16AyE(2_)l8JZ1ad29`X z#JAGc9E9Z922t=8oqv+)h~O!7(9AV6!5kyeL*6iA(e*ZSnotg$JEPo4i9^0yl78Ii z0mBcXe>8BiK#ZF(7h*QI;@my%;#`t+>73}92T$TB*)XS113@avXrg+k%!aZ|35ZnI z0%HCy#s}2nsR^gcFg_wzsa9;qfNisLTMDp<Ol7zCa@d*yX?4u&NPEmpKqg2l4GLm4 zKLLrMi$9YKuMP$Q2hb4@1J2HBm{|;qA_#w4RE7Bx2d@@VP|cXzNmBdjC^2(}N$VT5 zLNCN0D13Q=$iOg)pr^<87{~xL^7lrt3Gmg=4E+B#Dw0r2B;w)0JHNgEhoAb|-s|A! zB!3UwI8^^V(Uavqu6zk3LB+5qdFHobP3~J0$Zgi7u6V4LvrpXI1uly@LGd4hq|}>x z_1AeA9^Cs3aI;u>B|oeZ$}y{7okc6K$50KU1CI>Zlv|Yg*2{uGn)@_5)#Xc=Hi0z6 zDK2BehF&6IRfvTwFkw_HPsB2zs^=J%nAdePEo$a|B!Dpvkp^NRI<z7B3MJ6mmPlkc z6xGtkiUXuIbEKSLnNh<BtPR@0jO9HTX7Fbta@?vtIHq}u^(Dr&1=o3Tjs*#Jeqh(l zVh9J{+HjQxo;*Q$F<&Cz#!iC+2L1U$zGpjw5KSU$WK%(0*;Eh=c{GPL%p$>S6CK`R z1=D1D<%AVWTl^F0HVLJB5ww@c&BU8yb-Sq0exmy#{X%f84`D=rIgmU{(T{J9rVzZd z)&BQtQC;~?xTOEjxv7-49H(pz#YGOelSoc6(4Jn^+&pD8<x;e@JoEF=HdxItW5D1` z^atz98K<%)8B?EnAXm1Bd!=Mt&8_Kkn<5%rB**eViv4<VPG2-+eJLKrdO`&mOWGvi zfmI}LObx%<PfPuh{LQx=ctylgzhrPpoz~zD6lasq&ENleZFEVboI$emuNSLREeF{> z^CU78I=)N-=DH+{EI5lU<KZrAt{6k`T`erZxY*%A$cRkiSYQS;PQ;ilf5tAo2FnkT zSJWJ#0r-IspedHK3_s}yoUE31o)#GTueX2tp<n*|r{DYPhacLrn+f%MKm5?Y>;BJo z@8eFO-2UR%zT=*tT1;>D?!RnW%OVe4aq#f`-phJT&f}1zH~*g-^7s-u=3_vR)?~er zUwRP)G+#3PTeC(y2U9D#p63K=7cBZoT%b3Sh9W7l?w0=+B#lK<^nuwAMuYh&Di{yY zs#VY@A|y+h=I_B&9Sk&FJco<yI0<aPu=t_^90Uo%_Wv*|XD?tp<8apgm}~_)6`q2j z>B>a#X+4If;q@!NrXi*<U^)%9=)C<GQ5;t%4f-lB$2;OkF<3o<*$-tATTq*R3u^O* z9^A_R{1#LsLvvp^oMWzd87SH5IX+M#D@HUU=}rjdEKpKfUl9y`O;8bHg-bL+#|oEv z4T&+vFvjYD5gld<PRf8s0R<)WHu76tMMA&DRgsX8R0dUMqO^zAPT2P88tE|{FN>16 z8V-z3pn+m`3?*nb6p-8h?|^ZL`o=+{1mS`{mYEZaXJUA0kRBGJ*>VM>j;wrW1<TE} z*1APFs#o~A@Fjx=F~xk(YFqwEqf%{-D<U7BL*9OP-fk~*JYp=v%+j}v&%ZV}*%kI- zH!($t`(+<uM7M;1StIcixWy%nrd-EPqdL|Oco>WM#B0S;4|y7Xt+(PmjmcJ2K!M6~ z04IVs3bdYbL*}yDQg}m0wGm7UAb1JA(q(LlWflXek1vFv$U9_f%RSizpJ_R4kUy7g zkzo)75R+DwP|$8(yRy8N2msE$CcxOdAYL<~WXegTZ*-JoB?)_fC0E5~CQo+Ybr~(i z-G<+SB^XftlUE;R&W%@-GwC~&n{yi@LO-hOikaSzyLa?j-3|Pox;Ocqml>!rtMo_j zO>V(n>F0!rc7scDM(w9X;hvb24e}TZ6yDN{ruY6yFJ6(u!NqRv#%1U95^{ZD!ws6% z9`m<aSGD{ycM)q1@eACw$_DMLj;XWgnNO2NdvI)u<ivxdsl4#Pcx#mO32y1d{jShA z2;cM*4%~oIDi}b&JUs2d^g~N7>($-q)5$p~8X-GNrfZk?lIdXjBX{5X{jXg1;-Bwk zZQ2h-58h{bA56dZoj-rzmcuu^|Bj&Y4u*jrCgp9cj@RyHk}o+I{kb|meK*O8GcM2S z)Aa-Utm#%!E&tGSyP+}|{G)#+{Pc$)X8LmJS^aM>-}XKC&~Ey5_w)0IgFoWLEG7Y` zU-6oo@A>JyU%&PV`-zy6Kh-%}35M`bd(9PDgWj^{q20vmyA3C=d&|GP^Bdux;?Stm zcnXw}(iVQ04~S{~YAf(R%sL@yfOK_{At>M#bj%)4U$Q5Q)oSp^<Tc_O`1CE(Swh&{ zhrX}hkbl}%R_}lFvD-9Aunz9rQQ!07jHiA(KYWX*pT9%e3)3=_FT1!0VaMEdx0ALr zkD;@~I_i0Rnz@4f-VYkTXky`{ZChPA!XJcD<SdEBhd?=HiHE=m@{1Q?{^~FPps~&V z&gAJO3S65UOWsw=6+aJTXqX4W-H<YNB)p0nS^+mg{N)Vf2?t0h@yiWgXx(G?*qkW8 zR~!22u#vS7T|NNMgS;uy(Kj8oL0`|HxAxjNDETjNm%@v*vm$F>!o=pG=MF$^M~w@v zOC5uqS7ERV7H2R3I;?Kd&$vQc4;^3_;elSA#-~dzEZIgzwtns`5g8plO@GdzypCJ2 z{{_2+kKH#gCB&-@`UC6!v7YGYofh+~q%p*I?L)g=s0i4LkopKzo?zq;o67eVRED`K zRF07YL`3uON1`|5_4K~GO7CwSiQY`aSLn^zjf>FxFmHYZdLN`Z7``)f1iuN8N|kam zH;d_%lGNEaObd%jA;^nJ{uIjQre<yo)%3v->Q)tvQI{H3O*24JWk|v~t*)TZN4{|C zJWz-=P^9tn2fzQm)AmeH9+-acT|Ds`;^&ilrjJF{(&U3nE}OpQq1eCpr(n-?>%jEO z=RY+6;PVbQE}Q;I=--@vISA!TP?t?lUcOJ4^S|Kra(g;I|1(-?UcOH+ng;|a54c{g zYs%?4AgDT5rytn2Z=YoZ6X5h3u*cEUOQiYmC~Xvbo(uH$vCliiLgD4p;lAhGuobE7 zcBXH-_we&ByZj|rt#$e1TE13cX)V9$p5XTb1(U>B_Em@fx(GmFm!sI-jdMmR#Z`_{ zaTtdT)}#qJ>JTe<Mv#))jn@ZFe(IIl>+zkIGBG^1!V1fRFyoAjv1#-;%X_W~!me^o zrx2{uB<tKj5{1P%Pe#*bA*IQK)#f-)0OLRQBz4LPy2)+%v6rCs)w^<<uzxN8MxfWu zR^+d=RRBy8-n?g<Ub=maqCzvj|MNk9|Ci)|?tRh<J>pnj5yq!6+pI6jLgh`B_fs-n zi8kfB0EY!rPW#)Wj7_bS-(0ZCn|y5+{-TYm=!b<EIHEIjKPRMSvu0Thu3%K8P3;gs zjh{BgTpL-a#V|sUFvBFbgA$Mwr&)(+)6gv#BWX5JR3_%ZaAuW6l+o&h>!fgYiQ;1S z0w-H4pUBMIp3J6Q6)?EjC?ZBpVKFbgjdDC^sRP9slnyu(Cn%lA;8<D5XAT9D*4Wt^ zY&=_Dr+27-SfmV8h*RyLCB<ZczV5)eNyP*(bwU@H_1qMpn?l2*GXLLg+@udWCEh_d z75nCX*V^>tzy5W%;wa#~k9*g#8>DYAGz7VOXA^>LciNUBF8AH!bwq0#WK&*<jn+a9 zhm??+mMk*D@T<3h9~*Ax=j;7Q0_1vMGn#rh|18d!I3@_~=rvATX|+4!6HC&iV2yqd z6VX3f`BIGlYPdmmEpiZ6s$s)lzDC{xl;^OqB5dH3T{L5Xi<)`a!)WI9BbwnX^J+6M zf0WHw`b4qO@3BoIT6x98Xr_#XDuZGzH_#M8@XA*_jAmXuVo)5Cp+UK$XQP#`d>GAK z?!uh`6r&SE&wDSjXe9UNl@YZeCB?fdhu^U<sCaj+d)IL%lTi=TS-g9zdpA+OW3sw< zce8uP5{k=U59+Z(s(5$Uy^G6tOj{Q3KI+~j<vT*};@!vGyFM)VtHry|x_3*;dL&$m zcVBn!#>;n6|K0c8yZ#u6145&q{*>TDk5W-&LHpk-5&EE9iKByx$9Y-di~Q^%^S+~% zYzQ!U5rvVHQf#X7a%lfTc~SbnwmnkOCgBz|g>ons+64J+mS7;1UC@>#s7MGZy;Pv+ z6(3QDdE`*U!~hCd`)Bj8D{-%m{c)&ARQ)MuNe3jHG-3*vN9n&@#>m<?4DK253f*ZO zT4JUT0b}zh8eVA$Y3=+J)0uaxzA0)p;1=#T1{xD$J6>yw%9Q*#<*c3#kMlFS4H@xT zMz#%}#xod(3vKL}kjdZvK<d@g{C?@Uz;mP$PsRg`6j64ytHtLXw#NLM<Ao83f~{G~ z5at8^0}++;{a*Ny!$PlTr}hdec)9dsLUlXE*d7bg7d)TpLHiE}ba&kuM%g@P`Ya)e zy9fS!TX()#_Jy369afA%1Q)gRO~Pu>{tdN8vSU8rjE^sU%;5PRaO8KsOtsQ?x?+b0 zIlJb1jo>WoIO$tuZgc(>*}P0MazYd7FDEvX(cWsxXA#?{BkiqLTAeM`Jjp^m6KLu8 zlzO3Ma;3Z`scczO{V8*W_CyxGNN{4-m4Ypji|zM?EcOWZz^47AZ|zVP7fD|%Ka8eH zC^{nj8xx@`{hKJ`SiXs-^v}(v$U6wPfw|ZxYL#i_B^mOJv~oXHt0CMjd}@ccQoX$Q zlad6_T+zHs)%qN*O>#jnoP_pcG;mAg`F-sq8^0|ING+QiVkk9dN<1jv%A56Hscx^@ zcph#L*h9^?d6fP{v<cd0C{KK)1_^<?;mTLrc#Y6SMUiyHQe!M$&Ijp3uBtWjGk1OC zA~{?24c`C&gz?)H)RB0H2bYN0@C*nm71dQQ5w|*^sm;-EeBw@+983Srbtcc48eXre zDS0PqCx6dTE5AG%<&DCwR8QY10!35$CoKi!Zf&|;OP?U(l7q+6f0G2;%Ibkrqmjma zYNLmbDVBw^oQElq@%|;Ntw_MhW7Vq4$T`u*|F)<{C~?w22<sLTz$6L<OAet`#Cngg zR%n@TnP$K;>AZ1kT!31y3;bY~C~B}*I(_i+dVd3yc4pMP8bU~P1Y}LfJxPFaSfoQu z|1V>Dtq`HLKofBkGWi)zt$dAVCzcp$_XN_+F|@V(GeM!oK6Kk*Da#)Q>3-F=0_lSy zuAB(BPARKz7*AYNBo#CbV<$7%@>8?}C_)=AtFR>?a!$}6@WWcXGabNF{J78GobH<v zD>{I<Y7VwwD-m47%Lf}a1qPgAiI8;f$+QbKkPDrcSzE~{pm~^IqhEILW|z~)<jkRc z`bvO`S`bjYSY<CxrwGa1Dqik--C?o5@Ks`lU0Dt@xJmtvuB~=-(j)HZJ69i}BYdvn zbksroFp|YE%m=PC9^k%A6m1vS*1V8O5%YvFb<Vn=(A%<`ELJiGkDT&dz$dS{LtwIf zuEYi%IBeMbW0>w!Z*&LmNF4JYJI;b%^LtDcq@Q(MUY5A^PHXei;<~F|UNww`Ui(Y~ zi&HBy`1d9YqAD{Zz$|~RhnS@<B-TNAJdv*wv+~am^DY}Eg4rX1UyS`#A2lH4%COa} z`9~|dOej$+9#PBEKJwNSgZoB{`YT_nanpE~6m2N3h;xE-Sd+C1rCNQhb!m@n0X}kC zROSF*CB1#y-zossG`w<ie^e~)&fP1FJh_?OJ@{I~VxbMaBNp2JUdNQnq_th+THk8H zB)|IlBcJ&R^7({VHj4<r<Ac&r^BC1@5+_65FbjUyW??ngu^g_s3T^niSPZ-(Q-OUE z>(%!48tdabX#)_dGPbYB(#XR+cQL2QqdeS0`+7{G-K+}eGu$d30NG7~;vs_F8}){) z<Be$e_3>RkGYU)s<|-}Kxm!i=($eAlD$}@J+~I^N%5TENyV#c+Yl6F=s8f~B?}()( z`HyK;&y7+3s*e~W9HK&gz#1&vG>E9;I1hS9mw7Vl90+ND1M@Be^G2Ppp?_I&gQ}p0 zm<&Zdx~?RF2l=v8{yY}{2zsEDaV3EcelzfmH^wRTOZSPD_-`KCtO;&P)V0R_p2Bgl zDaQQ93M5z|D(9=j_M)7uB(<}WDvk<SiFA)^A+TV#D)zE)2#UK53Mr1y6;LQvxi^_v zWss7;`Hsv+fP66E`68G7phKnK$38U6{2gh8_8ZB1;8(7+cJizga!ECWGvS7rohQ`u zr{Kd;R|!MnDDgk(E9rPm%j!-HWsL5axoQp~y<wSGIX!&E{9)<(h)R8?$;+MDVfzK~ zXaK#KK4@a<PFYhTO}Uzt2MXYPQcVFMD!^$1vAly<q6i71{Beav{!3?hBqUzf+V)P< zwdwPuWS#pXK1*Mvd$F3Wf=3gpnrn?Wyn3VlBI@b$RnlI2wM{s<Na1i{VT*^0mQBOw z2c6=4Ls&uyzy>8=wb>FU9S>v3`>#6e2ry#Eg0dim;<YO^j-dUaC{`>|PTj*bKKS)G zLRYctgiw|FIrq6H0Aaf_dXOXNz}?E%2{p1CELv8atPCiK`nkARpfkJ2L1yx}^oBot zhsIudAxF$5W@`es1p(w+xlm%hs>HaS^)X&8QJ}d}qO>Y+f2qu&{B5b*!1<oGH^8;v zIj^LF^;wnJexs%98u#(|vcVW3a3bs*{k~)8P<_L(Zr=o~b4IX)Ax9QHx}x0(Sr3>V zS$VZ^$-6h+ei*p(TXp?_v-%G>tx(jUd=XGm#`V=cZy@?k95|BDGXmCSu6(^(H`9?Y z_3FeICF9{e<nlKO;+sWP98{L$UlsQ~YgT!Lt_K9C8;Oj3gx7+EV$ajt4F&QbLO8=o znN}1-+cPktp%LB%I^rPRCh4n9!a_5}d}q=_cc(QaTmmuJY{OBRUFlr+lkBR))pGHq zV8LX&s-zBFWlw?pMn@R=aWn?*E$Xm^rDi|!Qwtq(ld(@aFNjK4#fXWW(MLklEA*<z z$_r_WyDP=B*fCo)(0NpJj*lg-h!u)xK*-jlctoF@u~-K?MG;`%%b>&^mTU;IOj@4v z3Uw>}u-2Om4v4SHaO6pkuFt~s+hQFBVHI5uxJjYgjpBXHdH)DC!cd*&^pW!W!+w7x z6AM<84poL7mr3b#r#$3E6=v)v=VCDO(nKU8>QUT^q~ca2M{q0n!e7Oupj+HICPgH> zg|xIU8i3qm%5x0{Pbw_Tf<F{L7;oTmN^jxL>c1ziAk*-2*;^c3xCd_m540;!mKQH@ zFPus6Gzg_`J=$#f&Wp@izabo0&*m}c;fNd@ph<A#(!T`Gl9WWSKs)^hEU|-S90wLi zG{q7|gdU85|IS|FW`4v4#kR22de3ymu^^^_mm1949aQ2dtF=#$SY==M5>PnwqcLkx zO;9L02QM?}Po08#(crj_gkQ#mRdM4Qjbp|o3@aMZxa^Qdp*?VON0ua$Y@{!-L3qRM zL5<svl<7+)EkXO#QH1Y16QEZfB%J<ZH<IrS_*4Cq`H(+l#zlGr9K2FJ;cw$L)_@n- z<)_+V05zRYH66R9IM_0mIdk-;Xdxh9z2mSn|57%$x~oy~u2z-HJaoJ4A`6CZSC z?}mz%a-d2na8ZR175I;=+8vQ9f$$ByDDrYZUC0Gzay%IIS@K|9dIBYkgcP#y8kllD zc1$`1a_JtsW5KU|f*{VXjAiQiDtyEQYkFAZp&n}Q%Ggv{&PfjxAmhV&Z9kq$#;1d8 z?#f>k`^;2`Y2myGD(Mre_MQ?1%xc?br?gXIL3WC%RM<=qgM|`YsoWGPAyn(|!=3v+ zmXsl-(ti~bSYjWGMMqJ-M==PGR!4k93Du@LTBXR#cumkzbirE?>S+tPl+**HTn+l< z6eSMEh1Q<!BfKhO(ufGHGg#Ce;WFUMK*!Q$z(Xxv20m{+;4)CIBy!OHpGW}^o5!Z} zW@Ket%9c{xRJcRM{MUrcG>et7M&c#2Mts93FD9kiudE!5)gOx^24RdG1ow{{`0sYH zv{O&;sq+CyHZujwd-`x?LDqbdeoj&a_0>u|<(Q?LthBgUcC@zAl2N*>1G9AHU4{V& z=2^?#r~fEkQ9kZAcCbi`K|=I5qANbNb-dp-x)U<7n%E@0$ERfl01Tu=HyYD%9651i zBiLEPdL}zq6h}+Vq<3g+O1(J-$Vei{>+*Tb!BV8AtP(rLK4vIkk_dLG7wiH|V4&a~ zUKswxwM<+>EVQGr#+93Sl~YhqyM6uOR@-0|J<PLEv>W2LGfu8=F8nG;+jATE?cCV_ zocJQIaGW)fzFcS!VH`Igh;V!!*?M|MFZW8j6DN`&LY)AkFew|ag9!=byP;|@HDUWs z-CMR{<lp?4cYQ+#_wg14SP9czA-9qS+smNhw%-=UD0JAsK%g122xEy*E98pU+6O@8 ze03_h17)(R^DOiHS&b}c{~#nlEmyk2#Xc5-M6iksQBujGlHY=bd|Ya0J>B!b4G-P^ z<@uk#fA4<Ff=z$_vX6e_Eq`<Oy{q_$yp#jYR9mn@1SYD?2Re)*=1DiE-}}QK-*x8~ zKlJYp1^ZbG^qF^m@!bzz`H4f{3HJYg<h=`=T-ROadGD=9S6B6;WXYD|w(+f!6J&xS znThShUJ_j~I57qiz{BQ~ozEm~scoxUmej4b91pVHe#B&gnK45K4C`T(A>hE+On@+i z9kaB97cz?pW<NXvc)(=HV2EKoLk6;o!TbIF|L5GQ+f^;e4<^iFS*ogg&+~u&@AE(Z zbMOA-n`kO4K4{z@=uEKyQmR?89&XN{V4DX6BVJTtk{(nS(?T(GF6GG3`Ryu>4Cx^b zIw>4V2^|+W+gG5QLIuAfn8_i|qJ^PP3}~`drAZzAKRr#_#()`FS0zeAV&r8Hbprdh zMYt^f7Q<ls2c|At#m}}XiR6`Rs(8IP9ML5mKVZ9sY-}<8rgA92lyxAN^=K-53L@5- z7=CeZMxbOxN}=>o_o$W$1lhWAPTz=Zo?SL=;=1ZPtVVTi7JE0xc0V=u3lye!e3-XL zkVQ<`%>vt_4dxzX5IBMqmg~=QldEarCRJ!IGzP`6{GAID5gRLP<O#M8Z|r^zY=5W0 zZ73-owx2mR%qEmHkxdv`=I`FSjwYhkq66>RN*h75V-k{>feQbf`=UGD$F*SguKTn4 z+Jk^f#>8NMJR4Y&rwG{^v?lLCvDQMvbChEQQVx04y+<t>%y@La>{Qo=QKyV2cYnfr z-Id+nlENquqj7Ktok7{o_3_{Vcu3F^U^N3#1bLcXBRwJRVVNBR!3+^kR-9@D0X`f3 z^q6_SC~y_8WlWzie2Y*6y-)}rz+?De2%KP1|Mcgr^y?}8pukr=Xm$AVQLp@Knh8$8 z6b&bEe+B#(b61-lTYQBf5CTTmU;Lj5YFSu;KeQFdEoRH{;<YSW>70Dz33c3;imF2t zERhpLr&8*n%}O1ht*|U8C}Qn~a`vqjPQ;iz1FkMf^~&{h87Ejq0x=H}gQjRjKvN6K z;Am^-@wD#eq}U@j%TNlxx>72FKGI!5=}ixTg*RF#>~(VwbG|p{WRzt;S8~$~^T*-@ zmE{1EZahi|H7SlLs*XKuPz*x>`*<=z7Ag@c5CG|1!}bGAb$>C1?@~zmzH<P{$jAif z0k#4u%YW55RH2koiilE7lytwxMX7|9uAzL9R34{mTU#n-$JPckq*`z5{B&d7J+Qt+ zVWCU{$~@`e#pB2_Jez=jqTdO?Bq2+Wnb**ofD+u&j*1aFsh2QbO7@%Zw_J&&7(2He ziBVc+0X}rk0oihO4w~>zv14`9uI*FT$O%rH735hf$dT6lw8;zdXbt2=YalP019>6K ziA8yd@`6+4vW{VsqyQc(FOa>G7Y*&m4&;UE1$iM9Xn>uqC^4+)q^2bK1>|Z2$jK^4 z+Z5z-c3rVRbns8vunca1&-TKX4Cp{nQX=n22oVsZ{8b>M!;M~fB-IBcK<S7AGiCGd z=Unyrm4tP9!Ik<D^O8@B7?|ocI;CnSXK9h&x;%kzyrz<5jRYO~SCCrmh_DmGQmfqD zqtE~Beg2~Qyx%=qu}_`RjmFwT{wm5vHnhw1Xd7P9P~b6uz0^(rzTrEgbPFSOo*+8+ z2qnd&Y7W}G&^vZ?aEIv$|I|a^(NIv*XN+$S*t8O?P+L8-VfViviLEKAY&6m%Yzk?} z7!>LgM1Z6OC=%mf^Hk=6Y(|fQ34CFsqxYv3Y7df36riCpXfX^km|&SEvN1WlF?jb+ zxWtr02hUDYJmZJ#e!+yMInYDd;>SYC5E%wK4M|i#`z`RK-RD%d^L57dHCJY-NuTb% z+uv0cK(6Y~41NEcH8nQ2?7KB>;Ah$(R1Rw@sk}B!<+TBoX9Fs)GbmWe8K^uV=W3EN zEZKxW$Fv~WulpG^U{|{+AralF1K^IlY(X3Qj^RNDmGw(DIQ%P~Apq-#sd%&?9C^9d z;8#;m3~OYjn2LV^3>pyWlv1*6Y1v?K7FIZ+mF7gXS?m+VvG945ebyKG4+qdhk*fi* zaa{=rhr#`0LRg$fr?cT~6L2tVjqj=g|I+zl^a@xZdaWdeoyvz0484R%4HNZ93n*GD zl!<!qQ8&z36gB=Y+C|qgCTX*h1mZVR+S(a3F)?Vu(9~U%q%1^RP~t*8B*JA8p)ldv zf;#3FIZIWRF=B%ATAJN3ItXDL9e&@z9xXw@Ehrku&jfBO6almuxSIxU1{cAt@WQ^* zn;>Uw7MxcZ0XHUxsu;M*9zv>&D!47nmCYT+Cx9DeWh1O~{7q0l6SxOWjDXwHi);ip z9+4q%gBW&QiaSUFx6Q5u_b|mR8CP(?4q?oo|JukD<cXGfZ~unnmPe*^S_kzzkUDZa zq&7SFH_+EeCnqXCy3$U8#VCAYz{+dIcR7RRaAxuS{#6XMRy^yveXaPRSipFFt@xtr z`oAb9Yp(Dp9Z?~FAR{531PBJbK@&%i<;+FJ1YmGa9$3!Y%pMeQ4L!1)xprAY&BRYW zIH*1Zc?Kq;;<S#l95pVUliF>Acdnqdknrw=`O`u_K%FEc0)Detw;K!H>o9c!hS7}m z<bWei<HefxQLKi7T*w%&iW9{%Wkq|339ol5G7(zvlBw=RJf<rlg&a(bZwAcLO87^E zwKq$|^a6Z#uaXfWds$OCz1aMvAyN74(D&aovH5RP3-sOTX7?Z<!`nsifkmi?skjYt zL7webVe$p)`jKtX1Gi?XpT}=%=TP5*3UoeX0-L=+`E8WS5p#~aLKK)`YKuPVW;xK4 zJU`L8`GC1KBvdol*2Ol&&Ua@oalnL$!~rV;qkHxP-k|}*PBdU{($*f~h5-xo0LHw- zOany@`IN@)5~|XI;#8>IEw#C+4cgq)ur@~;ZFQQ`Y0Yyo2AysIjR2-0puF6z&gcCw z1H=51ZFT-kR*4KQd6zldOQuqxdjTA}mv$EEo*7)3ObgIX_ftj2ey4dHp?#?Uy6qC4 z;gtgITdf(@^2!aTeYQ!wvb`sJv)-$i-vWClB(JVY`e!|TbCqXTvdNO-)6m$AGnV$i zFx{h%M2*DRr!hm{;t<cn?O^P;bn!4{c^(XZoe*$B3|SUoA;Fqpb$qy1Z&=XG@CNDs zUx15h1P?DFI|X96T;zY~hf~L#-yOJI>hNZ{T%b&G>F9DXCWRU)mkTM0&E;Z14_+6U ztfkk5dgHt<bi;<}nY=C&?sdVe9x{fS!^;<=8q4K-^fXkli8S1dDhNP}RH1u=&yp%E z@bk(<l>=1}6gWf`@~J=-DmI`Bl`g4*IENvsKnItnFGHE6ko0V<xaeUb@)7?hTFvP_ zOmIZ^Fb#px;w^_k8KO4^lrSkvi`~1V^N+@BNg4}xK*;q2h9Ic4tHwup9u8XOC%fcQ z^abX)tuwry6><#1a4?9glWd><<k;`hBRM*SI8$p`Fzn@d6vMJHz>j^WjRRes7zdYU zoo_<o2T?uPzhpoF)rf)3((b1;5%r{r1kVMHYCh!_-N42118D^)b!RRhj21|y5K5Lf zQifANI<<}<8B*90Bt0j@LkEw#|H1pEXZb({DusLWpg9Ri%TH|I0HEzk^C5Fs`{g!k z$$2EE6o4TY%ul+0QluC|Tpw>RB>R*OB26g!6#c*rg0?xKgGjPOCpJHY1SKCv3yr6c zbXJ5R%h=WJ^ku>*G*w?1dBgB7N|k1W()4hnYTTG-9d@#O#|EFNX)bm{Xa`)|$x2ZK zTGc##2Z!bE{-vPwwYERFF%Q#cO(7c_HhaJ|z0CF%Xi^U9+CH_-I%$>6e`>3&PHfvt z&MBF0KwG9;7Hm(z72ms~qqu;P7k73(kq!j$AjFd8(HxTQZ)o6s?J(=7v0NdT0u}uM z9SaYeLpwS}@{Pci7KGULmcev?Q!SZefr_|v-|lD6<%a@R89OWdp`H6gA*S?M;V{}O zMaeW46+&V{UO%2HMD}kZECDdgE_VMwy-GjN_63FN;}1N(mI!*L<aYlr73g&SDodw^ zXe@Dp@G8BOHVMA@Pb!-2(;tC?m?jV;Exu^`)L|n*^a8_3wL~8JqeY?tQ3EL920nJX z7KuS%X^Y;lQ@~K%Y<$Oc8rOG{8$QTe;K5t{4ltl=Vuu7lR)FSGHASTmJY_qra+MF9 zB>sjye=1q_0l9f6-CIF!16{xJ)tE56wJnKBW*{GTEYP;(1YwJ>E4&huK;Jd45S<AI zpU)U5D_{=vQ8FHLivdb2TKq!NeahFT#u_bZJ;T<2T%%xMD#9r1zNoT&wgLi9>=g#% z8f54xukRHhF?vaCi@GSD5dT++T-E<t)qBDPR@?I3#SNp->kBd9pxkKjCl>6|{WDef zGgiV`AqC`M94?><RxPFg*)?ELICJX{Ya@)o)rz(67tZPU3YYj=<PP>#=rz2v-S8Tv z@kxVQvcG5#zFV*rHiz?j>afSd37=vVYp6>VnfugWzK&`l7OGDjrdKtC8VUC^CuOx? zFK-3tNHdBje9BRqbr&0>gg0f;lvv3Ya3r{h*AJ}>d`R*$*#g`GLBkUa{2X9((4P(9 zVW(*TK3mc;ema1b)wM&HP>jO@GE?@>Fb=Xk+xnNp9}6*SgN+>ECA!4YyaU5w`Dy3w zMvajUZd2M@242|80xx-&a|M*`TI~LzP{cT+-WsPyJ<W9(NvjFXJc<-WExiu4^om-p zXKzvTxeQ>!g9U%JM+mRA_pI7e)!oT1x#?>pZ$et(SYzC%R+i4&tI+!&S!q@|l+L`g z5x&Zo^F)q`JvaTnMEcy;mz!0)X|M*3q5H>bD<IQ0VY|d(_jmNvrLhJp&D*9{@BSBi zqlAn!uGqC2Brh9yB39xFLSA#Fgf11cNx};Lv#L_=x9t-W;l@yLOuivz1xB4k1cjA! zQ0`)LPQu?PRnwgtVXZYQnx)03)sjt+jlpL3bNVR>K6nWL$E+Ob2J3PwbCp^5x2%l| znYTBuQG1(`X||{jvs=lLU*iM#jx}aBn%I@)#FosEiF??)wHUi~16o)Zl-)CQp^@%g z3@TaEvP(K{1YMM|C;hCK7=j5``7LRn=g#e!pQT(;H5V%O-bGGXgr8Q5GB9?DGN6j4 zioPDonD?}62VMN@KM^hCyM~AENW!2tbnOK{I4Ee5#ow{vx237}qL%Tbqtq_rb=<Dy zGRYaAMFwb`(HUu}U3MpgJ*)Y|+p}aNowrunizcu@BsfWf1yd}cNDiM0Ee|Y=&aGKB z%D5KRgT?Dw0uhn8{%BbfZe7oWVUUEBJbAWipsXc*KvLE&%ccmk+fU_7v3ct6oXhL? z$~-iABwC5Bcm82z+=E^v<XtC6M_ue%+6(Voj6ln#ez2$%S;jZ$2tF-TDjQEb-&|?D z1PZ{QNjsQMlbB`Jq&N?t2~F}XHaUP2Xke>r2iZHVq-&><^~H`m>WdxWB3NY8)wM%9 zxy_9c^GSz>jODmR`=@KckEXnm7VP-Rna5yrwb)M0?7K{H|B0b!A@t|O7t&D!ch9z< z1hPIuveE4cG5yl(`IxCZ;*cQdCD{%<hp@2JXtVq4#*HNYvVB91X4OXFOqtGU(0~l8 z)3uGL7yfV3?&x5NjP!QTFlJ#iV-5`~rIs!mk@Ymt+*3OYl<gB8PPuiRI`@|*$HQ>M zxB4*fzO?%v1&dG;_kp-i+WkFyNXLGH;U>_{+Y)08fR_nT+-d5fIDl#IN%!-rsg&m0 zuIm1-erY3~0qe{TS;)<azypeKzYz>m4s5A}*n*wA)3PV_u%(#$A5DYK7H6_7!Ib|K zZx(xK!&th{`5OSF4v<$__nDzFNQj<k3^M=#+vx$NBx6Wh6LwnG)=-Gn)^Z!iT*nly z5$+lX8z2=A7}{mGf2u*~{6BnWs{8x;Riv;pVu)n9bpLOEkDY^<?tZp>4-U!(=|1o8 zjVS9w?@QI}Dy@dGzX3ako&2vdN4vjj?SV=fTJhE07xWu<1Bt8?K`Kws!xrJYy=><x z=80XI1AX}%(7%Ne_D%M3Fjs;)rn<H6`$f6D;yV^H%@kj47t*Mb6n7@z4YFWHQ$ZWN zhQL6^Tw_#f+H{kI>V0fCeX#WiK+vUoDZ}Q2E`kwmEur}l#tBdEJSVOZ;ZGGECAQHL zK)@Y_ndYq)aZu#4U*vwGjM{f|CUJ9T(b1W{`Za1&y5=$mNnnsb5_wg$Cj(}7)+K?> zncHNZ088}f+2Muv>~!TLx#0y0(y_M=_p(|jix=$p*s|a*C9P@mqmL;BBCs?fG0%B! z5YU`KT3{5xr9!pN|C{0M3WUka;|62OP(DP6Q(kZf*uIjSYe)c>aLVAV#niHEar;>b z={$2^Co@ssswK<-Kb39JD**7bszBaIuOOT}n;ryr2E+$U)&M3Qp`R_~*~ZlUNo`-w z$c%;CdWG_Q?{88m09YqH3=%@W01d-%<~Rg-pTBttGQo<s9y8$gKBV2h5(S?Rf$z1Y zfKx|vy`Kj6hT*^Qb85#h*UXhokC^JNnaEwRCW7twfSK01g;r9_-3R`x4=o}E2XU}J z@gLT)!okC*eqCP*t#(p8nOr*;Qb(Hab6fW=>*Z$S>=}UT-mM4yc)9{W;@B&mNB}=$ zPATG4F8sl6iRXAyg|P@E6djPM&$2A#EHLT(N(w^&q_n0k9UJuxyfU;*mdtk|_fkMr zp|2sk?j{<22pWw()OriIkjQrVgB<tY1_ITD+QKA94C#YPBs7kcg&0=c*qrd5t<bRO zuqy~Ig*|t^J@ti#gxp6HCL@+69%?F(Pz4~0ieY)QYIzu(1i7CD=A{W{g<W9a#Tch` zEWprlV6*C2twQZWRQqDhEOovyQP0!KTd{Ih!#c1LXvkb}zo@+?@AE!*hsTDw^T@nQ zs0(zVdhsBJ>3|<F40&jG<p!{HSoPB~Zu}Fl^N|u`flU_OlK1{Va7zk6Ub0^={3+fa zjPY%GWB;8TZRcZc?Ovqby5%3L7v7+XJjUU707R_ovE|}3Kl<xG+JM1A^_`s`HYwTI z`LFe<(9ASG0&H1)yj1*F0&a&_b${H8*CKj9creI8!llKRXHTd*+(GN%v&&B&e_TKL z{FpxJ{$lwfITKy1-Jvum6)<oo53_qLQzD*^Nj8Z`QLp$YKV)<BmD`TY{O0QuR`3(? z9j76@$dmSbeb7Lt`YE1UkvjUP#WDGukZfPSSiFhKC8*2UMW~~QN*!-FG*}GfIV)zD zxmH#B;toxYCj___Z?_PnDHIAUrxodvT6-|(Ih`&q*Ep^`-U6Z$lVa;)@qBHWFCCqS zCGNRjKLIpM5~+bIq}YU6)<rPsaS&AzzjTV%NggHLcFaHc<p+L6?~LG;JM!e#Wo9ah zvvncr&K!n{HD;$k9w|E4u=R~r)nJ&&6e&1sm2s$>7H5B%pM20`#+lbw3n0OcQNi*p ztt{WvpVWYDsTnZ6Yry=&PSw}}Pu{w$JJoE+BuCH|xNW~!R^m8T#%-C*4HfA~L#LE4 z0Hc#TyE1Yn*5am@MSuq9V2<0g$w5it&UjNhvD%-6EmZ2rR}N&PSLv1#_e$wDapC=x z5I}<iZ2CoXv+3|2sa|P^zKw(lDxwHbt-O^v!W}7T<ze2wsmNAw?tCok;{gNzmh;*Y zifOY4)N2|zkGV$+`M?)0Uqtg@iOZq@n!;Ri+;7fNt&vl14u0Ff^=H}>Qd4}_4KZGu zc5P_n4rRxdsp){t>ccWrB*gzXILunJ9Bbx7nv`Ui?BR7|?Nvsjp%NohmO=l`bp~+j z9Dc18o|1tmKK^reDlSVz0a_n}g;#(PAe6wZ8pTCD#lu5gIN4OGR=iHAeBwsJ5tS=n zRJgiSyoHEQdNH7BokvlX^yd-4CY7|r>VX!M;x*tzdSZ!xFS>eewWqEpBh<rU5HU)k zAH0{gDg+c*D_S8!=m{1@9sxsvKnFmS=HtQ>MO|6_yZz49Tx*d}%+VV{jP`Rv@U^+q zk#S29{)!I?Qfif?l((0)%vIRrjS^AVob)(a)?o+{80IxX8a*@c+q%f9t9_@$v13wS zY~0%UM2_2TQ9RKdc^859OPVrK{5yIYDPN}GL6brrzQcnm$fMbKsen9_LJXs9N;4QG z=$jOBm|CHouz)<^Oa*!DcR(HvhX{GvG`;~%hyb>Y*A0&&sq+$kK|DC4HD(7*ILF|p z0(nL2B|j9!^SES=++$9TI}w<1c`!nu(dC9oDMCc!qBjZcaI#}+bf{B(aMP{@66+Fq zGPclkW%v%#9inBi8O+u&qRjX=G&F?Tr6Mt4fAJB%NoM3t)6vUQE;#R2Di;6?Hv{)L z8P%}Ng4{2%)78@Dj{uU(ES3qhQOM=6`)dp^K2bBnePlg9R%7<s2i0-rgnKc$TdZ^< zNJnG#6=3a79>YVrJl$}DH%q-zpjgM$LJwiMNGU0~@u;QAy+w|#u<F8Oi+g6{{65qF zNsN0n*O|lK%0WiJk{e8RK4HmbM8XJA3kXa$5G<2SJ`52yfe85qTIm#jtt|rGj~*2` z7=29wsWRAI8GtdSOI67?rq3Qt-sCQFcxV_JF{)_Gy274!OF1wv+b5%Z9KTK0=cs$> zVOq>Y11dh1XxEgHFhj8k()=%lVZ<py*P2dq3H`^%uH{e)*OppodMAEjhUqcq@RCZM zC4YOqZhoSQI(pl*HnL1>Z_tFeF8fhAK<4;+bwynv(W)YeA7gPQBk<CE(p&C<ZQ+~Z zb(sdXgMS!{mK%g?HZ|8bEgAy3r5GsIG#=DdAPnh#Om;ZyvI|X?NmkZF4C3S3Qu9BR zcQQh?RF#Jj+r(*wYrsshY;sXoAxA7jP*{_dn&OV;%kmjiVWK3R-SCJ7iSrO!d=jN3 zCsqLMoJpH@4k(n92*DJHo{^xJOCfL8stBix4C|(l*G(Z8pUs6DQpizn^i>LZ_$r0G zUPvLg2gndFB88lwOrD1ZLU}3V_M|C=+!O&0FR(UJtWgj}8E-c{*OfoUWgRGBbetqb zB37D)ZfcjbFV=<~#~jlh&EBF<AVMKFlGhkMqN-@g^5gWN^mh8>^gzt=-HJV+)l3N& zxV=VKv)U+Vev)TWk`q*d$^eq!ZWpw5jRG+fHh@%=5f0CXwpmmLR9R<VO2!S*RNpx6 z2-9l&9D9JTp?2U-?p0KVj~0xlOlawVinWc?89IY~0t3)6Bm~TynRH%<WRS?OtVQN) zfvUU2jYtj!)M1~TS}Cr8+W^GHqA;rguyz+QWhRTAz$vU-J3_V2RU#Fzp^m%OY}ORr zT#|jNMqVu`*Oz4Le3fP$F&L~d%$`K%T~0w>Ig(G3c}n~oe50)J6Fg#-n5y)2F6u-v zk!n&e?!XSxI`Wi-RbddsXC^4~{Y4pUmF4N9-W6ZuDkhl?uxE;ylM|(l7vqfvIy9q! zU<y+qST3{#zz&86L~W5AfvMPS$tbimtlon{J5jujUmFzKmEtYOwYg|ozy%yH#{7>t z$O35qIBB#Z++N}w$|f#Sl0nunuZJl)l{o^Cj@f@KDm0`q!%)$H(MHgKxu{qgFffI0 zND0m-<(D-jc*6J?dM&D>)NYl6L8IjMJK|uIT7?dHVZ91#<db9KlK=o+uLn>}Em7kr zC=R5UF2N_;LORro9bQEM;EG0PB4DF*q753DG*UE?jl5eHCmh=CMBbOm7OI*lRt*~* z?MMAe+2D~&<d+@zgrw9PR)T3pMu96p8Eh^56R8SR$wc}cgn=Q<<OXQ#R79ecl419; zOu;@)Et)iE)Z7t6&~5mB+qs%GDd6Uls)@#rEYO<Sw{5`!U{cVBqDrEkP0Lz@E)%52 z4v3?)=<J1EeXSSPLA{ujZN;kM%*UK_2LTpcRR>nTOx##@;1N1y<fQ?TuMeZpx#kht zf{IJpYFM{yT;p{Vx*Bbv$W_TvCb!X6!?A`nwJmoLN3>;2C~rZCh?05E5TP)d*BAnY z)r;5hOBB{{lTjhAMq#KZR-293Q3}Hri1Z~Tq0KlOoLU!IVZ*X`)rCfBHL~I@aHU;} zXmA>;0LK<0L`K&T8D_1B3<E_()-;~qw2(0w{&lhahRBS?6PZyCoDrE3yHi?4keNn7 z7lRI`Lci*ewq+oxe;=@RS@@-~nG3_Fjvgs43mTgQlgrxHX2a|t*bCq+FA0tD><l3> zE@u2-LSmu`0}|V}AtaV?$o7dln6Ws5#JmWW<>p8%QXN8K+89M*03h`v*k6K2Kw_Si zNIdFOQbK~#rvx%?^Q=2X=~2Qa*oeZ|2Zq8UJ|#n1yq|79+JazN^Q)@-v4YSs{$TSy z$wRHhl1smgnP_zWq?Jyvqky90GMYHREX*k7VQPX~Y?5sXh`{AVnZV5j3jC7Aso=Dt z!G$k&whPoU1<&Ztsozm%qt{^DNHbuwZEf2IvYKI?Wiw}iiSUiB8gMx@D5Y6j7k*rw z87xBy=+9H~V1XanABDZ}EvVI4L?a{DmIl0(c22lU6zzvIs_L1O4Us72*}%c%O-Xw& zH9fTg6lYUOEJXz<!n<uAg-|bWZ0LFmJ+l}FF)<t)6CEVx1oAOuV^3uiN<$@~y}j3O zDww|Fjtz4|F*xCycT-se^>6{2sYs9gJC`8j;|U;R-!iR~XVK^a{U@KO&>z_$x;Yyy zXtbK02@(4S{b_4U&_PEN{lz(nRDl4Y5UPO?834du5UmhiAnMUVpgdU=^sX=vZAQBi z1``Urif>T`<J%Y^k@7eyU}3Q(B-jYzg%q%4gu-}Z6h@Ss7KF(M1<WYSP}q=dATlV1 zkf3>uAqa^CApvb6Br+EgAg2onmIeg1?SWAtfwM0w#Y9SraOk@1h&;myE?Hs7U&T(@ zhbAl7z!81nXL5qjh&2cJG_rybAS*~FEm;A;B`YROR!j&JOeHfDA(O1oK#&!bL!XnZ zuvMH8q6stAh4|^9LlLt2RfjaE8=uv`4;YBKtgsp?3Ivc9imU9{zSwuzzSuo8Bz_Sf zL0*PUh7p#7tLC+68HC#hFM<ct^j^j&pa3(Bn3TRWD_9cM2EGwIc0kQgN(7-lc@@c_ zDG@7`jVCZz!zJOXW&)bWu<4*J8&QZKpxgw0ASNpENBpW@XphR8>J*Pmp9tGKl;l9G zCP|!^OH0)@tG&iENWUS{*F{T&8(A4AeM=Q?@16AJXp+V)c87Y2lva6|9f=!>r3Exj zG&Pg%71Gudof{1_#D1j@5>nHUQAAOmL035@#sEzNz5wk`oCM@>PGXk7s<SAYa*{@c zlV~TxuChnDq@r;W`!>W$WS(o)0*J&(04EGcHQ*!zc-jkGr}2fZ8b=@tEXZg1*dh+l z9wray!<VX8e7x*3RNo-GO5o+GEf21#3W(t}4rG07vn}L}U7>-D5jS+kVQJ$&l5o|P zqO-sOSu>|wu}dcD=o|E9Lqu2G#`FMV69P+Dc7X;0SGb#k#i?xqjW>2#fwfk}=$h;N zya^axD>0g9hSBmmzdVcv`=sYP3YW9i{DaC+3piRzyDE4!fmZ`ZP1;#1)H6!EIO!1_ z9YAOVN25F|=fw<1u)@eSMj?#aLs&5zL>)v)AQ!S@Gf_vC@uCjw6UXAi&XqeG7Y=eI z(MytaP=zF&Vjnj;9KfPd(qUlvAE^XJsFHUUXkh|lgS?YQjwygAfMl(HP;jh>L#M`| z78^A!LdFmR4yuxXGksA4PP;S`aJFy_u~T8ICXWF39`d^B$ha6%4?kg;GKLIJJOnbZ zN)9Gayi8T;A`(Fqz~|uv8`y(5SFQ~JBIxbXm%P(CoGE@>08#LXc(4iaNAr|3j9if} znVb-(MN$l2-znPExgpN3&B3lk^<9fHv9ot9{>jDN2`FIu3l>8}n?xGv7FzDtm?RR# zdm<Rt>KTru#Xedea$YDDhtPvmp$Es7`6Tjyp{RL*PMC&b9ad6o#Rc=4We@|{xXvZ^ zmjVEyI}h*R>NnT{AevHMHViDw%tKiQU{wGPGMs5LRWb3EA~U>2N}&8R6n7c0Q7SdY zy^TEX{!{4`7KNu5Qp`tOsv|}UJk(7*8}No6H!hwHfc4$MifC@KMC7DHEir}6gSR%i zaYrl~kSX+uref=EjoGYW>zmEm`w+k!6$LC%KCnySjCdz|+KmCgD4PoB0+@tc{Ea(% z(cen<ZX_B9P#7cI-C9o>tm=<z+qF1+EcI}8OYr!>Tcs_9-e^QJB6_TUi5|sGl(v-V zwNk7_QyM$9ZI_ruqbaFV-kuYk7pGj{8@#h921-f%m=?-2LL8(Q5;MayDO9Z<%L_iK zjB5>0yenMx+hQU*`IIYEDE0!*z#CUxN{#3O{M*ui0K98J7L24p((FbJD1e@+S2Q4V z$jRu;(PV-KWZ%$$bRv|0qeCvviN?iW6gEW-G@t<h?1e0Rdja^`?v4w{FgZX5_C|-1 zA%qK$p|O-UoW^WJ)%21`XuKN`nieZCpG>SvtfV-J&%ii@;**G60EY}?HAGzg8{;;U zuGs=P2ACMsL(}GrAe}`p*YVo;Err0V78HM;StvDOh>j2p1pARg6%~>VdOoG6EMhh& zO3(?9VCW-yJ{mMFZ&Z^g?0P=+gqT(tp(Nr5%18rrC+FX#R%C%Memz0Zim1FnD{{?A zU7||a?4&j6H)>Q>&zDW!$ITS<d}&y&B$?E%&RU2d*CAG~xDwP{@e{e8kL^(E`5US; zy-jo|X3QFF#Bu|gJO*R*eDm1hB`@`SzJcXB=&WG{J)a)K2Bv{idOqdAa$ZC&Q*f4^ zZ!hAav+M?$Qn9bvZQ~SsmO%{M2931}NoW~MXz@+!F{0Rm+y>QwV!yGgaf-clri+GX z6Mkcz4K0njb<HCx_F#RgDOK>5St1n1bJy`0TU~ISSBZ)}4GD#<wJHh+#hy0-<ZgzF zH}fng_U45eGF8tOg;DIqXB=nCSr5RDEeq=fDNDt^3SJH2)j >}?l?=%j#@_H76$ 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 literal 0 HcmV?d00001 diff --git a/plugins/official/container-runner/dist/plugin.wasm.sources b/plugins/official/container-runner/dist/plugin.wasm.sources new file mode 100644 index 000000000..88f53d631 --- /dev/null +++ b/plugins/official/container-runner/dist/plugin.wasm.sources @@ -0,0 +1 @@ +929b309e182c078790940244429b9872d8c7723c0bc25a07b152c83300905c47 diff --git a/plugins/official/container-runner/openidea-example-wrapper b/plugins/official/container-runner/openidea-example-wrapper new file mode 100755 index 000000000..b4be0959e --- /dev/null +++ b/plugins/official/container-runner/openidea-example-wrapper @@ -0,0 +1,9 @@ +#!/bin/sh +# Install this directory on PATH before granting process:openidea-example-wrapper. +case "$1" in + terminal) printf 'Example wrapper terminal finished\n'; exit 0 ;; + exec) shift ;; + *) exit 2 ;; +esac +if [ "$1" = --cwd ]; then cd "$2" || exit; shift 2; fi +exec "$@" diff --git a/plugins/official/container-runner/plugin.toml b/plugins/official/container-runner/plugin.toml new file mode 100644 index 000000000..515eff627 --- /dev/null +++ b/plugins/official/container-runner/plugin.toml @@ -0,0 +1,15 @@ +id = "openidea.container-runner" +name = "Container Runner (API Example)" +version = "0.1.0" +description = "Small plugin API 1.4 example; see the package README." +authors = ["OpenIDEA contributors"] +license = "Apache-2.0" +engines.openidea = "^1.4" +wasm = "dist/plugin.wasm" +activation = ["on-command:*"] + +[capabilities] +required = ["project.read", "process:openidea-example-wrapper"] +[[contributes.commands]] +id = "start" +title = "Example: Register Container Wrapper" diff --git a/plugins/official/container-runner/src/lib.rs b/plugins/official/container-runner/src/lib.rs new file mode 100644 index 000000000..b6a738be5 --- /dev/null +++ b/plugins/official/container-runner/src/lib.rs @@ -0,0 +1,59 @@ +//! A fake executable makes the argv/cwd and terminal lifecycle easy to inspect. +use openidea_plugin::{Error, Event, Invocation, Plugin, PluginRunTarget, StatusItem, host, register_plugin}; +const PROGRAM: &str = "openidea-example-wrapper"; +#[derive(Default)] +pub struct ContainerRunner; +impl Plugin for ContainerRunner { + fn run_command(&mut self, _id: &str, ctx: &Invocation, _arg: Option<&str>) -> Result<Option<String>, Error> { + let project = ctx.project().ok_or("Open a project first")?; + host::register_run_target( + &project, + &PluginRunTarget { + id: "example".into(), + name: "Example wrapper".into(), + program: PROGRAM.into(), + args: vec!["exec".into()], + workdir_args: vec!["--cwd".into(), "{cwd}".into()], + container_root: None, + commands: vec![], + }, + )?; + let id = + host::open_terminal_session("example", "Wrapper example", &host::Command::new(PROGRAM).arg("terminal"))?; + host::set_window_status_item( + "wrapper", + &StatusItem { text: "Wrapper running".into(), tooltip: Some(id.clone()), command: Some("start".into()) }, + )?; + host::output_append("wrapper", &format!("Started terminal {id}\n"))?; + host::output_reveal("wrapper")?; + Ok(Some(id)) + } + fn handle_events(&mut self, events: &[Event], _ctx: &Invocation) { + for event in events { + if let Some(exit) = event.terminal_exited() { + let _ = host::output_append("wrapper", &format!("Terminal {} exited: {:?}\n", exit.id, exit.status)); + let _ = host::set_window_status_item( + "wrapper", + &StatusItem { text: "Wrapper exited".into(), tooltip: None, command: Some("start".into()) }, + ); + } + } + } +} +register_plugin!(ContainerRunner); +#[cfg(test)] +mod tests { + use super::*; + use openidea_plugin::mock::MockHost; + #[test] + fn terminal_completion_updates_status_and_output() { + let mock = MockHost::install().with_capabilities(&["project.read", "process:openidea-example-wrapper"]); + let mut plugin = ContainerRunner; + plugin.run_command("start", &mock.invocation(), None).unwrap(); + assert_eq!(mock.run_targets().len(), 1); + let event = mock.finish_terminal("example").unwrap(); + plugin.handle_events(&[event], &mock.invocation()); + assert_eq!(mock.status_items()["wrapper"].text, "Wrapper exited"); + assert!(mock.output()["wrapper"].contains("Some(0)")); + } +} diff --git a/plugins/official/csv-editor/dist/plugin.wasm b/plugins/official/csv-editor/dist/plugin.wasm index c0e3c433e4ff49de0d5121159d0854ce4d19f8a8..180db9233cce3c9118b27425a84f4fa1bfc525a7 100644 GIT binary patch delta 5411 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 TrHe+h>|Y<dcEcI_-<0xqqa2YL delta 5403 zcmcIodt8)N-k<MphD!`E7@)(&7zQ+4L`7EeO-!d%DO*#!Xx7$BsgaOtChnG*c<HjP zA~2)J3&LoWW-1`G$5vF_sPvTzlU%mggd$@%T1jWi-j{5D=XqvEu=ex*_5L&8^Shnj z^*qlrr?tbprNdl34#=p@!gs`wX}Xyy=Mw=g{tXPg!oLAQx3FMR(9}R`KU-TI77b&@ zh*9GG!xZOyGdzwp&V`TX-@%r#CKg!5>5s|ke8LjXydujurR*FPU~_g`Zs2{)5mN(i zSYL76U|wl(G<vbcivfyR=zMZu_8gCw6{A?aUc&9x?Y4Mv)2Rk+pBt2td#f(2^9swn zn4pB~H|xUoN@wJtxgi`=(vpHjhGnBg$6FLD+}#w);Z_bB%fEjclo_Te%wIHb{u0Ae zc9GsgD$gwIF#cU&P2t~-))D-B)H<Ahzf_#ssF+q4_BjtU*`1aH7tNma#|sS$Z9$IS z_0G_$#r^0Ps;2j)Ydfl<;;^1ght$TMDh0}F4>Xk;ou4#EIA=6Z$45?g^DWMb=Ahco zn@<7t&YBOVQqzXo??1?}QQL;v(U%s3x;NBTdcKID-i=~I0Ad*Bo&fv+wRklM1Bkkd zME!L*Nxen#g%HGt1ov)GAx89yjwnQ8pEwqUSghBfO#Ukhp9II%7wI}{3JVPevyE&) zNvjHZ?Pw~J{~U}VAYMc#;Ifh0HVR7;;!rPAlMqi$8^!HOtguh~CJ9w26A8(P=k2o? zM0OIw<$`2TIJFhYg`?p%Qujs~nu$lvH*{7gpJ@g|m_c`zXox6NPHhH9XN4?rV7duq zvSTuqaIM5W*|-C4;m*bwxaHYw>?i6e5zkG<ICgh@DlSt)i9CD<s+nIQ9={V0p-wd2 z2|M+;M9-ag7{%i5>1<srem@;g$M10~|HF)iM3Zr%&2;O^nPKG3dXG$)flyFSu}GVV z>C|2<8kWH-H_t>m=tPM)c^5iREnmAEcM;Z#EB9hKYQ)pCkWG!H;>}sGGkJa%HdA+n zcy=~!$BW{`Y-B~es5y1uW`jzzt3pKGhZGi$zYimZRy(yW{CTx1g3)16O62HkLdvA% zK722pT87EqxU%Gc`?1Za(%=6evcVzj{^lWIz@AdI-xObc9eZTZTv$PuD@0-r-eHvg z$btJO6vg&OkV{=o5ipO-TPPg!ke}RhZNuzv=scSiDjeMv^7K6H0+)E{0%U~rXbIWi zD3mn|@FI9eIW89~f6C*j$FNX&ylEjysijn$T8JgoSt=&vA!e{g%Ov8WcRmAcLH-H# zl!}5pJc(-Y_dMh>+?CIrKO}1MapA`m5H*iu{ZF&U%RemQ0gr2Nsj6}PYu;_(h2uKz zL8zjeU9x-$h7z@wh~E~#CeANKI<>n*OaXr7WgbxUjmvl}P85scIrs(lNb>Bg=CLW3 zlYWOhRRmG<6lU;rN#oNz8r=SA%dwVskY|?TB_ogXZ&zXl4#^WM5e+6kDMS|6G;S3N zBI~sY>T_%y_2Q*fJa`Rm(Y*?>3WHbUW`&cyShyNCMekaTM}KmI_%#@<6lSbJ5~D0w zgDrt@iSTuJfMMY}%!W(;c^%dS4s6x}NFl2o{>f@~%LhtOMkBiYLJBj`MDKZOtW+gU zA%orMGqIZ-jg?}Ei~V)G#Z(s-C_L!GFBvy)LQ$B$l|8_wX>Q+ySxV#5CX7+|v(5Oq z!nvC<!|wAMY!99m2AXIKn5?|}{j=T&n=xML4-lB6Fkf)SjBbG`3cCd22l@6f*+V9W z!5e(HTgGjHnV-FGk+KzcD_pS^Zz-I#4UZ|T+J<t4)3+m9;ezeRQ+Q@OhA8}>?HHzT zNEy<H`>Vl0UQ?jpL{litI<tR;c9-FySYLdHjCPZjp+7{CumkByx;)KLZ*-WR2n$dZ z;9}5KKODl7JNSXdrElDU$^L|^jftkd)v}f2Ii<9_9LrVQ$rbpu!sjaVJgO@Y>o>OA z1N(XCsK8jI9Op)ySIHKXNfExh0v#-z=0-}qzY>|0>?nLTG0^Cn7}h9pqrx`?CYxD} z%lMtRSMBWDi4+yy(VZAI(pQv->Hc!+EZr$TD$ENLeT8P@!Ml*9HhEwdHeB65MD72t zU3g82cD=+0oAJY!uuS2!O5CLI`AWPS<MS74H`$ES`uG(2FXMTo=6M+-6ehodR)tqy z(YH(Ajb{{HwHw0~{%tqf6dvBA>wmciXB6G=sy>^ZS201+WA@_FXrEhac3+n@dokKC zis~o2ycbU>>%7<aYk)CxpYCPBJ{(qb(tbUvS^M><TJ|G@`SQ90`~p`BH&*exlktu! z-Eeso8WruR){jzQwa%}u#vXqR{lj~_24lwRk@L&KkV(P(GGK_Zn0YXb{_)ix4^j5O zei?jJgSpCK)<K*P_V&~$7S$t0R8+&FMC?0WEUv|~is-534-&@kIz2N-oj$!s>+oNS z|6LtUD17sE{Gc6yKVrpy2z-|031ihli7|ea{`LKDi6W(P{~<h}P#)6jy!s8TEb`zR z{GwBWjKg}J9yzSnZ&<xvzubDApLUFMsH^{vu}nFlKk`-_L4yC#U0bW#BYLf(kK(*? z`u$P==Gzo~lAS30;+S5W*ap2eqCxL^Qv*H?AkSu5{Wj)+ziC2FV0cKG(B6e6$38jn z1bU3jPjABG<SCc#CZvMuh1i*mIN9|s&Vo9(ikypx4l8?JnK_cV*&dxW^yP20muVui z)^1bMh|j$*b2v`>PNz!@ZpKMeiZ7b+27kTnJdInZahvQo4G))6i1-f?E5AO2VTzJW z9Whf&nfSOJ_rW9PUc^8#wF72aTc!fBkxRpYVq*tFXzdOyI#I1OJenjr?7OeExZ*f2 z-v3+&e^WCa3!^~_Yb@la$98MQi0IYhb{K3HM~ZKPb&>b`ghh#cMB;cjMT+825vI1R z_*8e%kJ)xDbFkN$w@BAGySiiCHF_z1{r=`NcGX!X*L{Y){Kw#GIy28>P<&;X)`qL; zg!&I;omhLGpLmUB^2mAY1yyQIj1<pwaSnA}qFfP;Tjl;P3^h@ckRNs<Hvlc7<9%-% z(5bKUx!~L$jbEhJoqDTOBUoQ+TiZomuhrLA)Y>INF7eB=O^EbMNLD!e62|gZNbx01 z04im(2M>aWWW+bHQsZ`c+cy{&pt9`f#bD7LL@Pu>FB+I5ne$?}TI8AOCXi=`DqysD zBb3bK(xxI({56!qsM1UPuOjNa#NaR{T;*zbBE<wntknsN$+1o!pEUK6vO_EmyE-m? z>+Kt-uf1dC!;o;YDZSz0x*bPl`*G*(eS4}@{%;fXHM!#5-4}&#sHa(Y$Xj*@Uo_XE z((}!i@5?*EKO>zx<mVP@wNkIxeJq9ekDU<5Q?+9wKT4&c@R@oC%ywPhnBvkXCfGZ> z7e!(kMY6q|nMN~zWM}GYcVvWqFwc%4o6mvoK@!+UQ4SbMZ-;Vh@~`9QNB~chaL}s^ zj$3IzHExkTx6%S*Y|9qy4EVAOwVAvJx>78jLROwG1+jIynlF<+YVz<DnorcZMcQwt z_qD~PJLubB9_26Zp%(IN5y$VPUsB6fX_-Z1d2YpRvni1p%SGO7vZGEG&8BFrc)s== zy}#cO$&2^XFzxhu6=ddv)NZ7fa%s(>On&H=%NY+-Y9J>oR^-x3J|f@eQcgrjL4N*{ zg84c5PtKRQPtnPrQ+t%iI6zs6m1Hm^84QMFp%I3A7)zN>>_fXm@d5f)zEedri2vnU z57LdG*4w3{mJSkc?|GdXLewqL_09v9xB#^v=RYm++R7+)enycp^f-+)Awy)n#aU;F zg16|waCP01o3m(M{(OT0*E?$lr6NOyzD;?-tns_|=w^o6_c*5v@sIatG7Cq&PjmRf zN~}9gQ$w}e8oth9+u_=Uj&qdN%u0bDP&;2|i7(DjHj^V;D9Xs>q`%NTD#$da%@QMC z{|jX>^SzHLPMBILS(lVp;>B%#Dm@@UY;5Jx&5(a=rL)TMhL35YZZ?4-yp4XL8!FYM zZJO%#Hu@H3EjaBeDOFEQocx4RHMX8|nwOn=j&9T{k@LvX$Cl+Roj<bQ?UZ=o9P#~= zs5wVJV`w`^X$&Fl#FtMZqn#?1e{xn?V%5T^%B~hBz3Rg|Xqzg9<10$^D>A)`Y1JK5 zyqMWT(WODEHOIfA1WvW%E8XUq3pAR!RTs#{(0qaT>P!4^LFtHu9xaS%J(SGU<<FI- zOg&G5sz#%`Xe7hKU7G!-E=p(Woi0jM9zzm^ql+S)KUh?&2L7E-9ZOeqYbN`<H79M| WT5lr0qERgS|Bqd}+5GjNDE;4ieZuts diff --git a/plugins/official/csv-editor/dist/plugin.wasm.sources b/plugins/official/csv-editor/dist/plugin.wasm.sources index 66190f363..6d851be5b 100644 --- a/plugins/official/csv-editor/dist/plugin.wasm.sources +++ b/plugins/official/csv-editor/dist/plugin.wasm.sources @@ -1 +1 @@ -51def3be1a570ee61cb23f0b738002427298dbdafffcb687896531685bbe3c77 +5099f6ceba7936a9b54340a4ce0c9b50a49962dd96053c850c010ea5c803c6a2 diff --git a/plugins/official/git-toolbox/dist/plugin.wasm b/plugins/official/git-toolbox/dist/plugin.wasm index 3fde3ebc011c4f5c7e317c971c5c30ab8deb132e..18426a611afcebecaf4310f9aeaed3fd7fb2c2a3 100644 GIT binary patch delta 18033 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 HB;bDmJ^NQ< delta 18112 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 ^@BrxW#U?N^J^f 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 diff --git a/plugins/official/git-toolbox/dist/plugin.wasm.sources b/plugins/official/git-toolbox/dist/plugin.wasm.sources index 390cff508..6b3a47871 100644 --- a/plugins/official/git-toolbox/dist/plugin.wasm.sources +++ b/plugins/official/git-toolbox/dist/plugin.wasm.sources @@ -1 +1 @@ -6c4439b36b22289c8dc54498d6e7e72a4c4cc15eac1a4957fc2b71d4fbd3c330 +6deb9876934b6e21bd99c791f2a86167f1d67e808fbb1cc514c7e621514b654c diff --git a/plugins/official/graphviz/dist/plugin.wasm b/plugins/official/graphviz/dist/plugin.wasm index eb2f4ce8857eb04947df126c30cd3b4c9e606a32..403852a4caddf4391cfaffe6b7ec3c6266e2112b 100644 GIT binary patch delta 20096 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 WMD=}29s~Em6#u_$LC`@~?f(F6;Ir2N delta 20212 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�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 Y{lkjqPY%mX`2R<@`n9?(nXKmj0T<k^*8l(j diff --git a/plugins/official/graphviz/dist/plugin.wasm.sources b/plugins/official/graphviz/dist/plugin.wasm.sources index 7df840afa..6f56a0252 100644 --- a/plugins/official/graphviz/dist/plugin.wasm.sources +++ b/plugins/official/graphviz/dist/plugin.wasm.sources @@ -1 +1 @@ -00d56f431ab8e59c04796cf56054a98ba78040a5e4142c3f727725c31781a008 +5c2f5a254b25ba018369366df87707b423e77a573fcb73cc3c74b1f321f5aa61 diff --git a/plugins/official/key-promoter/dist/plugin.wasm b/plugins/official/key-promoter/dist/plugin.wasm index 8526a5f2272447a7487b12326ac4555aef0751f8..a3dacfd424c950637cd399d9808c609916be8474 100644 GIT binary patch delta 4992 zcmb_gdsJ0b9=_kc7X&UZdB_{ig-k>td4!6B-V{kp4KuZTg@zzLB2kGoXgE@$qM}FH z1eel*DKkp#(ws_~G#_cQN?Xim4LF#X<N~w_6`MOYzjN-n7j&Gp=8svdb-(l5``f?w zK6`U2yPYp~J0Eff5=0M_(r+a`q~9qliGE*a_mE(IO`Gn>wwIdEJ3B&~5Ho^DKh1)~ zTP}XYaMQ=q?>K$L0GiO|XRQ_u`Vbd-)-B5_%-1;a4NF-N>8RIR90oR<j%oC-YK-kF zo)$kDm@%iM#yT1xPnD^{Ar<$lc=hyAWof8P>Zq`;IH69~s~9Fv_K4QWo8ls`Oxcr& zRclsiX2<co92Uq&<S<uqxW{WK{f2tWzfXEc((lXOQRJq{`-f*)!&Y<gQg7%jTaFcL za)klLlC9#Kxq0`=0n4WMm(BUhZtB_Ytzz*~gZcG>Jz#a#=aj-cEe?qXix!|>bYGVu zeo*9Tp11A`;Di|VYC4+Dxv$<1M!T443PgKtk%^(*yzWgehGwyR?^h(u-525_?r-pL z*9i`SW5kBy;$n?)cbRzTy#Vxx2j2^1C1vKb@3lGw)s@NqdcM1n7#h96gaInMq0D@| zVK5ls^^bbBVJEgJ#9b;;E9e9l!AT|8Z4>cn5Np^bhJHGj?cOG)e)>UZiFHL|zZC-n zow}k&9C#!cJ?5Ar9_~0{PC2trOL4U|4b+G9m}E=Q8jYLAN;YUbtK|p;DwNs%Vt~%E zWV=ieFaOQQY;1WBtYo`rYE5UQJH(FG1XjAs9D1pq;i7o9Z7jumw#^_8y?lY~-(hAy z9Ad1vMr>$bz)a<$&s77l60a_1duzn%s|hlkzgoyXt`VR5F@V6F{i7a4$-nkAPK!Y| z1I(@0V!;mW5IujojHBj%{4|51Q%vpr9gc~|I@4Hvx#-drPC|571uWu;u5?yeBYJj^ z@u;kkeKsOVBL~teKHohQH^q|fn8B4hmDJv#GHC>@Q6n>$u|J;?|3ddY;{Wx=M4UDU z-MpZc!|C}Y4P>%5`4+s&%VjN-Y)Lbgmz$^G_5&-e5p(W*Nnnoc-TN?_c;#+3&WTfg z9g7aJ^w;GP#fqo>3J?yXbxcrvgVAF4`)4iCD;}}LyIfS<7hvo#_gYGUbLI{AiXUcZ zk2jX$+@5t1Sz|f(0#*e#DNcL7srHDIpcS-ghqUm$Kp<N95nwf|+{I%VvSqAdn9ufZ z=XV*p9}+dctHsv;GCo%NR)=1#VgD{USCs1wXOGH7ai)B`%JRp`gy)~Qp$9(FZ%%M! zILbfM!@#5Uh{b8XP>=c6p@Sr?aK$s?Uv8u-&5<lkaKj{UaUcdtRqmKax!`v^kb#TR zWKX06H~H>?D8W%FzzcO+^zc9Xf*MQm7zAICIM@$k(aRV4VU2gwgH<p#@#}s_3puAu zgli+2E}Z%$k~yW>i++wz^T!x6yTuBp{9z!e!yh>hRk%0+aWr2MfJbmnIud|49AV+l z3Ybf9Sio#pBxeIQI{LOLRwJ2zIPBKiq<uqB!-zUA3ch~L`{b-fGEKPFMhVv$oB6UR z(r)49QOG9fjlxudNyD(i)nX0BL6B*ehsoyn=;25t$QzEw3Emz~>7zx%5k}x2jTC~} z(RiNV!)U}4{1}Zif|wYD46-VeGvku#2DOtcQf3TXL7bJ3VJ5-x$8d^Z%LwEW+!}$m z{A~-J!W{(dWSjd&3$KsGav}x9;ZcI|afl*V7Kb<=d$_4~dNV&9he5E1+!cp~M4uWD zKZ2}yO16bp#zP?ZB%Vs&%v}<wWh^`+0X_sv67V_=b|s*Qq}<2x0>RnG5fx{3P;g9S z4O%@LZ~e0cXs?Z9pDz|ZBoU)&d1fMl)x-z`5}Bvox+>h!XyIZahLXL{5)nYm3yIik ztBFp~8=LvYk(fmj-;Y#ix>1OYv1Q70x;<>wH$}NHK+xwJ)HgR-SUd{Jq+#DEZ0}R; zLaN7%Mm<e*j8>8ol!OA3#3YO&XiCD@_L8`U>jdr8K4bjDG1x?;q_KF6Aa^XjBbb=1 z=qX6XI+AWBBbp#01+4@ar!Zs3p_wGd@k-kyjmLPBem5RDKDL<r`*xCr-yxoT#;5;G ziV>@5>0u+jA}D%7(O&-q8cA9@LGi;hLGdGSA`)l|O`nJmx&KzAB7$INs<Pr(Do&EL zX%bSL)D(ihJQ<NRSTz|T+&c|rb`S3J&77uelIv*@>^<)j?%3Z+;YnnYqTfG>YXrHI z6_cd4I!v{tNn}nQ)Jk+3{e~(1@;ftK31L&Z(uBGxNFoK}rr-=L?>IjNP{X)P#rlVP zW|~T^V&SKzBGFE`FL|ERu!9JDr_uY-!v8f*$>HhgvU^hRbQH^ZD$G#4ojVi3{A32+ zrJ<K*C|m!78OqG18K|<Wzb`Xo&meK6HNtR>QI3(~Bk)l|We@C_2Xc1z86{_A_nNID zOs+@(|9S>I$P2O?!pon<dV2PlMMbsn=vhj(UYMn1t96!AaIe|8;(!+E_B?u1p_6AU zz;J!Z9_7^08F%lIN*CaUmL^_VgcYpOBwbsCVRQl9Bl*0DpXp5JJfjxgJa7rzSfeV8 zB_XJZ%St?L_b?dG+xA)ue{l)bsFTNJ;}deMBO58KtWt{1K@ZhT3dzMpxpss>(z<1+ z)iYBS4==zR*7%0i+>Qb=*Q5$ZN=ax@g;o;K#4i`ndD1AuARh1v^uf(aOg49n8o|>T zsphzupRa{K87wNIbKEX%dj)~EWp7!}eOhMdTUJpBgN>ku`M(e%?$<yD7Z%EPWpyN& z3gI83dTYDw3WC4=x=~*xoiD@zx<f1e2C7umLjiVTCqE>|L$Bo`jAUh1l5QOi$TdE{ z9)nyfZYdXvFu}oSkw*-yj7vT1@u-e9?2&>uBFBN5D*I)-UKNyVH>!f1ZC0<Q{x?}L zSp^RmzwcHoMAL<<$x3&J@l1)_6-Ifmg=cKWV4UCuR;bxbr$CEzVKco2(JA?DrTa^e zrBdl4TO#_RD|<uQQHC)N)Gudu(k+0}K^V%j-@*pzl!)tYG;??#y`@=WHAf-!=)FHs zYqZEc;lqVmdhS%7-F3$ItzHGw@orJZJZS7S#nVI@YpIs=;OlnVN*hT-P1YfPpq6^8 zth#@zQxCZHmH6Sk6<Vh>oC;6ZDV6W6Q`^9YU#cVbn)$*17<qu>WNT~wGwniKGxl|B z_`@9udZ20gF0-a;9#%(}fM$Nbi}KcZa8;PS)#|II;cw%jKiVaS&jGEiDeYzJb3+oo zzz{r0aPA}U%;U-~EK)$?$1wx(59|kBAKkjGZH5N)O>oNUAlKrXN)v`q$(nh}NjdA% z?34J?m0S(`7F~|CKis}kUfj}mc%Ri*NK3xQ5-n@2;9s}Gk7r+j2Y0&!7iOxcxCB2h zQ^kE9=_=^>3s>MPrCg%-6PhI6hI2CJT)`imn2AdsopiUNUVEua-ee}es|(?DgnZNm zU;0;4=?`1s)G(-n9Qy@geLG41KO;)s^{P1U7hKjdlSvwPhmKfQZ;}dnG0YL|y!IX) zPA$?Szai1nW!0Lj%;ou6YnCsUUi4rm;&G7=oxtWB4ls=-LZi{N>pe9y2`fo9^pmUj zI}_M9{N4mMURpbmjbNyh-kHRPfc`1?CXI;<^Q6V;EZ)U|zCw6G+<-yK6H`8lD34l> z<;+|9X(l_PMFKa^VTlCS=de^HNF$zO&oL6W%w>}Z&d+5dXq3%kZS=K<Uzx9vhAv<R zdFhl)_7p*7CL8TU=AyH+R%>}m7JGyS?`5)K1W}9Fa8fXP5j!ud9h%ig4Yg5sWGU1m zSqilyi`|xO{XLt-yUU+aG8eC2x;Arl*03mUT)~2ADmjPIhZCNY!=jzlk1a7#eCJvw z(CFtmtWHOxOpcx8^-Ec(EbFXco0qaGQeUx}1t=_gUbF&F&SN2RigdxUGGDP+t0UhF zSF;M)A<wlehG5)U7DljeEsG}DwpNa|VkL`{J;4So{54{Ro1ZjeCEF~oYI#Y{Aa~1S u^u>oK<|)aUn<qysZOUWqa#oh-E1IkGSv*PK<jX7m{|+jj(b}f5*nb0q70ufK delta 4997 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 diff --git a/plugins/official/key-promoter/dist/plugin.wasm.sources b/plugins/official/key-promoter/dist/plugin.wasm.sources index 3d0ab2bcf..61effb082 100644 --- a/plugins/official/key-promoter/dist/plugin.wasm.sources +++ b/plugins/official/key-promoter/dist/plugin.wasm.sources @@ -1 +1 @@ -9fca272eec5a601b8de6071aa358954e9b79db08f45714f077624418e8f616bc +cb4ed7e9d793df4219e92edd5d64616db65411cb77e111df862ca8237f647efb diff --git a/plugins/official/license-header/dist/plugin.wasm b/plugins/official/license-header/dist/plugin.wasm index 57a9de4964476178cbc9b2969a5c38339255e45a..7f13fe3caa0d33274a151adead6d833e34d96052 100644 GIT binary patch delta 22034 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 G>;D7sByvvx delta 21790 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 diff --git a/plugins/official/license-header/dist/plugin.wasm.sources b/plugins/official/license-header/dist/plugin.wasm.sources index 60a829496..c7b41d834 100644 --- a/plugins/official/license-header/dist/plugin.wasm.sources +++ b/plugins/official/license-header/dist/plugin.wasm.sources @@ -1 +1 @@ -5adfc0d749feabe5fe7e20857fd636d01341b91f3dd8440779af6dcd14435619 +f43f1e52813a5f992c3d06cbe03aa26092dd56db45322b090906dfbb8d08b1ab diff --git a/plugins/official/mybatis/dist/plugin.wasm b/plugins/official/mybatis/dist/plugin.wasm index 97883f1e223c529d39123589a2cca4933700a2dc..14b74de757763b504494c66a6c76a65e9c7746a6 100644 GIT binary patch delta 5780 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 E2iBI%Q2+n{ delta 5765 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 diff --git a/plugins/official/mybatis/dist/plugin.wasm.sources b/plugins/official/mybatis/dist/plugin.wasm.sources index bcefdb21e..ecf858484 100644 --- a/plugins/official/mybatis/dist/plugin.wasm.sources +++ b/plugins/official/mybatis/dist/plugin.wasm.sources @@ -1 +1 @@ -1b388c40cb6be29c5e10e938c0c8f6848d10cb4bf1f7438db37f53b13314708e +656b51f57467a75b5416575871a5722e4f44be98407f6bfd0e6fa1e44f0bfa80 diff --git a/plugins/official/regex-tester/dist/plugin.wasm b/plugins/official/regex-tester/dist/plugin.wasm index 6974f2cff3fa92581a03ee4c4b3b0974ef570030..686d70ed3e57a776c25b2d6000a4376a6a3c4d4b 100644 GIT binary patch delta 5945 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 R&iF)|{~r@#@A#F0{{b=Haozv` delta 5943 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 diff --git a/plugins/official/regex-tester/dist/plugin.wasm.sources b/plugins/official/regex-tester/dist/plugin.wasm.sources index fd050c58b..5044baccb 100644 --- a/plugins/official/regex-tester/dist/plugin.wasm.sources +++ b/plugins/official/regex-tester/dist/plugin.wasm.sources @@ -1 +1 @@ -af4478864196b5802f505d26beafe4492bc1335536311bc873b9d18773f9eebb +bafe2cfc88d8c1272eea289d3d804287d56159b1c5893147b73a6be88e00370f diff --git a/plugins/official/solidity/dist/plugin.wasm b/plugins/official/solidity/dist/plugin.wasm index f73c4c5d52411f238a174f8290425b700c95c61c..4b490f21df9c0542684ffd452e3d8fcf3a4984e1 100644 GIT binary patch delta 5056 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 delta 5056 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 diff --git a/plugins/official/solidity/dist/plugin.wasm.sources b/plugins/official/solidity/dist/plugin.wasm.sources index b414f0c96..49b5d5e91 100644 --- a/plugins/official/solidity/dist/plugin.wasm.sources +++ b/plugins/official/solidity/dist/plugin.wasm.sources @@ -1 +1 @@ -46ce9f2d5b4f87a4be26facea4870f3de59952dc95c536fcf33fb0ff0a7c27a7 +ee5d474028c7068b06771efa5e8e2fa710cd0108d53737a36d8bf9f0f3490c08 diff --git a/plugins/official/string-tools/dist/plugin.wasm b/plugins/official/string-tools/dist/plugin.wasm index e9871c762b3e2072b3bb81b10280083d200efda2..5912af33d73279b69ba64f69f9ab64f8f5102f3e 100644 GIT binary patch delta 5129 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 Z>q(G1UnPEXKI<(%!2SQTbK@cw{9ilhSn&V= delta 5133 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 Uc*p|QSN;h1|If}}UBp8F3xy;u(EtDd diff --git a/plugins/official/string-tools/dist/plugin.wasm.sources b/plugins/official/string-tools/dist/plugin.wasm.sources index 2d44ef4df..1ddc444e0 100644 --- a/plugins/official/string-tools/dist/plugin.wasm.sources +++ b/plugins/official/string-tools/dist/plugin.wasm.sources @@ -1 +1 @@ -f4e37cdac925f376bac945d5bb86c648b559787aa22737e6440835456f9f74cf +867fc7bba0e795155e96c39f5a05cc3a08a3b6809c4e3922437e393d65f6d829 diff --git a/plugins/official/test-runner/Cargo.lock b/plugins/official/test-runner/Cargo.lock new file mode 100644 index 000000000..48e52f503 --- /dev/null +++ b/plugins/official/test-runner/Cargo.lock @@ -0,0 +1,336 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "openidea-plugin" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "openidea-plugin-openidea-test-runner" +version = "0.1.0" +dependencies = [ + "openidea-plugin", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/plugins/official/test-runner/Cargo.toml b/plugins/official/test-runner/Cargo.toml new file mode 100644 index 000000000..282ebf51e --- /dev/null +++ b/plugins/official/test-runner/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "openidea-plugin-openidea-test-runner" +version = "0.1.0" +edition = "2024" +publish = false + +# A plugin builds on its own (target wasm32-wasip2), outside any IDE workspace. +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openidea-plugin = { path = "../../../sdk/rust/openidea-plugin" } + +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 +panic = "abort" diff --git a/plugins/official/test-runner/README.md b/plugins/official/test-runner/README.md new file mode 100644 index 000000000..92e9902da --- /dev/null +++ b/plugins/official/test-runner/README.md @@ -0,0 +1,25 @@ +# test-runner — API 1.4 example + +Put this directory on PATH **before installing/granting** the plugin, so +`process:openidea-example-tests` resolves to the included executable. Copy +`example.tests` to a trusted project. Its presence enables the dynamic +`run-configurations` export. Open it as plain text and click a gutter run +button, or use **Example: Run Tests** / the Run selector for all tests. + +The gutter command appends the test name as a filter to `run.start`. The fake +executable prints a deliberately tiny `start NAME` / `pass NAME` protocol; +`test-results.parse` retains split lines and emits stable test ids. Replace +that protocol/parser and executable to integrate a real framework. This is an +API example, not a general test framework or a custom rerun-failed contract. + +Build from the repository root: + +```sh +plugins/build.sh official/test-runner +cargo test --manifest-path plugins/official/test-runner/Cargo.toml +cargo run -p ide-host --bin openidea-plugin -- check plugins/official/test-runner +``` + +Install the built package folder through Settings › Plugins › Install from +Disk. See the [plugin guide](../../README.md#api-14) and +[official/community policy](../../README.md#81-official-and-community-plugins). diff --git a/plugins/official/test-runner/dist/plugin.wasm b/plugins/official/test-runner/dist/plugin.wasm new file mode 100644 index 0000000000000000000000000000000000000000..57025d99e2a1c824e629520fe5406aae2fdd59fb GIT binary patch literal 194786 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̓E`a6+2}nT|F6V82>FZ84RDQmy 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 literal 0 HcmV?d00001 diff --git a/plugins/official/test-runner/dist/plugin.wasm.sources b/plugins/official/test-runner/dist/plugin.wasm.sources new file mode 100644 index 000000000..273f94c58 --- /dev/null +++ b/plugins/official/test-runner/dist/plugin.wasm.sources @@ -0,0 +1 @@ +b7aaac21c6c34d9d722aa23c13c66e61ef11028f038fa877fe67d03b03c18483 diff --git a/plugins/official/test-runner/example.tests b/plugins/official/test-runner/example.tests new file mode 100644 index 000000000..f3b70fe37 --- /dev/null +++ b/plugins/official/test-runner/example.tests @@ -0,0 +1,2 @@ +test adds +test subtracts diff --git a/plugins/official/test-runner/openidea-example-tests b/plugins/official/test-runner/openidea-example-tests new file mode 100755 index 000000000..0c675c486 --- /dev/null +++ b/plugins/official/test-runner/openidea-example-tests @@ -0,0 +1,10 @@ +#!/bin/sh +# A tiny deterministic protocol, not a test framework. +while IFS= read -r line; do + case "$line" in + 'test '*) name=${line#test } ;; + *) continue ;; + esac + [ -z "$1" ] || [ "$1" = "$name" ] || continue + printf 'start %s\npass %s\n' "$name" "$name" +done < example.tests diff --git a/plugins/official/test-runner/plugin.toml b/plugins/official/test-runner/plugin.toml new file mode 100644 index 000000000..7d33f9db1 --- /dev/null +++ b/plugins/official/test-runner/plugin.toml @@ -0,0 +1,20 @@ +id = "openidea.test-runner" +name = "Test Runner (API Example)" +version = "0.1.0" +description = "Small plugin API 1.4 example; see the package README." +authors = ["OpenIDEA contributors"] +license = "Apache-2.0" +engines.openidea = "^1.4" +wasm = "dist/plugin.wasm" +activation = ["on-command:*"] + +[capabilities] +required = ["process:openidea-example-tests"] +[[contributes.commands]] +id = "test" +title = "Example: Run Tests" +[[contributes.runConfigurations]] +files = ["example.tests"] +[[contributes.providers]] +kinds = ["gutter-actions"] +languages = ["plaintext", "text"] diff --git a/plugins/official/test-runner/src/lib.rs b/plugins/official/test-runner/src/lib.rs new file mode 100644 index 000000000..4156d15e7 --- /dev/null +++ b/plugins/official/test-runner/src/lib.rs @@ -0,0 +1,99 @@ +//! Lines beginning `test ` get a gutter button; the executable accepts the name as a filter. +use openidea_plugin::language_features::{Document, GutterAction, LanguageFeatures}; +use openidea_plugin::{ + Error, Invocation, Plugin, PluginRunConfiguration, PluginTestEvent, RunConfigurations, TestFailed, TestPassed, + TestResults, TestStart, host, register_plugin, +}; +#[derive(Default)] +pub struct TestRunner { + pending: String, +} +impl Plugin for TestRunner { + fn run_command(&mut self, _id: &str, _ctx: &Invocation, arg: Option<&str>) -> Result<Option<String>, Error> { + let args = arg.into_iter().map(str::to_owned).collect::<Vec<_>>(); + host::start_run("tests", &args, false).map(Some) + } +} +impl RunConfigurations for TestRunner { + fn configurations(&mut self, _root: &str) -> Vec<PluginRunConfiguration> { + vec![PluginRunConfiguration { + id: "tests".into(), + name: "Example tests".into(), + program: "openidea-example-tests".into(), + test: true, + framework: Some("plugin".into()), + ..Default::default() + }] + } +} +impl LanguageFeatures for TestRunner { + fn gutter_actions(&mut self, doc: &Document) -> Vec<GutterAction> { + if !doc.path.ends_with("example.tests") { + return vec![]; + } + doc.text + .lines() + .enumerate() + .filter_map(|(line, text)| { + let name = text.strip_prefix("test ")?.trim(); + Some(GutterAction { + line: line as u32, + icon: Some("run".into()), + tooltip: format!("Run {name}"), + command: "test".into(), + argument: Some(name.into()), + }) + }) + .collect() + } +} +impl TestResults for TestRunner { + fn parse(&mut self, _config: &str, chunk: &str) -> Vec<PluginTestEvent> { + self.pending.push_str(chunk); + let mut events = vec![]; + while let Some(end) = self.pending.find('\n') { + let line: String = self.pending.drain(..=end).collect(); + let Some((kind, name)) = line.trim().split_once(' ') else { continue }; + let id = name.to_owned(); + match kind { + "start" => events.push(PluginTestEvent::TestStarted(TestStart { + id, + parent: None, + name: name.into(), + location: None, + })), + "pass" => events.push(PluginTestEvent::Passed(TestPassed { id, duration_ms: None })), + "fail" => events.push(PluginTestEvent::Failed(TestFailed { + id, + duration_ms: None, + message: "Example failure".into(), + expected: None, + actual: None, + location: None, + })), + _ => {} + } + } + if self.pending.len() > 64 * 1024 { + self.pending.clear(); + } + events + } +} +register_plugin!(TestRunner, run_configurations, test_results, language_features); +#[cfg(test)] +mod tests { + use super::*; + use openidea_plugin::mock::MockHost; + #[test] + fn filters_reach_runs_and_split_output_builds_the_test_tree() { + let mock = MockHost::install().with_capabilities(&["process:openidea-example-tests"]); + let mut plugin = TestRunner::default(); + mock.set_run_configurations(plugin.configurations("/project")); + plugin.run_command("test", &mock.invocation(), Some("adds")).unwrap(); + assert_eq!(mock.configuration_runs()[0].2, vec!["adds"]); + assert!(plugin.parse("tests", "sta").is_empty()); + let events = plugin.parse("tests", "rt adds\npass adds\n"); + assert!(matches!(&events[..], [PluginTestEvent::TestStarted(_), PluginTestEvent::Passed(_)])); + } +} diff --git a/plugins/official/todo-finder/Cargo.lock b/plugins/official/todo-finder/Cargo.lock new file mode 100644 index 000000000..11ac1af6d --- /dev/null +++ b/plugins/official/todo-finder/Cargo.lock @@ -0,0 +1,337 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "openidea-plugin" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "openidea-plugin-openidea-todo-finder" +version = "0.1.0" +dependencies = [ + "openidea-plugin", + "serde_json", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/plugins/official/todo-finder/Cargo.toml b/plugins/official/todo-finder/Cargo.toml new file mode 100644 index 000000000..3c4c0f009 --- /dev/null +++ b/plugins/official/todo-finder/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "openidea-plugin-openidea-todo-finder" +version = "0.1.0" +edition = "2024" +publish = false + +# A plugin builds on its own (target wasm32-wasip2), outside any IDE workspace. +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openidea-plugin = { path = "../../../sdk/rust/openidea-plugin" } + +serde_json = "1" + +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 +panic = "abort" diff --git a/plugins/official/todo-finder/README.md b/plugins/official/todo-finder/README.md new file mode 100644 index 000000000..b33ae82e6 --- /dev/null +++ b/plugins/official/todo-finder/README.md @@ -0,0 +1,23 @@ +# todo-finder — API 1.4 example + +Run **Example: Find TODOs**, including from a search result's context menu. +`project.search-text` supplies a list view; activating a row opens its location. +The invoking window's status item combines the match count with +`diagnostics.problems`. Both search and rendering use the invocation's project, +so the example keeps no cross-window cache. Results are capped at 500. + +Grant: `project.read`. Remote Problems snapshots are currently empty; text +search still runs on the server. This is a small view/API example, not a TODO +indexer or a replacement for Find in Files. + +Build from the repository root: + +```sh +plugins/build.sh official/todo-finder +cargo test --manifest-path plugins/official/todo-finder/Cargo.toml +cargo run -p ide-host --bin openidea-plugin -- check plugins/official/todo-finder +``` + +Install the built package folder through Settings › Plugins › Install from +Disk. See the [plugin guide](../../README.md#api-14) and +[official/community policy](../../README.md#81-official-and-community-plugins). diff --git a/plugins/official/todo-finder/dist/plugin.wasm b/plugins/official/todo-finder/dist/plugin.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b548e7ef04ae1d686770d4de2f44e763558906ef GIT binary patch literal 192815 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 literal 0 HcmV?d00001 diff --git a/plugins/official/todo-finder/dist/plugin.wasm.sources b/plugins/official/todo-finder/dist/plugin.wasm.sources new file mode 100644 index 000000000..ef43b99b8 --- /dev/null +++ b/plugins/official/todo-finder/dist/plugin.wasm.sources @@ -0,0 +1 @@ +66e92c09c0050dbf4c302793eea5048e21d1c91b9b44811b5f7edbae6eaa9075 diff --git a/plugins/official/todo-finder/plugin.toml b/plugins/official/todo-finder/plugin.toml new file mode 100644 index 000000000..d3238e783 --- /dev/null +++ b/plugins/official/todo-finder/plugin.toml @@ -0,0 +1,23 @@ +id = "openidea.todo-finder" +name = "Todo Finder (API Example)" +version = "0.1.0" +description = "Small plugin API 1.4 example; see the package README." +authors = ["OpenIDEA contributors"] +license = "Apache-2.0" +engines.openidea = "^1.4" +wasm = "dist/plugin.wasm" +activation = ["on-command:*"] + +[capabilities] +required = ["project.read"] +[[contributes.commands]] +id = "find" +title = "Example: Find TODOs" +[[contributes.views]] +id = "todos" +title = "TODO Example" +icon = "search" +location = "right" +[[contributes.menus]] +menu = "searchResults.context" +command = "find" diff --git a/plugins/official/todo-finder/src/lib.rs b/plugins/official/todo-finder/src/lib.rs new file mode 100644 index 000000000..6f759635b --- /dev/null +++ b/plugins/official/todo-finder/src/lib.rs @@ -0,0 +1,79 @@ +//! A view searches its invoking project afresh, so instance state never mixes windows. +use openidea_plugin::views::{Body, ListItem, View, ViewEvent, Viewport, Views}; +use openidea_plugin::{Error, Invocation, Location, Plugin, StatusItem, TextQuery, host, register_plugin}; +#[derive(Default)] +pub struct TodoFinder; +fn query() -> TextQuery { + TextQuery { pattern: "TODO".into(), case_sensitive: true, ..Default::default() } +} +impl Plugin for TodoFinder { + fn run_command(&mut self, _id: &str, ctx: &Invocation, _arg: Option<&str>) -> Result<Option<String>, Error> { + let found = ctx.project().ok_or("Open a project first")?.search_text(&query(), 500)?; + let problems: usize = host::problems(None)?.iter().map(|p| p.diagnostics.len()).sum(); + host::set_window_status_item( + "todos", + &StatusItem { + text: format!("{} TODOs · {problems} problems", found.matches.len()), + tooltip: None, + command: Some("find".into()), + }, + )?; + host::refresh_view("todos"); + host::reveal_view("todos")?; + Ok(None) + } +} +impl Views for TodoFinder { + fn render(&mut self, _view: &str, _viewport: Option<Viewport>, ctx: &Invocation) -> Result<View, Error> { + let found = ctx.project().ok_or("Open a project first")?.search_text(&query(), 500)?; + let items = found + .matches + .into_iter() + .map(|m| { + ListItem::new(&serde_json::to_string(&m.location).unwrap(), &m.line).detail(&format!( + "{}:{}", + m.location.path, + m.location.range.start.line + 1 + )) + }) + .collect(); + Ok(View { + toolbar: vec![], + body: Body::List(items), + status: found.limit_hit.then(|| "Showing the first 500 matches".into()), + }) + } + fn handle(&mut self, _view: &str, event: ViewEvent, _ctx: &Invocation) -> Result<(), Error> { + if let ViewEvent::Activate(key) = event { + let location: Location = serde_json::from_str(&key).map_err(|e| Error::Invalid(e.to_string()))?; + host::open_location(&location.path, location.range.start.line, location.range.start.character)?; + } + Ok(()) + } +} +register_plugin!(TodoFinder, views); +#[cfg(test)] +mod tests { + use super::*; + use openidea_plugin::{Range, TextMatch, TextSearch, mock::MockHost}; + #[test] + fn search_results_open_their_location_and_count_problems() { + let mock = MockHost::install(); + mock.on_search(|q, _| { + assert_eq!(q.pattern, "TODO"); + Ok(TextSearch { + matches: vec![TextMatch { + location: Location { path: "/project/a.txt".into(), range: Range::default() }, + line: "TODO fix".into(), + }], + limit_hit: false, + }) + }); + TodoFinder.run_command("find", &mock.invocation(), None).unwrap(); + assert_eq!(mock.status_items()["todos"].text, "1 TODOs · 0 problems"); + let view = TodoFinder.render("todos", None, &mock.invocation()).unwrap(); + let Body::List(items) = view.body else { panic!() }; + TodoFinder.handle("todos", ViewEvent::Activate(items[0].key.clone()), &mock.invocation()).unwrap(); + assert_eq!(mock.opened_locations()[0], ("/project/a.txt".into(), 0, 0)); + } +} diff --git a/sdk/rust/openidea-plugin/src/execution.rs b/sdk/rust/openidea-plugin/src/execution.rs new file mode 100644 index 000000000..e8866312d --- /dev/null +++ b/sdk/rust/openidea-plugin/src/execution.rs @@ -0,0 +1,193 @@ +//! Runs, keyed terminals and plugin-owned content editors (API 1.4). + +use serde::{Deserialize, Serialize}; + +/// ContentAction data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ContentAction { + pub id: String, + pub label: String, + pub icon: Option<String>, + pub primary: bool, + pub save: bool, +} + +/// ContentTab data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ContentTab { + pub key: String, + pub title: String, + pub file_name: Option<String>, + pub language: Option<String>, + pub content: String, + pub read_only: bool, + pub actions: Vec<ContentAction>, +} + +/// ContentDiff data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ContentDiff { + pub left: String, + pub right: String, + pub left_label: Option<String>, + pub right_label: Option<String>, + pub title: Option<String>, +} + +/// ContentTabReply data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ContentTabReply { + pub content: Option<String>, + pub saved: Option<bool>, + pub read_only: Option<bool>, + pub status: Option<String>, + pub error: Option<String>, + pub diff: Option<ContentDiff>, +} + +/// PluginRunConfiguration data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginRunConfiguration { + pub id: String, + pub name: String, + pub program: String, + pub args: Vec<String>, + pub cwd: Option<String>, + pub test: bool, + pub framework: Option<String>, +} + +/// RunExited data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RunExited { + pub id: String, + pub config: String, + pub status: Option<i32>, + pub killed: bool, +} + +/// TerminalExited data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TerminalExited { + pub id: String, + pub key: String, + pub status: Option<i32>, + pub closed: bool, +} + +/// ContentTabAction data exchanged with the IDE. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ContentTabAction { + pub request_id: String, + pub key: String, + pub action: String, + pub content: String, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TestStart { + pub id: String, + pub parent: Option<String>, + pub name: String, + pub location: Option<crate::Location>, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TestPassed { + pub id: String, + pub duration_ms: Option<u32>, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TestFailed { + pub id: String, + pub duration_ms: Option<u32>, + pub message: String, + pub expected: Option<String>, + pub actual: Option<String>, + pub location: Option<crate::Location>, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TestSkipped { + pub id: String, + pub reason: Option<String>, +} + +/// Incremental test tree data. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TestOutput { + pub id: String, + pub text: String, +} + +/// A test update, carried by the optional test-results export. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum PluginTestEvent { + SuiteStarted(TestStart), + TestStarted(TestStart), + Passed(TestPassed), + Failed(TestFailed), + Skipped(TestSkipped), + Output(TestOutput), + Other { kind: String, data: String }, +} + +/// Supplies complete project configurations; register with run_configurations. +pub trait RunConfigurations { + fn configurations(&mut self, project_root: &str) -> Vec<PluginRunConfiguration>; +} +/// Parses bounded test output batches; register with test_results. +pub trait TestResults { + fn parse(&mut self, config: &str, chunk: &str) -> Vec<PluginTestEvent>; +} + +impl crate::Event { + /// Completion of a run started by this plugin, even after idle reclamation. + pub fn run_exited(&self) -> Option<RunExited> { + self.execution_event("run-exited") + } + /// Exactly one completion for a keyed terminal operation. + pub fn terminal_exited(&self) -> Option<TerminalExited> { + self.execution_event("terminal-exited") + } + /// A toolbar action or Save from this plugin's wasm content tab. + pub fn content_tab_action(&self) -> Option<ContentTabAction> { + self.execution_event("content-tab-action") + } + fn execution_event<T: serde::de::DeserializeOwned>(&self, expected: &str) -> Option<T> { + let Self::Other { kind, data } = self else { return None }; + (kind == expected).then(|| serde_json::from_str(data).ok()).flatten() + } +} + +/// A plugin-owned argv wrapper for project runs (API 1.4). +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginRunTarget { + pub id: String, + pub name: String, + pub program: String, + pub args: Vec<String>, + pub workdir_args: Vec<String>, + pub container_root: Option<String>, + pub commands: Vec<(String, Vec<String>)>, +} diff --git a/sdk/rust/openidea-plugin/src/host.rs b/sdk/rust/openidea-plugin/src/host.rs index 0fe71b92b..b492677a0 100644 --- a/sdk/rust/openidea-plugin/src/host.rs +++ b/sdk/rust/openidea-plugin/src/host.rs @@ -91,6 +91,37 @@ mod imp { wit::ui::set_status_item(id, &item) } + pub fn set_window_status_item(id: &str, item: &StatusItem) -> Result<(), Error> { + wit::ui::set_window_status_item( + id, + &wit::ui::StatusItem { + text: item.text.clone(), + tooltip: item.tooltip.clone(), + command: item.command.clone(), + }, + ) + .map_err(error) + } + pub fn remove_window_status_item(id: &str) -> Result<(), Error> { + wit::ui::remove_window_status_item(id).map_err(error) + } + pub fn execute_command(id: &str, arg: Option<&str>) -> Result<Option<String>, Error> { + wit::command_runner::execute(id, arg).map_err(error) + } + pub type ConnectionRes = wit::ipc::Connection; + pub fn ipc_connect(name: &str) -> Result<ConnectionRes, Error> { + wit::ipc::connect(name).map_err(error) + } + pub fn ipc_write(c: &ConnectionRes, bytes: &[u8]) -> Result<(), Error> { + c.write(bytes).map_err(error) + } + pub fn ipc_read(c: &ConnectionRes, max: u32, timeout: u32) -> Result<Vec<u8>, Error> { + c.read(max, timeout).map_err(error) + } + pub fn ipc_close(c: &ConnectionRes) -> Result<(), Error> { + c.close().map_err(error) + } + pub fn remove_status_item(id: &str) { wit::ui::remove_status_item(id) } @@ -232,6 +263,150 @@ mod imp { e.apply_edits(version, &edits).map_err(error) } + pub fn open_editors() -> Result<Vec<OpenEditor>, Error> { + wit::editor::open_editors() + .map(|v| { + v.into_iter() + .map(|e| OpenEditor { path: e.path, language: e.language, dirty: e.dirty, active: e.active }) + .collect() + }) + .map_err(error) + } + pub fn editor_open(path: &str, selection: Option<Range>) -> Result<(), Error> { + wit::editor::open(path, selection.map(range_out)).map_err(error) + } + pub fn editor_set_selections(e: &EditorRes, ranges: &[Range]) -> Result<(), Error> { + e.set_selections(&ranges.iter().copied().map(range_out).collect::<Vec<_>>()).map_err(error) + } + pub fn editor_reveal(e: &EditorRes, range: Range) -> Result<(), Error> { + e.reveal(range_out(range)).map_err(error) + } + pub fn project_search_text(p: &ProjectRes, q: &TextQuery, limit: u32) -> Result<TextSearch, Error> { + let q = wit::workspace::TextQuery { + pattern: q.pattern.clone(), + regex: q.regex, + case_sensitive: q.case_sensitive, + whole_word: q.whole_word, + include: q.include.clone(), + exclude: q.exclude.clone(), + }; + p.search_text(&q, limit) + .map(|s| TextSearch { + limit_hit: s.limit_hit, + matches: s + .matches + .into_iter() + .map(|m| TextMatch { + location: Location { path: m.location.path, range: range_in(m.location.range) }, + line: m.line, + }) + .collect(), + }) + .map_err(error) + } + fn diagnostic_in(d: wit::diagnostics::Diagnostic) -> Diagnostic { + Diagnostic { + range: range_in(d.range), + severity: match d.severity { + wit::diagnostics::Severity::Error => Severity::Error, + wit::diagnostics::Severity::Warning => Severity::Warning, + wit::diagnostics::Severity::Info => Severity::Info, + wit::diagnostics::Severity::Hint => Severity::Hint, + }, + message: d.message, + code: d.code, + source: d.source, + unnecessary: d.tags.iter().any(|s| s == "unnecessary"), + deprecated: d.tags.iter().any(|s| s == "deprecated"), + related: d + .related + .into_iter() + .map(|r| RelatedInformation { + location: Location { path: r.location.path, range: range_in(r.location.range) }, + message: r.message, + }) + .collect(), + } + } + pub fn problems(path: Option<&str>) -> Result<Vec<FileProblems>, Error> { + wit::diagnostics::problems(path) + .map(|v| { + v.into_iter() + .map(|f| FileProblems { + path: f.path, + source: f.source, + diagnostics: f.diagnostics.into_iter().map(diagnostic_in).collect(), + }) + .collect() + }) + .map_err(error) + } + pub fn set_decorations(path: &str, set: &str, items: &[Decoration]) -> Result<(), Error> { + let items = items + .iter() + .map(|d| wit::decorations::Decoration { + range: range_out(d.range), + style: d.style.clone(), + hover: d.hover.clone(), + after: d.after.clone(), + }) + .collect::<Vec<_>>(); + wit::decorations::set(path, set, &items).map_err(error) + } + pub fn clear_decorations(set: &str) -> Result<(), Error> { + wit::decorations::clear(set).map_err(error) + } + pub fn set_file_decoration(path: &str, set: &str, item: &FileDecoration) -> Result<(), Error> { + wit::decorations::set_file( + path, + set, + &wit::decorations::FileDecoration { + badge: item.badge.clone(), + style: item.style.clone(), + tooltip: item.tooltip.clone(), + }, + ) + .map_err(error) + } + pub fn clear_file_decorations(set: &str) -> Result<(), Error> { + wit::decorations::clear_file(set).map_err(error) + } + pub fn vcs_log(p: &ProjectRes, path: Option<&str>, limit: u32) -> Result<Vec<CommitInfo>, Error> { + wit::vcs::log(p, path, limit) + .map(|v| { + v.into_iter() + .map(|c| CommitInfo { + id: c.id, + short_id: c.short_id, + author: c.author, + author_email: c.author_email, + date: c.date, + subject: c.subject, + body: c.body, + parents: c.parents, + }) + .collect() + }) + .map_err(error) + } + pub fn vcs_blame(p: &ProjectRes, path: &str) -> Result<Vec<BlameLine>, Error> { + wit::vcs::blame(p, path) + .map(|v| { + v.into_iter() + .map(|c| BlameLine { + line: c.line, + commit: c.commit, + author: c.author, + date: c.date, + summary: c.summary, + }) + .collect() + }) + .map_err(error) + } + pub fn vcs_diff(p: &ProjectRes, path: Option<&str>, against: Option<&str>) -> Result<Option<String>, Error> { + wit::vcs::diff(p, path, against).map_err(error) + } pub fn vcs_status(p: &ProjectRes) -> Result<Option<RepositoryStatus>, Error> { wit::vcs::status(p) .map(|s| { @@ -435,6 +610,31 @@ mod imp { .map_err(error) } + pub fn secret_set(name: &str, value: &str) -> Result<(), Error> { + wit::secrets::set(name, value).map_err(error) + } + pub fn secret_delete(name: &str) -> Result<(), Error> { + wit::secrets::delete(name).map_err(error) + } + pub type LoopbackRes = wit::auth::Loopback; + pub fn begin_loopback(path: &str) -> Result<LoopbackRes, Error> { + wit::auth::begin_loopback(path).map_err(error) + } + pub fn loopback_uri(l: &LoopbackRes) -> String { + l.redirect_uri() + } + pub fn loopback_wait(l: &LoopbackRes, timeout_ms: u32) -> Result<Option<String>, Error> { + l.wait(timeout_ms).map_err(error) + } + pub fn output_append(channel: &str, text: &str) -> Result<(), Error> { + wit::output::append(channel, text).map_err(error) + } + pub fn output_clear(channel: &str) -> Result<(), Error> { + wit::output::clear(channel).map_err(error) + } + pub fn output_reveal(channel: &str) -> Result<(), Error> { + wit::output::reveal(channel).map_err(error) + } pub fn secret_is_set(name: &str) -> bool { wit::secrets::is_set(name) } @@ -447,6 +647,121 @@ mod imp { wit::views::post_message(view, message).map_err(error) } + fn content_action_to(v: &crate::ContentAction) -> wit::views::ContentAction { + wit::views::ContentAction { + id: v.id.clone(), + label: v.label.clone(), + icon: v.icon.clone(), + primary: v.primary, + save: v.save, + } + } + + fn content_tab_to(v: &crate::ContentTab) -> wit::views::ContentTab { + wit::views::ContentTab { + key: v.key.clone(), + title: v.title.clone(), + file_name: v.file_name.clone(), + language: v.language.clone(), + content: v.content.clone(), + read_only: v.read_only, + actions: v.actions.iter().map(content_action_to).collect(), + } + } + + fn content_diff_to(v: &crate::ContentDiff) -> wit::views::ContentDiff { + wit::views::ContentDiff { + left: v.left.clone(), + right: v.right.clone(), + left_label: v.left_label.clone(), + right_label: v.right_label.clone(), + title: v.title.clone(), + } + } + + fn content_tab_reply_to(v: &crate::ContentTabReply) -> wit::views::ContentTabReply { + wit::views::ContentTabReply { + content: v.content.clone(), + saved: v.saved, + read_only: v.read_only, + status: v.status.clone(), + error: v.error.clone(), + diff: v.diff.as_ref().map(content_diff_to), + } + } + + pub fn database_upsert(project: &ProjectRes, source: &PluginDataSource) -> Result<(), Error> { + wit::database::upsert_source( + project, + &wit::database::DataSource { + id: source.id.clone(), + name: source.name.clone(), + driver: source.driver.clone(), + host: source.host.clone(), + port: source.port, + database: source.database.clone(), + user: source.user.clone(), + url: source.url.clone(), + options: source.options.clone(), + }, + ) + .map_err(error) + } + pub fn database_remove(project: &ProjectRes, id: &str) -> Result<(), Error> { + wit::database::remove_source(project, id).map_err(error) + } + pub fn database_sources(project: &ProjectRes) -> Result<Vec<String>, Error> { + wit::database::sources(project).map_err(error) + } + pub fn database_credential(project: &ProjectRes, id: &str, password: &str) -> Result<(), Error> { + wit::database::set_credential(project, id, password).map_err(error) + } + pub fn database_connect(project: &ProjectRes, id: &str) -> Result<(), Error> { + wit::database::connect(project, id).map_err(error) + } + pub fn mcp_stdio_command() -> Option<Vec<String>> { + wit::mcp::stdio_command() + } + + pub fn register_run_target(project: &ProjectRes, target: &crate::PluginRunTarget) -> Result<(), Error> { + wit::run_targets::register( + project, + &wit::run_targets::RunTarget { + id: target.id.clone(), + name: target.name.clone(), + program: target.program.clone(), + args: target.args.clone(), + workdir_args: target.workdir_args.clone(), + container_root: target.container_root.clone(), + commands: target.commands.clone(), + }, + ) + .map_err(error) + } + pub fn unregister_run_target(project: &ProjectRes, id: &str) { + wit::run_targets::unregister(project, id); + } + pub fn start_run(config: &str, args: &[String], debug: bool) -> Result<String, Error> { + wit::run::start(config, args, debug).map_err(error) + } + pub fn open_terminal_session(key: &str, title: &str, cmd: &super::Command) -> Result<String, Error> { + wit::views::open_terminal_session( + key, + title, + &wit::process::Command { program: cmd.program.clone(), args: cmd.args.clone(), env: cmd.env.clone() }, + ) + .map_err(error) + } + pub fn write_terminal(key: &str, text: &str) -> Result<(), Error> { + wit::views::write_terminal(key, text).map_err(error) + } + pub fn open_content_tab(tab: &crate::ContentTab) -> Result<(), Error> { + wit::views::open_content_tab(&content_tab_to(tab)).map_err(error) + } + pub fn reply_content_tab(request: &str, reply: &crate::ContentTabReply) -> Result<(), Error> { + wit::views::reply_content_tab(request, &content_tab_reply_to(reply)).map_err(error) + } + pub fn open_terminal(title: &str, program: &str, args: &[String]) -> Result<(), Error> { wit::views::open_terminal(title, program, args).map_err(error) } @@ -585,6 +900,45 @@ pub struct Project { } impl Project { + /// Creates/replaces only this plugin's source. Requires database.sources and trust. + pub fn upsert_data_source(&self, source: &PluginDataSource) -> Result<(), Error> { + imp::database_upsert(&self.inner, source) + } + /// Removes an owned source and its credentials; other ids are not-found. + pub fn remove_data_source(&self, id: &str) -> Result<(), Error> { + imp::database_remove(&self.inner, id) + } + /// Lists only this plugin's private source ids. + pub fn data_sources(&self) -> Result<Vec<String>, Error> { + imp::database_sources(&self.inner) + } + /// Writes directly to the Database secure store, without a read API. + pub fn set_data_source_credential(&self, id: &str, password: &str) -> Result<(), Error> { + imp::database_credential(&self.inner, id, password) + } + /// Normal Database connection/introspection in this project's invoking command/event window. + pub fn connect_data_source(&self, id: &str) -> Result<(), Error> { + imp::database_connect(&self.inner, id) + } + + /// Find in Files with unsaved text, project excludes, cancellation and a 10,000 match cap. + /// Requires project.read; locations use zero-based lines and UTF-16 columns. + pub fn search_text(&self, query: &TextQuery, limit: u32) -> Result<TextSearch, Error> { + imp::project_search_text(&self.inner, query, limit) + } + /// Trusted project history, newest first, at most 1000 commits. Requires vcs.read. + pub fn vcs_log(&self, path: Option<&str>, limit: u32) -> Result<Vec<CommitInfo>, Error> { + imp::vcs_log(&self.inner, path, limit) + } + /// Trusted project attribution including unsaved text. Requires vcs.read. + pub fn vcs_blame(&self, path: &str) -> Result<Vec<BlameLine>, Error> { + imp::vcs_blame(&self.inner, path) + } + /// Disk working tree versus HEAD or a revision, failing above 4 MiB. Requires vcs.read and trust. + pub fn vcs_diff(&self, path: Option<&str>, against: Option<&str>) -> Result<Option<String>, Error> { + imp::vcs_diff(&self.inner, path, against) + } + /// Absolute path of the root. Needs `project.read`. pub fn root(&self) -> Result<String, Error> { imp::project_root(&self.inner) @@ -684,8 +1038,8 @@ pub fn publish_diagnostics(path: &str, items: &[Diagnostic]) -> Result<(), Error imp::publish_diagnostics(path, items) } -/// HTTP(S) to a host granted with `net:<host>` (plain http only to -/// localhost). `{{secret:<name>}}` in a header is filled in by the IDE (needs +/// HTTPS with `net:<host>`; unencrypted HTTP with `net.http:<host>` +/// (loopback HTTP retains `net:<host>`). `{{secret:<name>}}` in a header is filled in by the IDE (needs /// `secrets`). `Error::Cancelled` when the call was superseded. pub fn fetch(request: &HttpRequest) -> Result<HttpResponse, Error> { imp::fetch(request) @@ -869,6 +1223,15 @@ pub struct TextEditor { } impl TextEditor { + /// Changes this window's selections. Commands/events only; no capability needed. + pub fn set_selections(&self, ranges: &[Range]) -> Result<(), Error> { + imp::editor_set_selections(&self.inner, ranges) + } + /// Scrolls a range into view in this window. Commands/events only; no capability needed. + pub fn reveal(&self, range: Range) -> Result<(), Error> { + imp::editor_reveal(&self.inner, range) + } + pub fn path(&self) -> String { imp::editor_path(&self.inner) } @@ -1177,3 +1540,148 @@ pub fn notify_with_actions( pub fn __diagnostic_to(d: &Diagnostic) -> crate::bindings::openidea::plugin::diagnostics::Diagnostic { imp::diagnostic_to(d) } + +/// Overrides an item in the invoking window, until removed or the window/plugin closes. +pub fn set_window_status_item(id: &str, item: &StatusItem) -> Result<(), Error> { + imp::set_window_status_item(id, item) +} +/// Removes this window's status override, exposing the global item. +pub fn remove_window_status_item(id: &str) -> Result<(), Error> { + imp::remove_window_status_item(id) +} + +/// An approved byte stream on the project machine (the server in remote projects). +pub struct Connection { + inner: imp::ConnectionRes, +} +impl Connection { + /// Writes at most 1 MiB. + pub fn write(&self, bytes: &[u8]) -> Result<(), Error> { + imp::ipc_write(&self.inner, bytes) + } + /// Reads up to 1 MiB; empty on timeout or EOF. + pub fn read(&self, max_bytes: u32, timeout_ms: u32) -> Result<Vec<u8>, Error> { + imp::ipc_read(&self.inner, max_bytes, timeout_ms) + } + /// Closes this stream. Dropping it also closes it. + pub fn close(&self) -> Result<(), Error> { + imp::ipc_close(&self.inner) + } +} +/// Connects to a granted absolute socket path or Windows named pipe (API 1.4). +pub fn ipc_connect(name: &str) -> Result<Connection, Error> { + imp::ipc_connect(name).map(|inner| Connection { inner }) +} + +/// Executes a stable action or plugin command in the invoking window (API 1.4). +/// Requires high-risk `ide.commands`, except for this plugin's declared commands. +/// IDE actions return `None`; plugin commands return their result. Waiting instances +/// cannot re-enter themselves; command nesting is bounded. +pub fn execute_command(id: &str, arg: Option<&str>) -> Result<Option<String>, Error> { + imp::execute_command(id, arg) +} + +/// Stores a declared secret in the desktop keychain; requires `secrets`. +pub fn secret_set(name: &str, value: &str) -> Result<(), Error> { + imp::secret_set(name, value) +} +/// Deletes a declared secret; requires `secrets`. +pub fn secret_delete(name: &str) -> Result<(), Error> { + imp::secret_delete(name) +} +/// A one-shot browser redirect on the desktop, including for remote projects. +pub struct Loopback { + inner: imp::LoopbackRes, +} +impl Loopback { + /// The authorization flow's loopback redirect URI. + pub fn redirect_uri(&self) -> String { + imp::loopback_uri(&self.inner) + } + /// Awaits the raw query string; `None` on timeout. Waiting excludes the call budget. + pub fn wait(&self, timeout_ms: u32) -> Result<Option<String>, Error> { + imp::loopback_wait(&self.inner, timeout_ms) + } +} +/// Begins an OAuth redirect listener in the invoking window (no capability). +pub fn begin_loopback(path: &str) -> Result<Loopback, Error> { + imp::begin_loopback(path).map(|inner| Loopback { inner }) +} + +/// Appends to a plugin-owned Output tab's 1 MiB ring buffer. +pub fn output_append(channel: &str, text: &str) -> Result<(), Error> { + imp::output_append(channel, text) +} +/// Clears a plugin-owned output channel. +pub fn output_clear(channel: &str) -> Result<(), Error> { + imp::output_clear(channel) +} +/// Reveals a read-only Output tab in the invoking window. +pub fn output_reveal(channel: &str) -> Result<(), Error> { + imp::output_reveal(channel) +} + +/// The invoking window's open editors. Requires editor.read; commands/events only. +pub fn open_editors() -> Result<Vec<OpenEditor>, Error> { + imp::open_editors() +} +/// Opens a project file (project.read) or plugin-dir file, optionally selecting a range. +/// Only available in commands/events of a window. +pub fn open_editor(path: &str, selection: Option<Range>) -> Result<(), Error> { + imp::editor_open(path, selection) +} +/// The invoking window's project Problems view. Requires project.read. Empty for remote projects. +pub fn problems(path: Option<&str>) -> Result<Vec<FileProblems>, Error> { + imp::problems(path) +} +/// Replace a file's editor decorations. Requires project.read; 10,000 items/file, 50 sets. +pub fn set_decorations(path: &str, set: &str, items: &[Decoration]) -> Result<(), Error> { + imp::set_decorations(path, set, items) +} +/// Clear this plugin's editor decoration set in all projects. Requires project.read. +pub fn clear_decorations(set: &str) -> Result<(), Error> { + imp::clear_decorations(set) +} +/// Set a project-tree badge/style. Requires project.read. Badge is at most two characters. +pub fn set_file_decoration(path: &str, set: &str, item: &FileDecoration) -> Result<(), Error> { + imp::set_file_decoration(path, set, item) +} +/// Clear this plugin's tree decoration set in all projects. Requires project.read. +pub fn clear_file_decorations(set: &str) -> Result<(), Error> { + imp::clear_file_decorations(set) +} + +/// Starts an owned run configuration in the invoking window. Its id receives run-exited. +pub fn start_run(config: &str, args: &[String], debug: bool) -> Result<String, Error> { + imp::start_run(config, args, debug) +} +/// Opens/reuses a keyed, approved terminal with append-only environment variables. +pub fn open_terminal_session(key: &str, title: &str, cmd: &Command) -> Result<String, Error> { + imp::open_terminal_session(key, title, cmd) +} +/// Writes to this plugin's running keyed terminal in the invoking window. +pub fn write_terminal(key: &str, text: &str) -> Result<(), Error> { + imp::write_terminal(key, text) +} +/// Opens a content editor through the existing content-tab protocol. +pub fn open_content_tab(tab: &crate::ContentTab) -> Result<(), Error> { + imp::open_content_tab(tab) +} +/// Replies to a content-tab-action event in its originating window. +pub fn reply_content_tab(request: &str, reply: &crate::ContentTabReply) -> Result<(), Error> { + imp::reply_content_tab(request, reply) +} + +/// Registers/replaces a read-only project target. Requires the program's process grant. +pub fn register_run_target(project: &Project, target: &crate::PluginRunTarget) -> Result<(), Error> { + imp::register_run_target(&project.inner, target) +} +/// Withdraws a target owned by this plugin. +pub fn unregister_run_target(project: &Project, id: &str) { + imp::unregister_run_target(&project.inner, id); +} + +/// Stdio bridge argv for the invoking project, none while the built-in MCP server is off. +pub fn mcp_stdio_command() -> Option<Vec<String>> { + imp::mcp_stdio_command() +} diff --git a/sdk/rust/openidea-plugin/src/language_features.rs b/sdk/rust/openidea-plugin/src/language_features.rs new file mode 100644 index 000000000..683748f51 --- /dev/null +++ b/sdk/rust/openidea-plugin/src/language_features.rs @@ -0,0 +1,347 @@ +//! Optional API 1.4 language features and embedded language ranges. +pub use crate::providers::Document; +use crate::{Error, Location, Position, Range, TextEdit}; +/// A parameter label, as text or UTF-16 offsets within the signature. +#[derive(Clone, Debug, PartialEq)] +pub enum ParameterLabel { + Text(String), + Offsets((u32, u32)), +} +/// FormatOptions data. +#[derive(Clone, Debug, PartialEq)] +pub struct FormatOptions { + pub tab_size: u32, + pub insert_spaces: bool, +} +/// Parameter data. +#[derive(Clone, Debug, PartialEq)] +pub struct Parameter { + pub label: ParameterLabel, + pub documentation: Option<String>, +} +/// Signature data. +#[derive(Clone, Debug, PartialEq)] +pub struct Signature { + pub label: String, + pub documentation: Option<String>, + pub parameters: Vec<Parameter>, +} +/// SignatureHelp data. +#[derive(Clone, Debug, PartialEq)] +pub struct SignatureHelp { + pub signatures: Vec<Signature>, + pub active_signature: u32, + pub active_parameter: u32, +} +/// Highlight data. +#[derive(Clone, Debug, PartialEq)] +pub struct Highlight { + pub range: Range, + pub kind: String, +} +/// DocumentLink data. +#[derive(Clone, Debug, PartialEq)] +pub struct DocumentLink { + pub range: Range, + pub target: String, +} +/// CodeLens data. +#[derive(Clone, Debug, PartialEq)] +pub struct CodeLens { + pub range: Range, + pub title: String, + pub command: String, + pub argument: Option<String>, +} +/// FoldRange data. +#[derive(Clone, Debug, PartialEq)] +pub struct FoldRange { + pub start_line: u32, + pub end_line: u32, + pub kind: String, +} +/// ColorInfo data. +#[derive(Clone, Debug, PartialEq)] +pub struct ColorInfo { + pub range: Range, + pub red: f32, + pub green: f32, + pub blue: f32, + pub alpha: f32, +} +/// DocumentSymbol data. +#[derive(Clone, Debug, PartialEq)] +pub struct DocumentSymbol { + pub name: String, + pub detail: String, + pub kind: String, + pub range: Range, + pub selection_range: Range, + pub parent: Option<u32>, +} +/// GutterAction data. +#[derive(Clone, Debug, PartialEq)] +pub struct GutterAction { + pub line: u32, + pub icon: Option<String>, + pub tooltip: String, + pub command: String, + pub argument: Option<String>, +} +/// Injection data. +#[derive(Clone, Debug, PartialEq)] +pub struct Injection { + pub range: Range, + pub language: String, +} +/// Implement only the kinds declared under contributes.providers. +pub trait LanguageFeatures { + fn format( + &mut self, + _doc: &Document, + _range: Option<Range>, + _options: FormatOptions, + ) -> Result<Vec<TextEdit>, Error> { + Ok(Vec::new()) + } + fn signature_help(&mut self, _doc: &Document, _at: Position) -> Option<SignatureHelp> { + None + } + fn document_highlights(&mut self, _doc: &Document, _at: Position) -> Vec<Highlight> { + Vec::new() + } + fn document_links(&mut self, _doc: &Document) -> Vec<DocumentLink> { + Vec::new() + } + fn code_lenses(&mut self, _doc: &Document) -> Vec<CodeLens> { + Vec::new() + } + fn selection_ranges(&mut self, _doc: &Document, _at: Position) -> Vec<Range> { + Vec::new() + } + fn folding(&mut self, _doc: &Document) -> Vec<FoldRange> { + Vec::new() + } + fn colors(&mut self, _doc: &Document) -> Vec<ColorInfo> { + Vec::new() + } + fn color_presentations(&mut self, _doc: &Document, _color: ColorInfo) -> Vec<String> { + Vec::new() + } + fn document_symbols(&mut self, _doc: &Document) -> Vec<DocumentSymbol> { + Vec::new() + } + fn implementation(&mut self, _doc: &Document, _at: Position) -> Vec<Location> { + Vec::new() + } + fn type_definition(&mut self, _doc: &Document, _at: Position) -> Vec<Location> { + Vec::new() + } + fn gutter_actions(&mut self, _doc: &Document) -> Vec<GutterAction> { + Vec::new() + } +} +/// Identifies embedded ranges in a host document. +pub trait Injections { + fn injections(&mut self, doc: &Document) -> Vec<Injection>; +} + +#[cfg(target_arch = "wasm32")] +#[doc(hidden)] +pub mod wasm { + use super::*; + use crate::bindings::openidea::plugin::types as b; + use crate::injections_bindings::exports::openidea::plugin::injections as wi; + use crate::language_features_bindings::exports::openidea::plugin::language_features as w; + fn format_options_from(v: w::FormatOptions) -> FormatOptions { + FormatOptions { tab_size: v.tab_size, insert_spaces: v.insert_spaces } + } + fn parameter_to(v: Parameter) -> w::Parameter { + w::Parameter { label: parameter_label_to(v.label), documentation: v.documentation } + } + fn signature_to(v: Signature) -> w::Signature { + w::Signature { + label: v.label, + documentation: v.documentation, + parameters: v.parameters.into_iter().map(parameter_to).collect(), + } + } + fn signature_help_to(v: SignatureHelp) -> w::SignatureHelpResult { + w::SignatureHelpResult { + signatures: v.signatures.into_iter().map(signature_to).collect(), + active_signature: v.active_signature, + active_parameter: v.active_parameter, + } + } + fn highlight_to(v: Highlight) -> w::Highlight { + w::Highlight { range: crate::host::__range_to(v.range), kind: v.kind } + } + fn document_link_to(v: DocumentLink) -> w::DocumentLink { + w::DocumentLink { range: crate::host::__range_to(v.range), target: v.target } + } + fn code_lens_to(v: CodeLens) -> w::CodeLens { + w::CodeLens { + range: crate::host::__range_to(v.range), + title: v.title, + command: v.command, + argument: v.argument, + } + } + fn fold_range_to(v: FoldRange) -> w::FoldRange { + w::FoldRange { start_line: v.start_line, end_line: v.end_line, kind: v.kind } + } + fn color_info_to(v: ColorInfo) -> w::ColorInfo { + w::ColorInfo { + range: crate::host::__range_to(v.range), + red: v.red, + green: v.green, + blue: v.blue, + alpha: v.alpha, + } + } + fn color_info_from(v: w::ColorInfo) -> ColorInfo { + ColorInfo { + range: crate::host::__range_from(v.range), + red: v.red, + green: v.green, + blue: v.blue, + alpha: v.alpha, + } + } + fn document_symbol_to(v: DocumentSymbol) -> w::DocumentSymbol { + w::DocumentSymbol { + name: v.name, + detail: v.detail, + kind: v.kind, + range: crate::host::__range_to(v.range), + selection_range: crate::host::__range_to(v.selection_range), + parent: v.parent, + } + } + fn gutter_action_to(v: GutterAction) -> w::GutterAction { + w::GutterAction { line: v.line, icon: v.icon, tooltip: v.tooltip, command: v.command, argument: v.argument } + } + fn parameter_label_to(v: ParameterLabel) -> w::ParameterLabel { + match v { + ParameterLabel::Text(t) => w::ParameterLabel::Text(t), + ParameterLabel::Offsets(t) => w::ParameterLabel::Offsets(t), + } + } + pub fn format<T: crate::Plugin + LanguageFeatures + Default>( + d: w::Document, + range: Option<b::Range>, + options: w::FormatOptions, + ) -> Result<Vec<b::TextEdit>, b::Error> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| { + p.format(&doc, range.map(crate::host::__range_from), format_options_from(options)) + }); + result + .map(|result| { + result + .into_iter() + .map(|v| b::TextEdit { range: crate::host::__range_to(v.range), text: v.text }) + .collect() + }) + .map_err(|e| match e { + Error::Denied(s) => b::Error::Denied(s), + Error::Stale => b::Error::Stale, + Error::NotFound(s) => b::Error::NotFound(s), + Error::Invalid(s) => b::Error::Invalid(s), + Error::Cancelled => b::Error::Cancelled, + Error::Failed(s) => b::Error::Failed(s), + }) + } + pub fn signature_help<T: crate::Plugin + LanguageFeatures + Default>( + d: w::Document, + at: b::Position, + ) -> Option<w::SignatureHelpResult> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = + crate::__rt::with::<T, _>(|p| p.signature_help(&doc, Position { line: at.line, character: at.character })); + result.map(signature_help_to) + } + pub fn document_highlights<T: crate::Plugin + LanguageFeatures + Default>( + d: w::Document, + at: b::Position, + ) -> Vec<w::Highlight> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| { + p.document_highlights(&doc, Position { line: at.line, character: at.character }) + }); + result.into_iter().map(highlight_to).collect() + } + pub fn document_links<T: crate::Plugin + LanguageFeatures + Default>(d: w::Document) -> Vec<w::DocumentLink> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| p.document_links(&doc)); + result.into_iter().map(document_link_to).collect() + } + pub fn code_lenses<T: crate::Plugin + LanguageFeatures + Default>(d: w::Document) -> Vec<w::CodeLens> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| p.code_lenses(&doc)); + result.into_iter().map(code_lens_to).collect() + } + pub fn selection_ranges<T: crate::Plugin + LanguageFeatures + Default>( + d: w::Document, + at: b::Position, + ) -> Vec<b::Range> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| { + p.selection_ranges(&doc, Position { line: at.line, character: at.character }) + }); + result.into_iter().map(crate::host::__range_to).collect() + } + pub fn folding<T: crate::Plugin + LanguageFeatures + Default>(d: w::Document) -> Vec<w::FoldRange> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| p.folding(&doc)); + result.into_iter().map(fold_range_to).collect() + } + pub fn colors<T: crate::Plugin + LanguageFeatures + Default>(d: w::Document) -> Vec<w::ColorInfo> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| p.colors(&doc)); + result.into_iter().map(color_info_to).collect() + } + pub fn color_presentations<T: crate::Plugin + LanguageFeatures + Default>( + d: w::Document, + color: w::ColorInfo, + ) -> Vec<String> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| p.color_presentations(&doc, color_info_from(color))); + result.into_iter().collect() + } + pub fn document_symbols<T: crate::Plugin + LanguageFeatures + Default>(d: w::Document) -> Vec<w::DocumentSymbol> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| p.document_symbols(&doc)); + result.into_iter().map(document_symbol_to).collect() + } + pub fn implementation<T: crate::Plugin + LanguageFeatures + Default>( + d: w::Document, + at: b::Position, + ) -> Vec<b::Location> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = + crate::__rt::with::<T, _>(|p| p.implementation(&doc, Position { line: at.line, character: at.character })); + result.into_iter().map(|v| b::Location { path: v.path, range: crate::host::__range_to(v.range) }).collect() + } + pub fn type_definition<T: crate::Plugin + LanguageFeatures + Default>( + d: w::Document, + at: b::Position, + ) -> Vec<b::Location> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = + crate::__rt::with::<T, _>(|p| p.type_definition(&doc, Position { line: at.line, character: at.character })); + result.into_iter().map(|v| b::Location { path: v.path, range: crate::host::__range_to(v.range) }).collect() + } + pub fn gutter_actions<T: crate::Plugin + LanguageFeatures + Default>(d: w::Document) -> Vec<w::GutterAction> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + let result = crate::__rt::with::<T, _>(|p| p.gutter_actions(&doc)); + result.into_iter().map(gutter_action_to).collect() + } + pub fn injections<T: crate::Plugin + Injections + Default>(d: wi::Document) -> Vec<wi::Injection> { + let doc = Document { path: d.path, language: d.language, version: d.version, text: d.text }; + crate::__rt::with::<T, _>(|p| p.injections(&doc)) + .into_iter() + .map(|i| wi::Injection { range: crate::host::__range_to(i.range), language: i.language }) + .collect() + } +} diff --git a/sdk/rust/openidea-plugin/src/lib.rs b/sdk/rust/openidea-plugin/src/lib.rs index 8518a3a4c..faa56f39c 100644 --- a/sdk/rust/openidea-plugin/src/lib.rs +++ b/sdk/rust/openidea-plugin/src/lib.rs @@ -98,7 +98,7 @@ pub mod providers_bindings { export_macro_name: "__export_editor_providers_world", default_bindings_module: "openidea_plugin::providers_bindings", with: { - "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types, + "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types, }, }); } @@ -115,10 +115,10 @@ pub mod views_bindings { export_macro_name: "__export_views_world", default_bindings_module: "openidea_plugin::views_bindings", with: { - "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types, - "openidea:plugin/workspace@1.3.0": crate::bindings::openidea::plugin::workspace, - "openidea:plugin/editor@1.3.0": crate::bindings::openidea::plugin::editor, - "openidea:plugin/invocation@1.3.0": crate::bindings::openidea::plugin::invocation, + "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types, + "openidea:plugin/workspace@1.4.0": crate::bindings::openidea::plugin::workspace, + "openidea:plugin/editor@1.4.0": crate::bindings::openidea::plugin::editor, + "openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation, }, }); } @@ -130,7 +130,7 @@ pub mod indexing_bindings { world: "indexing", path: "wit", pub_export_macro: true, export_macro_name: "__export_indexing_world", default_bindings_module: "openidea_plugin::indexing_bindings", - with: { "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types }, + with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types }, }); } @@ -142,10 +142,10 @@ pub mod details_bindings { export_macro_name: "__export_details_world", default_bindings_module: "openidea_plugin::details_bindings", with: { - "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types, - "openidea:plugin/workspace@1.3.0": crate::bindings::openidea::plugin::workspace, - "openidea:plugin/editor@1.3.0": crate::bindings::openidea::plugin::editor, - "openidea:plugin/invocation@1.3.0": crate::bindings::openidea::plugin::invocation, + "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types, + "openidea:plugin/workspace@1.4.0": crate::bindings::openidea::plugin::workspace, + "openidea:plugin/editor@1.4.0": crate::bindings::openidea::plugin::editor, + "openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation, }, }); } @@ -157,7 +157,7 @@ pub mod semantic_bindings { world: "semantic-tokens", path: "wit", pub_export_macro: true, export_macro_name: "__export_semantic_tokens_world", default_bindings_module: "openidea_plugin::semantic_bindings", - with: { "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types }, + with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types }, }); } @@ -169,8 +169,8 @@ pub mod diagnostics_bindings { export_macro_name: "__export_diagnostics_world", default_bindings_module: "openidea_plugin::diagnostics_bindings", with: { - "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types, - "openidea:plugin/diagnostics@1.3.0": crate::bindings::openidea::plugin::diagnostics, + "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types, + "openidea:plugin/diagnostics@1.4.0": crate::bindings::openidea::plugin::diagnostics, }, }); } @@ -182,7 +182,7 @@ pub mod hover_bindings { world: "hover", path: "wit", pub_export_macro: true, export_macro_name: "__export_hover_world", default_bindings_module: "openidea_plugin::hover_bindings", - with: { "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types }, + with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types }, }); } @@ -194,11 +194,11 @@ pub mod commit_checks_bindings { export_macro_name: "__export_commit_checks_world", default_bindings_module: "openidea_plugin::commit_checks_bindings", with: { - "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types, - "openidea:plugin/workspace@1.3.0": crate::bindings::openidea::plugin::workspace, - "openidea:plugin/editor@1.3.0": crate::bindings::openidea::plugin::editor, - "openidea:plugin/invocation@1.3.0": crate::bindings::openidea::plugin::invocation, - "openidea:plugin/commit@1.3.0": crate::bindings::openidea::plugin::commit, + "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types, + "openidea:plugin/workspace@1.4.0": crate::bindings::openidea::plugin::workspace, + "openidea:plugin/editor@1.4.0": crate::bindings::openidea::plugin::editor, + "openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation, + "openidea:plugin/commit@1.4.0": crate::bindings::openidea::plugin::commit, }, }); } @@ -210,7 +210,7 @@ pub mod inlay_bindings { world: "inlay-hints", path: "wit", pub_export_macro: true, export_macro_name: "__export_inlay_hints_world", default_bindings_module: "openidea_plugin::inlay_bindings", - with: { "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types }, + with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types }, }); } @@ -221,7 +221,7 @@ pub mod file_actions_bindings { world: "file-actions-provider", path: "wit", pub_export_macro: true, export_macro_name: "__export_file_actions_world", default_bindings_module: "openidea_plugin::file_actions_bindings", - with: { "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types }, + with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types }, }); } @@ -233,10 +233,10 @@ pub mod endpoints_bindings { export_macro_name: "__export_endpoints_world", default_bindings_module: "openidea_plugin::endpoints_bindings", with: { - "openidea:plugin/types@1.3.0": crate::bindings::openidea::plugin::types, - "openidea:plugin/workspace@1.3.0": crate::bindings::openidea::plugin::workspace, - "openidea:plugin/editor@1.3.0": crate::bindings::openidea::plugin::editor, - "openidea:plugin/invocation@1.3.0": crate::bindings::openidea::plugin::invocation, + "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types, + "openidea:plugin/workspace@1.4.0": crate::bindings::openidea::plugin::workspace, + "openidea:plugin/editor@1.4.0": crate::bindings::openidea::plugin::editor, + "openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation, }, }); } @@ -286,6 +286,12 @@ macro_rules! register_plugin { #[doc(hidden)] #[macro_export] macro_rules! __optional_exports { + (injections, $plugin:ty, $component:ident) => { $crate::__injections_exports!($plugin, $component); }; + (language_features, $plugin:ty, $component:ident) => { $crate::__language_features_exports!($plugin, $component); }; + (mcp, $plugin:ty, $component:ident) => { $crate::__mcp_exports!($plugin, $component); }; + (run_configurations, $plugin:ty, $component:ident) => { $crate::__run_configurations_exports!($plugin, $component); }; + (test_results, $plugin:ty, $component:ident) => { $crate::__test_results_exports!($plugin, $component); }; + (providers, $plugin:ty, $component:ident) => { $crate::__providers_exports!($plugin, $component); }; @@ -653,7 +659,7 @@ pub mod __rt { static INSTANCE: RefCell<Option<Box<dyn Any>>> = const { RefCell::new(None) }; } - fn with<T: Plugin + Default, R>(f: impl FnOnce(&mut T) -> R) -> R { + pub(crate) fn with<T: Plugin + Default, R>(f: impl FnOnce(&mut T) -> R) -> R { INSTANCE.with(|cell| { let mut slot = cell.borrow_mut(); let plugin = slot.get_or_insert_with(|| Box::new(T::default())); @@ -1069,6 +1075,72 @@ pub mod __rt { with::<T, _>(|p| p.render(&view, viewport, &ctx)).map(view_out).map_err(error_out) } + pub fn mcp_call<T: Plugin + crate::McpProvider + Default>( + tool: &str, + arguments: &str, + ctx: &b::invocation::Invocation, + ) -> Result<String, b::types::Error> { + let ctx = Invocation::__new(ctx); + with::<T, _>(|p| p.call(tool, arguments, &ctx)).map_err(error_out) + } + + pub fn configurations<T: Plugin + crate::RunConfigurations + Default>( + root: &str, + ) -> Vec<crate::run_configurations_bindings::exports::openidea::plugin::run_configurations::RunConfiguration> { + use crate::run_configurations_bindings::exports::openidea::plugin::run_configurations::RunConfiguration as C; + with::<T, _>(|p| p.configurations(root)) + .into_iter() + .map(|c| C { + id: c.id, + name: c.name, + program: c.program, + args: c.args, + cwd: c.cwd, + test: c.test, + framework: c.framework, + }) + .collect() + } + pub fn test_results<T: Plugin + crate::TestResults + Default>( + config: &str, + chunk: &str, + ) -> Vec<crate::test_results_bindings::exports::openidea::plugin::test_results::TestEvent> { + use crate::PluginTestEvent as E; + use crate::test_results_bindings::exports::openidea::plugin::test_results as w; + let location = |l: crate::Location| b::types::Location { + path: l.path, + range: b::types::Range { + start: b::types::Position { line: l.range.start.line, character: l.range.start.character }, + end: b::types::Position { line: l.range.end.line, character: l.range.end.character }, + }, + }; + let start = |v: crate::TestStart| w::TestStart { + id: v.id, + parent: v.parent, + name: v.name, + location: v.location.map(location), + }; + with::<T, _>(|p| p.parse(config, chunk)) + .into_iter() + .map(|event| match event { + E::SuiteStarted(v) => w::TestEvent::SuiteStarted(start(v)), + E::TestStarted(v) => w::TestEvent::TestStarted(start(v)), + E::Passed(v) => w::TestEvent::Passed(w::TestPassed { id: v.id, duration_ms: v.duration_ms }), + E::Failed(v) => w::TestEvent::Failed(w::TestFailed { + id: v.id, + duration_ms: v.duration_ms, + message: v.message, + expected: v.expected, + actual: v.actual, + location: v.location.map(location), + }), + E::Skipped(v) => w::TestEvent::Skipped(w::TestSkipped { id: v.id, reason: v.reason }), + E::Output(v) => w::TestEvent::Output(w::TestOutput { id: v.id, text: v.text }), + E::Other { kind, data } => w::TestEvent::Other(b::types::Extension { kind, data }), + }) + .collect() + } + pub fn endpoints<T: Plugin + crate::endpoints::EndpointsProvider + Default>( ctx: &b::invocation::Invocation, ) -> Result<Vec<crate::endpoints_bindings::exports::openidea::plugin::endpoints_provider::Endpoint>, b::types::Error> @@ -1211,3 +1283,132 @@ pub mod __rt { .collect() } } + +pub mod execution; +pub use execution::*; + +#[cfg(target_arch = "wasm32")] +#[doc(hidden)] +pub mod run_configurations_bindings { + wit_bindgen::generate!({ world: "configurations-provider", path: "wit", pub_export_macro: true, + export_macro_name: "__export_configurations_world", default_bindings_module: "openidea_plugin::run_configurations_bindings" }); +} +#[cfg(target_arch = "wasm32")] +#[doc(hidden)] +pub mod test_results_bindings { + wit_bindgen::generate!({ world: "test-results-provider", path: "wit", pub_export_macro: true, + export_macro_name: "__export_tests_world", default_bindings_module: "openidea_plugin::test_results_bindings", + with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types }, + }); +} +#[doc(hidden)] +#[macro_export] +macro_rules! __run_configurations_exports { + ($plugin:ty, $component:ident) => { + impl $crate::run_configurations_bindings::exports::openidea::plugin::run_configurations::Guest for $component { + fn configurations(root: String) -> Vec<$crate::run_configurations_bindings::exports::openidea::plugin::run_configurations::RunConfiguration> { + $crate::__rt::configurations::<$plugin>(&root) + } + } + $crate::run_configurations_bindings::__export_configurations_world!($component with_types_in $crate::run_configurations_bindings); + }; +} +#[doc(hidden)] +#[macro_export] +macro_rules! __test_results_exports { + ($plugin:ty, $component:ident) => { + impl $crate::test_results_bindings::exports::openidea::plugin::test_results::Guest for $component { + fn parse(config: String, chunk: String) -> Vec<$crate::test_results_bindings::exports::openidea::plugin::test_results::TestEvent> { + $crate::__rt::test_results::<$plugin>(&config, &chunk) + } + } + $crate::test_results_bindings::__export_tests_world!($component with_types_in $crate::test_results_bindings); + }; +} + +pub mod language_features; + +#[cfg(target_arch = "wasm32")] +#[doc(hidden)] +pub mod language_features_bindings { + wit_bindgen::generate!({world: "editor-language-features", path: "wit", pub_export_macro: true, + export_macro_name: "__export_language_features_world", default_bindings_module: "openidea_plugin::language_features_bindings", + with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types }, + }); +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __language_features_exports { + ($plugin:ty, $component:ident) => { + impl $crate::language_features_bindings::exports::openidea::plugin::language_features::Guest for $component { +fn format(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document, range: Option<$crate::bindings::openidea::plugin::types::Range>, options: $crate::language_features_bindings::exports::openidea::plugin::language_features::FormatOptions) -> Result<Vec<$crate::bindings::openidea::plugin::types::TextEdit>, $crate::bindings::openidea::plugin::types::Error> { $crate::language_features::wasm::format::<$plugin>(doc, range, options) } +fn signature_help(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document, at: $crate::bindings::openidea::plugin::types::Position) -> Option<$crate::language_features_bindings::exports::openidea::plugin::language_features::SignatureHelpResult> { $crate::language_features::wasm::signature_help::<$plugin>(doc, at) } +fn document_highlights(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document, at: $crate::bindings::openidea::plugin::types::Position) -> Vec<$crate::language_features_bindings::exports::openidea::plugin::language_features::Highlight> { $crate::language_features::wasm::document_highlights::<$plugin>(doc, at) } +fn document_links(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document) -> Vec<$crate::language_features_bindings::exports::openidea::plugin::language_features::DocumentLink> { $crate::language_features::wasm::document_links::<$plugin>(doc) } +fn code_lenses(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document) -> Vec<$crate::language_features_bindings::exports::openidea::plugin::language_features::CodeLens> { $crate::language_features::wasm::code_lenses::<$plugin>(doc) } +fn selection_ranges(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document, at: $crate::bindings::openidea::plugin::types::Position) -> Vec<$crate::bindings::openidea::plugin::types::Range> { $crate::language_features::wasm::selection_ranges::<$plugin>(doc, at) } +fn folding(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document) -> Vec<$crate::language_features_bindings::exports::openidea::plugin::language_features::FoldRange> { $crate::language_features::wasm::folding::<$plugin>(doc) } +fn colors(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document) -> Vec<$crate::language_features_bindings::exports::openidea::plugin::language_features::ColorInfo> { $crate::language_features::wasm::colors::<$plugin>(doc) } +fn color_presentations(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document, color: $crate::language_features_bindings::exports::openidea::plugin::language_features::ColorInfo) -> Vec<String> { $crate::language_features::wasm::color_presentations::<$plugin>(doc, color) } +fn document_symbols(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document) -> Vec<$crate::language_features_bindings::exports::openidea::plugin::language_features::DocumentSymbol> { $crate::language_features::wasm::document_symbols::<$plugin>(doc) } +fn implementation(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document, at: $crate::bindings::openidea::plugin::types::Position) -> Vec<$crate::bindings::openidea::plugin::types::Location> { $crate::language_features::wasm::implementation::<$plugin>(doc, at) } +fn type_definition(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document, at: $crate::bindings::openidea::plugin::types::Position) -> Vec<$crate::bindings::openidea::plugin::types::Location> { $crate::language_features::wasm::type_definition::<$plugin>(doc, at) } +fn gutter_actions(doc: $crate::language_features_bindings::exports::openidea::plugin::language_features::Document) -> Vec<$crate::language_features_bindings::exports::openidea::plugin::language_features::GutterAction> { $crate::language_features::wasm::gutter_actions::<$plugin>(doc) } + } + $crate::language_features_bindings::__export_language_features_world!($component with_types_in $crate::language_features_bindings); + }; +} + +#[cfg(target_arch = "wasm32")] +#[doc(hidden)] +pub mod injections_bindings { + wit_bindgen::generate!({world: "language-injections", path: "wit", pub_export_macro: true, + export_macro_name: "__export_injections_world", default_bindings_module: "openidea_plugin::injections_bindings", + with: { "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types }, + }); +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __injections_exports { + ($plugin:ty, $component:ident) => { + impl $crate::injections_bindings::exports::openidea::plugin::injections::Guest for $component { +fn injections(doc: $crate::injections_bindings::exports::openidea::plugin::injections::Document) -> Vec<$crate::injections_bindings::exports::openidea::plugin::injections::Injection> { $crate::language_features::wasm::injections::<$plugin>(doc) } + } + $crate::injections_bindings::__export_injections_world!($component with_types_in $crate::injections_bindings); + }; +} + +/// Embedded-language range providers (API 1.4). +pub mod injections { + pub use crate::language_features::{Document, Injection, Injections}; +} + +/// Implements tools declared in contributes.mcpTools. Arguments/results are JSON/text. +pub trait McpProvider { + fn call(&mut self, tool: &str, arguments: &str, ctx: &Invocation) -> Result<String, Error>; +} +#[cfg(target_arch = "wasm32")] +#[doc(hidden)] +pub mod mcp_bindings { + wit_bindgen::generate!({ world: "mcp-tools", path: "wit", pub_export_macro: true, + export_macro_name: "__export_mcp_world", default_bindings_module: "openidea_plugin::mcp_bindings", + with: { + "openidea:plugin/types@1.4.0": crate::bindings::openidea::plugin::types, + "openidea:plugin/invocation@1.4.0": crate::bindings::openidea::plugin::invocation, + }, + }); +} +#[doc(hidden)] +#[macro_export] +macro_rules! __mcp_exports { + ($plugin:ty, $component:ident) => { + impl $crate::mcp_bindings::exports::openidea::plugin::mcp_provider::Guest for $component { + fn call(tool: String, arguments: String, ctx: &$crate::bindings::openidea::plugin::invocation::Invocation) -> Result<String, $crate::bindings::openidea::plugin::types::Error> { + $crate::__rt::mcp_call::<$plugin>(&tool, &arguments, ctx) + } + } + $crate::mcp_bindings::__export_mcp_world!($component with_types_in $crate::mcp_bindings); + }; +} diff --git a/sdk/rust/openidea-plugin/src/mock.rs b/sdk/rust/openidea-plugin/src/mock.rs index 46edd3292..bb02de632 100644 --- a/sdk/rust/openidea-plugin/src/mock.rs +++ b/sdk/rust/openidea-plugin/src/mock.rs @@ -36,6 +36,10 @@ pub struct MockTask { pub ended: bool, } +type CommandHandler = dyn Fn(Option<&str>) -> Result<Option<String>, Error>; + +type SearchHandler = Box<dyn Fn(&TextQuery, u32) -> Result<TextSearch, Error>>; + type NetHandler = Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Error>>; type ProcessHandler = Box<dyn Fn(&[String], Option<&str>) -> Result<ProcessOutput, Error>>; @@ -94,6 +98,13 @@ struct State { action_notifications: Vec<ActionNotification>, logs: Vec<(LogLevel, String)>, status: BTreeMap<String, StatusItem>, + window_status: BTreeMap<String, StatusItem>, + ipc: BTreeMap<u64, VecDeque<u8>>, + next_ipc: u64, + loopbacks: BTreeSet<u64>, + next_loopback: u64, + output: BTreeMap<String, String>, + revealed_output: Vec<String>, context: BTreeMap<String, ContextValue>, storage: BTreeMap<String, String>, subscriptions: BTreeSet<EventKind>, @@ -112,6 +123,19 @@ struct State { diagnostics: BTreeMap<String, Vec<Diagnostic>>, provider_diagnostics: BTreeMap<String, Vec<Diagnostic>>, secrets: BTreeMap<String, String>, + declared_secrets: BTreeSet<String>, + commands: BTreeMap<String, Rc<CommandHandler>>, + command_depth: usize, + owned_commands: BTreeSet<String>, + search: Option<SearchHandler>, + commits: Vec<CommitInfo>, + blame: BTreeMap<String, Vec<BlameLine>>, + patch: Option<String>, + restricted: bool, + decorations: BTreeMap<(String, String), Vec<Decoration>>, + file_decorations: BTreeMap<(String, String), FileDecoration>, + revealed_ranges: Vec<Range>, + loopback_queries: VecDeque<Option<String>>, net: Option<NetHandler>, requests: Vec<HttpRequest>, processes: BTreeMap<String, ProcessHandler>, @@ -122,6 +146,17 @@ struct State { refreshed: Vec<String>, view_messages: Vec<(String, String)>, terminals: Vec<MockTerminal>, + terminal_sessions: BTreeMap<String, (String, crate::host::Command, String)>, + terminal_writes: Vec<(String, String)>, + content_tabs: BTreeMap<String, crate::ContentTab>, + content_replies: Vec<(String, crate::ContentTabReply)>, + database_sources: BTreeMap<(String, String), PluginDataSource>, + database_credentials: BTreeSet<(String, String)>, + database_connections: Vec<(String, String)>, + mcp_stdio: Option<Vec<String>>, + run_targets: BTreeMap<(String, String), crate::PluginRunTarget>, + run_configurations: BTreeMap<String, crate::PluginRunConfiguration>, + configuration_runs: Vec<(String, String, Vec<String>, bool)>, log_tabs: Vec<MockLogTab>, locations: Vec<(String, u32, u32)>, settings: BTreeMap<String, String>, @@ -158,6 +193,8 @@ pub struct MockHost { /// Every capability a plugin can hold, except `process:`/`net:`/`fs.read:`. const ALL: &[&str] = &[ + "ide.commands", + "database.sources", "project.read", "editor.read", "editor.write", @@ -170,6 +207,41 @@ const ALL: &[&str] = &[ ]; impl MockHost { + /// Registers one of this plugin's declared commands, callable without ide.commands. + pub fn on_own_command(&self, id: &str, handler: impl Fn(Option<&str>) -> Result<Option<String>, Error> + 'static) { + let stable = format!("plugin.mock.{id}"); + self.on_command(&stable, handler); + self.state.borrow_mut().owned_commands.insert(stable); + } + + /// Supplies search responses without duplicating the IDE search engine. + pub fn on_search(&self, handler: impl Fn(&TextQuery, u32) -> Result<TextSearch, Error> + 'static) { + self.state.borrow_mut().search = Some(Box::new(handler)); + } + /// Supplies read-only repository data. + pub fn set_vcs_reads(&self, commits: Vec<CommitInfo>, path: &str, blame: Vec<BlameLine>, patch: Option<String>) { + let mut s = self.state.borrow_mut(); + s.commits = commits; + s.blame.insert(path.into(), blame); + s.patch = patch; + } + /// Simulates Restricted Mode for new VCS read APIs. + pub fn set_restricted(&self, restricted: bool) { + self.state.borrow_mut().restricted = restricted; + } + /// Editor decoration snapshots keyed by (set id, absolute path). + pub fn decorations(&self) -> BTreeMap<(String, String), Vec<Decoration>> { + self.state.borrow().decorations.clone() + } + /// Tree decoration snapshots keyed by (set id, absolute path). + pub fn file_decorations(&self) -> BTreeMap<(String, String), FileDecoration> { + self.state.borrow().file_decorations.clone() + } + /// Ranges requested to scroll into view. + pub fn revealed_ranges(&self) -> Vec<Range> { + self.state.borrow().revealed_ranges.clone() + } + /// Installs a mock granting the usual capabilities, with project `/project`. pub fn install() -> Self { let state = Rc::new(RefCell::new(State { @@ -274,7 +346,8 @@ impl MockHost { } pub fn status_items(&self) -> BTreeMap<String, StatusItem> { - self.state.borrow().status.clone() + let s = self.state.borrow(); + s.status.iter().chain(&s.window_status).map(|(k, v)| (k.clone(), v.clone())).collect() } pub fn context_keys(&self) -> BTreeMap<String, ContextValue> { @@ -372,6 +445,16 @@ impl MockHost { self.state.borrow().diagnostics.clone() } + /// Snapshot supplied to optional language-feature and injection traits in native tests. + pub fn provider_document(&self) -> Option<crate::providers::Document> { + self.state.borrow().editor.as_ref().map(|e| crate::providers::Document { + path: e.path.clone(), + language: e.language.clone(), + version: e.version, + text: e.text.clone(), + }) + } + /// Runs a diagnostics provider against the current mock editor snapshot. /// This explicit call replaces the real host's debounce; it needs no capability. pub fn refresh_diagnostics(&self, provider: &mut impl crate::diagnostics::DiagnosticsProvider) { @@ -411,6 +494,35 @@ impl MockHost { self.state.borrow().locations.clone() } + /// Registered targets of this mock plugin, scoped by project root. + pub fn run_targets(&self) -> BTreeMap<(String, String), crate::PluginRunTarget> { + self.state.borrow().run_targets.clone() + } + /// Configurations available to start_run in native tests. + /// Run launches recorded as (operation id, configuration id, appended args, debug). + pub fn configuration_runs(&self) -> Vec<(String, String, Vec<String>, bool)> { + self.state.borrow().configuration_runs.clone() + } + /// Configures only argv discovery; the mock has no token or URL. + pub fn set_mcp_stdio_command(&self, command: Option<Vec<String>>) { + self.state.borrow_mut().mcp_stdio = command; + } + pub fn set_run_configurations(&self, configs: Vec<crate::PluginRunConfiguration>) { + self.state.borrow_mut().run_configurations = configs.into_iter().map(|c| (c.id.clone(), c)).collect(); + } + /// Text written through write_terminal. + pub fn terminal_writes(&self) -> Vec<(String, String)> { + self.state.borrow().terminal_writes.clone() + } + /// Simulates exit, releasing the key for another operation. + pub fn finish_terminal(&self, key: &str) -> Option<crate::Event> { + let (id, _, _) = self.state.borrow_mut().terminal_sessions.remove(key)?; + Some(crate::Event::Other { + kind: "terminal-exited".into(), + data: serde_json::to_string(&crate::TerminalExited { id, key: key.into(), status: Some(0), closed: false }) + .unwrap(), + }) + } /// Terminals the plugin opened. pub fn terminals(&self) -> Vec<MockTerminal> { self.state.borrow().terminals.clone() @@ -431,8 +543,29 @@ impl MockHost { self.state.borrow_mut().locale = Some(tag.into()); } + /// Registers the result of a stable action or plugin command. + pub fn on_command(&self, id: &str, handler: impl Fn(Option<&str>) -> Result<Option<String>, Error> + 'static) { + let mut state = self.state.borrow_mut(); + state.owned_commands.remove(id); + state.commands.insert(id.into(), Rc::new(handler)); + } + /// Queues the raw browser query returned by the next loopback wait. + pub fn queue_loopback_query(&self, query: Option<&str>) { + self.state.borrow_mut().loopback_queries.push_back(query.map(str::to_owned)); + } + + /// Declares a secret the plugin is allowed to store (the mock has no manifest). + pub fn declare_secret(&self, name: &str) { + self.state.borrow_mut().declared_secrets.insert(name.into()); + } + /// The plugin's read-only output buffers. + pub fn output(&self) -> BTreeMap<String, String> { + self.state.borrow().output.clone() + } + /// A secret the user stored. pub fn set_secret(&self, name: &str, value: &str) { + self.state.borrow_mut().declared_secrets.insert(name.into()); self.state.borrow_mut().secrets.insert(name.into(), value.into()); } @@ -663,6 +796,85 @@ pub mod current { with(|s| s.status.insert(id.to_owned(), item.clone())); } + pub fn set_window_status_item(id: &str, item: &StatusItem) -> Result<(), Error> { + with(|s| { + s.window_status.insert(id.into(), item.clone()); + }); + Ok(()) + } + pub fn remove_window_status_item(id: &str) -> Result<(), Error> { + with(|s| { + s.window_status.remove(id); + }); + Ok(()) + } + pub fn execute_command(id: &str, arg: Option<&str>) -> Result<Option<String>, Error> { + let handler = with(|s| { + if !s.owned_commands.contains(id) { + require(s, "ide.commands")?; + } + if s.command_depth >= 8 { + return Err(Error::Invalid("command nesting limit (8)".into())); + } + let handler = s.commands.get(id).cloned().ok_or_else(|| Error::NotFound(id.into()))?; + s.command_depth += 1; + Ok(handler) + })?; + let result = handler(arg); + with(|s| s.command_depth -= 1); + result + } + pub struct ConnectionRes { + id: u64, + owner: Rc<RefCell<State>>, + } + impl Drop for ConnectionRes { + fn drop(&mut self) { + self.owner.borrow_mut().ipc.remove(&self.id); + } + } + pub fn ipc_connect(name: &str) -> Result<ConnectionRes, Error> { + with(|s| { + let cap = format!("ipc:{name}"); + let granted = s.capabilities.iter().any(|g| { + g == &cap + || g.strip_prefix("ipc:").is_some_and(|pattern| { + let Some((base, tail)) = pattern.rsplit_once('/') else { return false }; + let Some((parent, file)) = name.rsplit_once('/') else { return false }; + base == parent && tail.strip_suffix('*').is_some_and(|prefix| file.starts_with(prefix)) + }) + }); + if !granted { + return Err(Error::Denied(cap)); + } + if s.ipc.len() >= 8 { + return Err(Error::Invalid("at most eight IPC connections".into())); + } + s.next_ipc += 1; + s.ipc.insert(s.next_ipc, VecDeque::new()); + let owner = CURRENT.with(|c| c.borrow().as_ref().unwrap().clone()); + Ok(ConnectionRes { id: s.next_ipc, owner }) + }) + } + pub fn ipc_write(c: &ConnectionRes, bytes: &[u8]) -> Result<(), Error> { + if bytes.len() > 1 << 20 { + return Err(Error::Invalid("at most 1 MiB".into())); + } + let mut s = c.owner.borrow_mut(); + let data = s.ipc.get_mut(&c.id).ok_or_else(|| Error::NotFound("connection".into()))?; + data.extend(bytes); + Ok(()) + } + pub fn ipc_read(c: &ConnectionRes, max: u32, _timeout: u32) -> Result<Vec<u8>, Error> { + let mut s = c.owner.borrow_mut(); + let data = s.ipc.get_mut(&c.id).ok_or_else(|| Error::NotFound("connection".into()))?; + let count = data.len().min(max as usize).min(1 << 20); + Ok(data.drain(..count).collect()) + } + pub fn ipc_close(c: &ConnectionRes) -> Result<(), Error> { + c.owner.borrow_mut().ipc.remove(&c.id); + Ok(()) + } pub fn remove_status_item(id: &str) { with(|s| s.status.remove(id)); } @@ -1098,6 +1310,204 @@ pub mod current { }) } + pub fn open_editors() -> Result<Vec<OpenEditor>, Error> { + with(|s| { + require(s, "editor.read")?; + Ok(s.editor + .iter() + .map(|e| OpenEditor { + path: e.path.clone(), + language: e.language.clone(), + dirty: e.version > 1, + active: true, + }) + .collect()) + }) + } + pub fn editor_open(path: &str, selection: Option<Range>) -> Result<(), Error> { + let text = with(|s| { + if is_private_path(path) { + return s.external.get(path).cloned().ok_or_else(|| Error::NotFound(path.into())); + } + require(s, "project.read")?; + let p = MockProject { root: s.root.clone().ok_or_else(|| Error::Invalid("no project".into()))? }; + let rel = rel_in_root(&p, path)?; + s.files.get(rel).or_else(|| s.files.get(path)).cloned().ok_or_else(|| Error::NotFound(path.into())) + })?; + with(|s| { + let path = + if path.starts_with('/') { path.into() } else { format!("{}/{path}", s.root.as_deref().unwrap()) }; + s.editor = Some(Editor { + path, + language: "text".into(), + text, + version: 1, + selections: selection.into_iter().collect(), + }); + }); + Ok(()) + } + fn valid_ranges(text: &str, ranges: &[Range]) -> bool { + ranges.iter().all(|r| matches!((crate::text::offset(text, r.start), crate::text::offset(text, r.end)), (Some(a), Some(b)) if a <= b)) + } + pub fn editor_set_selections(_: &EditorRes, ranges: &[Range]) -> Result<(), Error> { + editor(|_, e| { + if ranges.is_empty() || ranges.len() > 1000 || !valid_ranges(&e.text, ranges) { + return Err(Error::Invalid("invalid selections".into())); + } + e.selections = ranges.to_vec(); + Ok(()) + }) + } + pub fn editor_reveal(_: &EditorRes, range: Range) -> Result<(), Error> { + editor(|s, e| { + if !valid_ranges(&e.text, &[range]) { + return Err(Error::Invalid("invalid range".into())); + } + s.revealed_ranges.push(range); + Ok(()) + }) + } + pub fn project_search_text(_: &ProjectRes, query: &TextQuery, limit: u32) -> Result<TextSearch, Error> { + with(|s| { + require(s, "project.read")?; + if limit == 0 { + return Ok(TextSearch::default()); + } + let mut result = match &s.search { + Some(f) => f(query, limit.min(10_000))?, + None => TextSearch::default(), + }; + result.limit_hit |= result.matches.len() > limit.min(10_000) as usize; + result.matches.truncate(limit.min(10_000) as usize); + Ok(result) + }) + } + pub fn problems(path: Option<&str>) -> Result<Vec<FileProblems>, Error> { + with(|s| { + require(s, "project.read")?; + let p = MockProject { root: s.root.clone().ok_or_else(|| Error::Invalid("no project".into()))? }; + let wanted = path.map(|path| rel_in_root(&p, path).map(|rel| format!("{}/{rel}", p.root))).transpose()?; + Ok(s.diagnostics + .iter() + .chain(s.provider_diagnostics.iter()) + .filter(|(path, _)| wanted.as_ref().is_none_or(|p| p == *path)) + .map(|(path, diagnostics)| FileProblems { + path: path.clone(), + source: "mock".into(), + diagnostics: diagnostics.clone(), + }) + .collect()) + }) + } + fn decoration_path(s: &State, path: &str) -> Result<String, Error> { + let p = MockProject { root: s.root.clone().ok_or_else(|| Error::Invalid("no project".into()))? }; + Ok(format!("{}/{}", p.root, rel_in_root(&p, path)?)) + } + fn style(s: &str) -> bool { + matches!( + s, + "highlight" + | "search" + | "added" + | "modified" + | "removed" + | "warning" + | "error" + | "info" + | "underline" + | "strikethrough" + | "muted" + ) + } + pub fn set_decorations(path: &str, set: &str, items: &[Decoration]) -> Result<(), Error> { + with(|s| { + require(s, "project.read")?; + let path = decoration_path(s, path)?; + let mut next = s.decorations.clone(); + next.insert((set.into(), path.clone()), items.iter().filter(|d| style(&d.style)).cloned().collect()); + let names: BTreeSet<_> = next.keys().map(|(set, _)| set).collect(); + let count: usize = next.iter().filter(|((_, p), _)| *p == path).map(|(_, i)| i.len()).sum(); + if set.is_empty() || set.len() > 128 || names.len() > 50 || count > 10_000 { + return Err(Error::Invalid("decoration limit".into())); + } + s.decorations = next; + Ok(()) + }) + } + pub fn clear_decorations(set: &str) -> Result<(), Error> { + with(|s| { + require(s, "project.read")?; + s.decorations.retain(|(id, _), _| id != set); + Ok(()) + }) + } + pub fn set_file_decoration(path: &str, set: &str, item: &FileDecoration) -> Result<(), Error> { + with(|s| { + require(s, "project.read")?; + let path = decoration_path(s, path)?; + let mut next = s.file_decorations.clone(); + next.remove(&(set.into(), path.clone())); + if style(&item.style) { + next.insert((set.into(), path), item.clone()); + } + let names: BTreeSet<_> = next.keys().map(|(set, _)| set).collect(); + if set.is_empty() + || set.len() > 128 + || names.len() > 50 + || item.badge.as_ref().is_some_and(|b| b.chars().count() > 2) + { + return Err(Error::Invalid("file decoration limit".into())); + } + s.file_decorations = next; + Ok(()) + }) + } + pub fn clear_file_decorations(set: &str) -> Result<(), Error> { + with(|s| { + require(s, "project.read")?; + s.file_decorations.retain(|(id, _), _| id != set); + Ok(()) + }) + } + fn vcs_read(s: &State) -> Result<(), Error> { + require(s, "vcs.read")?; + if s.restricted { + return Err(Error::Failed("project is in Restricted Mode".into())); + } + Ok(()) + } + pub fn vcs_log(p: &ProjectRes, path: Option<&str>, limit: u32) -> Result<Vec<CommitInfo>, Error> { + with(|s| { + vcs_read(s)?; + if let Some(path) = path { + rel_in_root(p, path)?; + } + Ok(s.commits.iter().take(limit.min(1000) as usize).cloned().collect()) + }) + } + pub fn vcs_blame(p: &ProjectRes, path: &str) -> Result<Vec<BlameLine>, Error> { + with(|s| { + vcs_read(s)?; + rel_in_root(p, path)?; + Ok(s.blame.get(path).cloned().unwrap_or_default()) + }) + } + pub fn vcs_diff(p: &ProjectRes, path: Option<&str>, against: Option<&str>) -> Result<Option<String>, Error> { + with(|s| { + vcs_read(s)?; + if let Some(path) = path { + rel_in_root(p, path)?; + } + if against.is_some_and(|r| r.is_empty() || r.starts_with('-') || r.contains('\0')) { + return Err(Error::Invalid("invalid revision".into())); + } + if s.patch.as_ref().is_some_and(|p| p.len() > 4 << 20) { + return Err(Error::Failed("diff exceeds 4 MiB".into())); + } + Ok(s.patch.clone()) + }) + } pub fn vcs_status(_: &ProjectRes) -> Result<Option<RepositoryStatus>, Error> { with(|s| { require(s, "vcs.read")?; @@ -1181,8 +1591,11 @@ pub mod current { pub fn subscribe(kind: EventKind) -> Result<(), Error> { with(|s| { let cap = match kind { - EventKind::ActionPerformed => Some("events.input"), - EventKind::Scheduled => None, + EventKind::ActionPerformed | EventKind::InputModeChanged => Some("events.input"), + EventKind::Scheduled + | EventKind::RunExited + | EventKind::TerminalExited + | EventKind::ContentTabAction => None, _ => Some("events"), }; if let Some(cap) = cap { @@ -1242,8 +1655,18 @@ pub mod current { pub fn fetch(req: &HttpRequest) -> Result<HttpResponse, Error> { with(|s| { let host = net_host(&req.url).ok_or_else(|| Error::Invalid(format!("{:?} is not a URL", req.url)))?; - let cap = format!("net:{host}"); - if !s.capabilities.iter().any(|c| *c == cap || c == "net:*") { + let kind = if req.url.starts_with("http://") && !matches!(host.as_str(), "localhost" | "127.0.0.1" | "::1") + { + "net.http" + } else { + "net" + }; + let cap = format!("{kind}:{host}"); + if !s.capabilities.iter().any(|c| { + *c == cap + || c == &format!("{kind}:*") + || c.strip_prefix(&format!("{kind}:*.")).is_some_and(|suffix| host.ends_with(&format!(".{suffix}"))) + }) { return Err(Error::Denied(cap)); } let mut sent = req.clone(); @@ -1265,6 +1688,87 @@ pub mod current { }) } + pub fn secret_set(name: &str, value: &str) -> Result<(), Error> { + with(|s| { + require(s, "secrets")?; + if !s.declared_secrets.contains(name) { + return Err(Error::NotFound(name.into())); + } + s.secrets.insert(name.into(), value.into()); + Ok(()) + }) + } + pub fn secret_delete(name: &str) -> Result<(), Error> { + with(|s| { + require(s, "secrets")?; + if !s.declared_secrets.contains(name) { + return Err(Error::NotFound(name.into())); + } + s.secrets.remove(name); + Ok(()) + }) + } + pub struct LoopbackRes { + id: u64, + owner: Rc<RefCell<State>>, + uri: String, + } + impl Drop for LoopbackRes { + fn drop(&mut self) { + self.owner.borrow_mut().loopbacks.remove(&self.id); + } + } + pub fn begin_loopback(path: &str) -> Result<LoopbackRes, Error> { + with(|s| { + if s.loopbacks.len() >= 2 { + return Err(Error::Invalid("at most two loopbacks".into())); + } + s.next_loopback += 1; + s.loopbacks.insert(s.next_loopback); + Ok(LoopbackRes { + id: s.next_loopback, + owner: CURRENT.with(|c| c.borrow().as_ref().unwrap().clone()), + uri: format!("http://127.0.0.1:{}/{}", 49152 + s.next_loopback, path.trim_start_matches('/')), + }) + }) + } + pub fn loopback_uri(l: &LoopbackRes) -> String { + l.uri.clone() + } + pub fn loopback_wait(l: &LoopbackRes, _timeout_ms: u32) -> Result<Option<String>, Error> { + if !l.owner.borrow_mut().loopbacks.remove(&l.id) { + return Err(Error::Invalid("loopback already awaited".into())); + } + Ok(l.owner.borrow_mut().loopback_queries.pop_front().flatten()) + } + pub fn output_append(channel: &str, text: &str) -> Result<(), Error> { + with(|s| { + if channel.is_empty() || channel.len() > 128 || channel.chars().any(char::is_control) { + return Err(Error::Invalid("invalid channel".into())); + } + if !s.output.contains_key(channel) && s.output.len() >= 32 { + return Err(Error::Invalid("at most 32 channels".into())); + } + let buffer = s.output.entry(channel.into()).or_default(); + buffer.push_str(text); + let mut remove = buffer.len().saturating_sub(1 << 20); + while !buffer.is_char_boundary(remove) { + remove += 1; + } + buffer.drain(..remove); + Ok(()) + }) + } + pub fn output_clear(channel: &str) -> Result<(), Error> { + output_append(channel, "")?; + with(|s| s.output.get_mut(channel).unwrap().clear()); + Ok(()) + } + pub fn output_reveal(channel: &str) -> Result<(), Error> { + output_append(channel, "")?; + with(|s| s.revealed_output.push(channel.into())); + Ok(()) + } pub fn secret_is_set(name: &str) -> bool { with(|s| s.capabilities.iter().any(|c| c == "secrets") && s.secrets.contains_key(name)) } @@ -1278,6 +1782,147 @@ pub mod current { Ok(()) } + pub fn database_upsert(project: &ProjectRes, source: &PluginDataSource) -> Result<(), Error> { + with(|s| { + require(s, "database.sources")?; + if s.restricted { + return Err(Error::Denied("project is not trusted".into())); + } + if source.id.is_empty() || source.id.len() > 128 || source.name.trim().is_empty() { + return Err(Error::Invalid("invalid data source".into())); + } + s.database_sources.insert((project.root.clone(), source.id.clone()), source.clone()); + Ok(()) + }) + } + pub fn database_sources(project: &ProjectRes) -> Result<Vec<String>, Error> { + with(|s| { + require(s, "database.sources")?; + if s.restricted { + return Err(Error::Denied("project is not trusted".into())); + } + Ok(s.database_sources.keys().filter(|(r, _)| r == &project.root).map(|(_, id)| id.clone()).collect()) + }) + } + pub fn database_remove(project: &ProjectRes, id: &str) -> Result<(), Error> { + with(|s| { + require(s, "database.sources")?; + if s.restricted { + return Err(Error::Denied("project is not trusted".into())); + } + let key = (project.root.clone(), id.into()); + s.database_sources + .remove(&key) + .ok_or_else(|| Error::NotFound("no owned data source with this id".into()))?; + s.database_credentials.remove(&key); + Ok(()) + }) + } + pub fn database_credential(project: &ProjectRes, id: &str, _password: &str) -> Result<(), Error> { + with(|s| { + require(s, "database.sources")?; + if s.restricted { + return Err(Error::Denied("project is not trusted".into())); + } + let key = (project.root.clone(), id.into()); + if !s.database_sources.contains_key(&key) { + return Err(Error::NotFound("no owned data source with this id".into())); + } + s.database_credentials.insert(key); + Ok(()) + }) + } + pub fn database_connect(project: &ProjectRes, id: &str) -> Result<(), Error> { + with(|s| { + require(s, "database.sources")?; + if s.restricted { + return Err(Error::Denied("project is not trusted".into())); + } + let key = (project.root.clone(), id.into()); + if !s.database_sources.contains_key(&key) { + return Err(Error::NotFound("no owned data source with this id".into())); + } + s.database_connections.push(key); + Ok(()) + }) + } + pub fn mcp_stdio_command() -> Option<Vec<String>> { + with(|s| s.mcp_stdio.clone()) + } + + pub fn register_run_target(project: &ProjectRes, target: &crate::PluginRunTarget) -> Result<(), Error> { + with(|s| { + require(s, &format!("process:{}", target.program))?; + if target.id.is_empty() + || target.name.is_empty() + || target + .args + .iter() + .chain(&target.workdir_args) + .chain(target.commands.iter().flat_map(|(_, a)| a)) + .any(|a| a.contains('\0')) + { + return Err(Error::Invalid("invalid run target".into())); + } + s.run_targets.insert((project.root.clone(), target.id.clone()), target.clone()); + Ok(()) + }) + } + pub fn unregister_run_target(project: &ProjectRes, id: &str) { + with(|s| { + s.run_targets.remove(&(project.root.clone(), id.into())); + }); + } + pub fn start_run(config: &str, args: &[String], debug: bool) -> Result<String, Error> { + with(|s| { + let c = s.run_configurations.get(config).ok_or_else(|| Error::NotFound(config.into()))?; + require(s, &format!("process:{}", c.program))?; + if debug { + return Err(Error::Invalid("this configuration has no debugger".into())); + } + let id = format!("mock-run-{}", s.configuration_runs.len()); + s.configuration_runs.push((id.clone(), config.into(), args.to_vec(), debug)); + Ok(id) + }) + } + pub fn open_terminal_session(key: &str, title: &str, cmd: &crate::host::Command) -> Result<String, Error> { + command_env(cmd)?; + with(|s| { + require(s, &format!("process:{}", cmd.program))?; + if key.is_empty() || key.len() > 128 || title.is_empty() || title.len() > 256 { + return Err(Error::Invalid("invalid terminal key or title".into())); + } + if let Some((id, _, _)) = s.terminal_sessions.get(key) { + return Ok(id.clone()); + } + s.next_child += 1; + let id = format!("mock-terminal-{}", s.next_child); + s.terminal_sessions.insert(key.into(), (id.clone(), cmd.clone(), title.into())); + Ok(id) + }) + } + pub fn write_terminal(key: &str, text: &str) -> Result<(), Error> { + with(|s| { + if !s.terminal_sessions.contains_key(key) { + return Err(Error::NotFound(key.into())); + } + s.terminal_writes.push((key.into(), text.into())); + Ok(()) + }) + } + pub fn open_content_tab(tab: &crate::ContentTab) -> Result<(), Error> { + if tab.key.is_empty() || tab.title.is_empty() || tab.content.contains('\0') { + return Err(Error::Invalid("invalid content tab".into())); + } + with(|s| { + s.content_tabs.insert(tab.key.clone(), tab.clone()); + }); + Ok(()) + } + pub fn reply_content_tab(request: &str, reply: &crate::ContentTabReply) -> Result<(), Error> { + with(|s| s.content_replies.push((request.into(), reply.clone()))); + Ok(()) + } pub fn open_terminal(title: &str, program: &str, args: &[String]) -> Result<(), Error> { with(|s| { require(s, &format!("process:{program}"))?; diff --git a/sdk/rust/openidea-plugin/src/types.rs b/sdk/rust/openidea-plugin/src/types.rs index 1168f7519..763bad4b6 100644 --- a/sdk/rust/openidea-plugin/src/types.rs +++ b/sdk/rust/openidea-plugin/src/types.rs @@ -67,7 +67,7 @@ pub enum NotificationLevel { } /// 0-based line and UTF-16 code unit column (as in the editor and LSP). -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)] pub struct Position { pub line: u32, pub character: u32, @@ -80,7 +80,7 @@ impl Position { } /// Half-open range; a cursor is an empty range. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)] pub struct Range { pub start: Position, pub end: Position, @@ -211,6 +211,12 @@ impl From<String> for ContextValue { #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum EventKind { + FileClosed, + SettingsChanged, + InputModeChanged, + RunExited, + TerminalExited, + ContentTabAction, FileOpened, FileSaved, FilesChanged, @@ -225,6 +231,12 @@ impl EventKind { /// The name the IDE knows it by. pub fn name(self) -> &'static str { match self { + Self::FileClosed => "file-closed", + Self::SettingsChanged => "settings-changed", + Self::InputModeChanged => "input-mode-changed", + Self::RunExited => "run-exited", + Self::TerminalExited => "terminal-exited", + Self::ContentTabAction => "content-tab-action", Self::FileOpened => "file-opened", Self::FileSaved => "file-saved", Self::FilesChanged => "files-changed", @@ -365,7 +377,7 @@ pub struct CommitDraft { } /// A range in a file. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Location { /// Absolute path. pub path: String, @@ -517,3 +529,92 @@ impl Event { Self::Other { kind, data } } } + +/// An editor in the invoking window. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OpenEditor { + pub path: String, + pub language: String, + pub dirty: bool, + pub active: bool, +} +/// Find in Files options. Include and exclude are project-relative globs. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TextQuery { + pub pattern: String, + pub regex: bool, + pub case_sensitive: bool, + pub whole_word: bool, + pub include: Vec<String>, + pub exclude: Vec<String>, +} +/// A text search match with zero-based UTF-16 coordinates. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TextMatch { + pub location: Location, + pub line: String, +} +/// Bounded text search result. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TextSearch { + pub matches: Vec<TextMatch>, + pub limit_hit: bool, +} +/// A file's Problems view diagnostics from one source. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FileProblems { + pub path: String, + pub source: String, + pub diagnostics: Vec<Diagnostic>, +} +/// An editor annotation. Styles are fixed theme-token names; unknown names are ignored. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Decoration { + pub range: Range, + pub style: String, + pub hover: Option<String>, + pub after: Option<String>, +} +/// A project-tree annotation. Badge is at most two Unicode characters. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FileDecoration { + pub badge: Option<String>, + pub style: String, + pub tooltip: Option<String>, +} +/// Repository history metadata. Date is Unix seconds. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CommitInfo { + pub id: String, + pub short_id: String, + pub author: String, + pub author_email: String, + pub date: i64, + pub subject: String, + pub body: String, + pub parents: Vec<String>, +} +/// Attribution for a zero-based document line. Date is Unix seconds. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BlameLine { + pub line: u32, + pub commit: String, + pub author: String, + pub date: i64, + pub summary: String, +} + +/// Non-secret settings of a plugin-owned project database source (API 1.4). +#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginDataSource { + pub id: String, + pub name: String, + pub driver: String, + pub host: Option<String>, + pub port: Option<u16>, + pub database: Option<String>, + pub user: Option<String>, + pub url: Option<String>, + pub options: Vec<(String, String)>, +} diff --git a/sdk/rust/openidea-plugin/tests/api14.rs b/sdk/rust/openidea-plugin/tests/api14.rs new file mode 100644 index 000000000..26e485a3c --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api14.rs @@ -0,0 +1,62 @@ +use openidea_plugin::{Error, HttpRequest, StatusItem, host, mock::MockHost}; + +#[test] +fn ipc_is_granted_bounded_and_closed_on_drop() { + let mock = MockHost::install().with_capabilities(&[]); + assert!(matches!(host::ipc_connect("/tmp/test-1"), Err(Error::Denied(_)))); + let _mock = mock.with_capabilities(&["ipc:/tmp/test-*"]); + let connection = host::ipc_connect("/tmp/test-1").unwrap(); + connection.write(b"echo").unwrap(); + assert_eq!(connection.read(4, 100).unwrap(), b"echo"); + let others: Vec<_> = (0..7).map(|_| host::ipc_connect("/tmp/test-2").unwrap()).collect(); + assert!(matches!(host::ipc_connect("/tmp/test-3"), Err(Error::Invalid(_)))); + connection.close().unwrap(); + assert!(connection.read(4, 0).is_err()); + host::ipc_connect("/tmp/test-3").unwrap(); + drop(others); +} + +#[test] +fn http_requires_its_own_grant_and_secrets_are_declaration_scoped() { + let mock = MockHost::install().with_capabilities(&["net:*.example.com", "secrets"]); + assert!( + matches!(host::fetch(&HttpRequest::get("http://api.example.com/")), Err(Error::Denied(c)) if c == "net.http:api.example.com") + ); + assert!(matches!(host::secret_set("token", "value"), Err(Error::NotFound(_)))); + mock.declare_secret("token"); + host::secret_set("token", "value").unwrap(); + assert!(host::secret_is_set("token")); + host::secret_delete("token").unwrap(); + assert!(!host::secret_is_set("token")); +} + +#[test] +fn scoped_status_commands_output_and_loopbacks_are_available_in_the_sdk() { + let mock = MockHost::install(); + let item = |text: &str| StatusItem { text: text.into(), tooltip: None, command: None }; + host::set_status_item("state", &item("global")); + host::set_window_status_item("state", &item("window")).unwrap(); + host::set_status_item("state", &item("updated")); + assert_eq!(mock.status_items()["state"].text, "window"); + host::remove_window_status_item("state").unwrap(); + assert_eq!(mock.status_items()["state"].text, "updated"); + mock.on_command("other.command", |arg| Ok(arg.map(str::to_owned))); + assert_eq!(host::execute_command("other.command", Some("result")).unwrap(), Some("result".into())); + assert!(matches!(host::execute_command("missing", None), Err(Error::NotFound(_)))); + mock.on_command("recursive", |_| host::execute_command("recursive", None)); + assert!(matches!(host::execute_command("recursive", None), Err(Error::Invalid(_)))); + host::output_append("build", &"😀".repeat(300_000)).unwrap(); + assert_eq!(mock.output()["build"].len(), 1 << 20); + host::output_clear("build").unwrap(); + assert!(mock.output()["build"].is_empty()); + host::output_reveal("build").unwrap(); + let listener = host::begin_loopback("callback").unwrap(); + assert!(listener.redirect_uri().starts_with("http://127.0.0.1:")); + let other = host::begin_loopback("second").unwrap(); + assert!(matches!(host::begin_loopback("third"), Err(Error::Invalid(_)))); + mock.queue_loopback_query(Some("code=a%2Bb&state=test")); + assert_eq!(listener.wait(100).unwrap().as_deref(), Some("code=a%2Bb&state=test")); + assert!(listener.wait(100).is_err()); + drop(other); + host::begin_loopback("third").unwrap(); +} diff --git a/sdk/rust/openidea-plugin/tests/api14_stage2.rs b/sdk/rust/openidea-plugin/tests/api14_stage2.rs new file mode 100644 index 000000000..2241d3792 --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api14_stage2.rs @@ -0,0 +1,94 @@ +use openidea_plugin::{Decoration, Error, FileDecoration, Range, TextQuery, TextSearch, host, mock::MockHost}; + +#[test] +fn command_permission_distinguishes_own_commands() { + let mock = MockHost::install().with_capabilities(&[]); + for id in ["test.action", "plugin.other.run", "plugin.mock.other.run"] { + mock.on_command(id, |_| Ok(Some("ok".into()))); + } + mock.on_own_command("run", |_| Ok(Some("ok".into()))); + for id in ["test.action", "plugin.other.run", "plugin.mock.other.run"] { + assert!(matches!(host::execute_command(id, None), Err(Error::Denied(c)) if c == "ide.commands")); + } + assert_eq!(host::execute_command("plugin.mock.run", None).unwrap().as_deref(), Some("ok")); + let _mock = mock.with_capabilities(&["ide.commands"]); + assert!(host::execute_command("test.action", None).is_ok()); + assert!(host::execute_command("plugin.other.run", None).is_ok()); +} + +#[test] +fn workbench_wrappers_enforce_grants_and_editor_ranges_need_no_grant() { + let mock = MockHost::install().with_capabilities(&[]); + mock.open_editor("/project/a.txt", "text", "😀text", &[]); + mock.add_file("a.txt", "disk"); + let ctx = mock.invocation(); + let editor = ctx.editor().unwrap(); + editor + .set_selections(&[Range::new(openidea_plugin::Position::new(0, 2), openidea_plugin::Position::new(0, 4))]) + .unwrap(); + editor.reveal(Range::default()).unwrap(); + assert_eq!(mock.revealed_ranges(), [Range::default()]); + let project = ctx.project().unwrap(); + assert!(matches!(host::open_editors(), Err(Error::Denied(c)) if c == "editor.read")); + assert!(host::open_editor("a.txt", None).is_err()); + assert!(project.search_text(&TextQuery::default(), 1).is_err()); + assert!(host::problems(None).is_err()); + assert!(host::set_decorations("a.txt", "set", &[]).is_err()); + assert!(host::clear_decorations("set").is_err()); + assert!(host::clear_file_decorations("set").is_err()); + assert!(project.vcs_log(None, 1).is_err()); + assert!(project.vcs_blame("a.txt").is_err()); + assert!(project.vcs_diff(None, None).is_err()); + let _mock = mock.with_capabilities(&["project.read", "editor.read", "vcs.read"]); + assert_eq!(host::open_editors().unwrap()[0].language, "text"); + host::open_editor("a.txt", Some(Range::default())).unwrap(); + assert!(host::open_editor("../outside", None).is_err()); + assert!(host::problems(None).unwrap().is_empty()); +} + +#[test] +fn decorations_replace_clear_validate_styles_badges_paths_and_limits() { + let mock = MockHost::install(); + let item = |style: &str| Decoration { + range: Range::default(), + style: style.into(), + hover: None, + after: Some("text".into()), + }; + host::set_decorations("a.txt", "test", &[item("highlight"), item("unknown")]).unwrap(); + assert_eq!(mock.decorations().values().next().unwrap().len(), 1); + assert!(host::set_decorations("../outside", "test", &[]).is_err()); + host::set_decorations("a.txt", "test", &[]).unwrap(); + assert!(mock.decorations().values().next().unwrap().is_empty()); + host::clear_decorations("test").unwrap(); + assert!(mock.decorations().is_empty()); + let file = |badge: &str| FileDecoration { badge: Some(badge.into()), style: "added".into(), tooltip: None }; + assert!(host::set_file_decoration("a.txt", "test", &file("abc")).is_err()); + host::set_file_decoration("a.txt", "test", &file("😀a")).unwrap(); + host::clear_file_decorations("test").unwrap(); + assert!(mock.file_decorations().is_empty()); + for i in 0..50 { + host::set_decorations("a.txt", &format!("set{i}"), &[]).unwrap(); + } + assert!(host::set_decorations("a.txt", "overflow", &[]).is_err()); +} + +#[test] +fn search_limits_and_vcs_trust_are_modelled() { + let mock = MockHost::install(); + mock.on_search(|query, limit| { + assert_eq!(query.pattern, "needle"); + assert_eq!(limit, 10_000); + Ok(TextSearch::default()) + }); + let ctx = mock.invocation(); + let project = ctx.project().unwrap(); + project.search_text(&TextQuery { pattern: "needle".into(), ..Default::default() }, u32::MAX).unwrap(); + mock.set_vcs_reads(Vec::new(), "a.txt", Vec::new(), Some("patch".into())); + assert_eq!(project.vcs_diff(None, None).unwrap().as_deref(), Some("patch")); + assert!(project.vcs_diff(None, Some("--output=x")).is_err()); + mock.set_restricted(true); + assert!(project.vcs_log(None, 1).is_err()); + assert!(project.vcs_blame("a.txt").is_err()); + assert!(project.vcs_diff(None, None).is_err()); +} diff --git a/sdk/rust/openidea-plugin/tests/api14_stage3.rs b/sdk/rust/openidea-plugin/tests/api14_stage3.rs new file mode 100644 index 000000000..86f011b73 --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api14_stage3.rs @@ -0,0 +1,55 @@ +use openidea_plugin::{host, mock::MockHost, *}; +#[test] +fn keyed_terminals_require_program_grants_and_reuse_only_live_operations() { + let mock = MockHost::install(); + let cmd = host::Command::new("runner"); + assert!(matches!(host::open_terminal_session("key", "Terminal", &cmd), Err(Error::Denied(_)))); + let mock = mock.with_capabilities(&["process:runner"]); + let first = host::open_terminal_session("key", "Terminal", &cmd).unwrap(); + assert_eq!(host::open_terminal_session("key", "Again", &cmd).unwrap(), first); + host::write_terminal("key", "hello\n").unwrap(); + assert_eq!(mock.terminal_writes(), [("key".into(), "hello\n".into())]); + let event = mock.finish_terminal("key").unwrap(); + assert_eq!(event.terminal_exited().unwrap().id, first); + assert_ne!(host::open_terminal_session("key", "Next", &cmd).unwrap(), first); + let _ = mock.finish_terminal("key"); + assert!(matches!(host::write_terminal("key", "late"), Err(Error::NotFound(_)))); + assert!(host::open_terminal_session("key", "Next", &cmd.env("VALUE", "first").env("VALUE", "collision")).is_err()); +} +#[test] +fn owned_configurations_require_grants_and_reject_debug_without_a_debugger() { + let mock = MockHost::install(); + mock.set_run_configurations(vec![PluginRunConfiguration { + id: "tests".into(), + program: "runner".into(), + ..Default::default() + }]); + assert!(matches!(host::start_run("tests", &[], false), Err(Error::Denied(_)))); + let _mock = mock.with_capabilities(&["process:runner"]); + assert!(host::start_run("tests", &["extra".into()], false).unwrap().starts_with("mock-run-")); + assert!(matches!(host::start_run("tests", &[], true), Err(Error::Invalid(_)))); + assert!(matches!(host::start_run("foreign", &[], false), Err(Error::NotFound(_)))); +} + +#[test] +fn run_targets_require_a_program_grant_and_are_project_scoped() { + let mock = MockHost::install(); + let project = mock.invocation().project().unwrap(); + let target = PluginRunTarget { + id: "ddev".into(), + name: "DDEV".into(), + program: "ddev".into(), + args: vec!["exec".into()], + ..Default::default() + }; + assert!(matches!(host::register_run_target(&project, &target), Err(Error::Denied(_)))); + let mock = mock.with_capabilities(&["process:ddev"]); + host::register_run_target(&project, &target).unwrap(); + assert_eq!(mock.run_targets().len(), 1); + host::register_run_target(&project, &target).unwrap(); + assert_eq!(mock.run_targets().len(), 1); + host::unregister_run_target(&project, "missing"); + assert_eq!(mock.run_targets().len(), 1); + host::unregister_run_target(&project, "ddev"); + assert!(mock.run_targets().is_empty()); +} diff --git a/sdk/rust/openidea-plugin/tests/api14_stage4.rs b/sdk/rust/openidea-plugin/tests/api14_stage4.rs new file mode 100644 index 000000000..418e2972f --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api14_stage4.rs @@ -0,0 +1,39 @@ +use openidea_plugin::{Position, Range, language_features::*, mock::MockHost}; +struct Features; +impl LanguageFeatures for Features { + fn signature_help(&mut self, d: &Document, _: Position) -> Option<SignatureHelp> { + Some(SignatureHelp { + signatures: vec![Signature { + label: d.text.clone(), + documentation: None, + parameters: vec![Parameter { + label: ParameterLabel::Offsets((0, 2)), + documentation: Some(d.language.clone()), + }], + }], + active_signature: 0, + active_parameter: 0, + }) + } +} +impl Injections for Features { + fn injections(&mut self, _: &Document) -> Vec<Injection> { + vec![Injection { + range: Range { start: Position::default(), end: Position { line: 0, character: 2 } }, + language: "sql".into(), + }] + } +} +#[test] +fn optional_traits_use_the_unsaved_mock_snapshot_without_extra_capabilities() { + let host = MockHost::install().with_capabilities(&[]); + host.open_editor("/project/a.txt", "text", "😀", &[]); + host.type_in_editor(" SELECT"); + let doc = host.provider_document().unwrap(); + assert_eq!(doc.version, 2); + let mut provider = Features; + assert_eq!(provider.signature_help(&doc, Position::default()).unwrap().signatures[0].label, "😀 SELECT"); + assert_eq!(provider.injections(&doc)[0].range.end.character, 2); + assert!(provider.document_links(&doc).is_empty()); + assert!(provider.format(&doc, None, FormatOptions { tab_size: 4, insert_spaces: true }).unwrap().is_empty()); +} diff --git a/sdk/rust/openidea-plugin/tests/api14_stage5.rs b/sdk/rust/openidea-plugin/tests/api14_stage5.rs new file mode 100644 index 000000000..83a0472d6 --- /dev/null +++ b/sdk/rust/openidea-plugin/tests/api14_stage5.rs @@ -0,0 +1,34 @@ +use openidea_plugin::{Error, PluginDataSource, host, mock::MockHost}; +#[test] +fn database_wrappers_are_owned_project_scoped_and_write_only() { + let mock = MockHost::install().with_capabilities(&[]); + let p = mock.invocation().project().unwrap(); + let source = + PluginDataSource { id: "local".into(), name: "Local".into(), driver: "postgres".into(), ..Default::default() }; + assert!(matches!(p.upsert_data_source(&source), Err(Error::Denied(c)) if c == "database.sources")); + assert!(p.data_sources().is_err()); + assert!(p.remove_data_source("foreign").is_err()); + assert!(p.set_data_source_credential("local", "password").is_err()); + assert!(p.connect_data_source("local").is_err()); + let mock = mock.with_capabilities(&["database.sources"]); + p.upsert_data_source(&source).unwrap(); + p.set_data_source_credential("local", "password").unwrap(); + p.connect_data_source("local").unwrap(); + assert_eq!(p.data_sources().unwrap(), ["local"]); + assert!(matches!(p.remove_data_source("foreign"), Err(Error::NotFound(_)))); + mock.set_project_root(Some("/other")); + assert!(mock.invocation().project().unwrap().data_sources().unwrap().is_empty()); + mock.set_restricted(true); + assert!(p.data_sources().is_err()); + mock.set_restricted(false); + p.remove_data_source("local").unwrap(); + assert!(p.data_sources().unwrap().is_empty()); +} +#[test] +fn stdio_discovery_returns_only_argv_and_none_while_off() { + let mock = MockHost::install(); + assert!(host::mcp_stdio_command().is_none()); + let command = vec!["openidea".into(), "--mcp-stdio".into(), "--project".into(), "/project".into()]; + mock.set_mcp_stdio_command(Some(command.clone())); + assert_eq!(host::mcp_stdio_command(), Some(command)); +} diff --git a/sdk/rust/openidea-plugin/wit/commit.wit b/sdk/rust/openidea-plugin/wit/commit.wit index a70b889c1..91dbe0e28 100644 --- a/sdk/rust/openidea-plugin/wit/commit.wit +++ b/sdk/rust/openidea-plugin/wit/commit.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// The commit being prepared in the window the current command or event came /// from (the Commit tool window, or the Commit dialog while it is open): the diff --git a/sdk/rust/openidea-plugin/wit/database.wit b/sdk/rust/openidea-plugin/wit/database.wit new file mode 100644 index 000000000..4e77bd9c8 --- /dev/null +++ b/sdk/rust/openidea-plugin/wit/database.wit @@ -0,0 +1,34 @@ +package openidea:plugin@1.4.0; + +/// Plugin-owned project data sources. Requires database.sources and project trust. +/// Runs on the Database subsystem's host, including on remote project servers. +interface database { + use types.{error}; + use workspace.{project}; + /// Non-secret connection settings. Id is private to this plugin (1–128 bytes). + /// Drivers: sqlite, postgres, mysql, sqlServer, clickhouse, oracle. + /// SQLite uses database as its file path, or a sqlite: URL. Options are driver properties. + /// URLs and options must not contain passwords, tokens or other credentials. + record data-source { + id: string, + name: string, + driver: string, + host: option<string>, + port: option<u16>, + database: option<string>, + user: option<string>, + url: option<string>, + options: list<tuple<string, string>>, + } + /// Creates or replaces only this plugin's source; unrelated file entries are preserved. + upsert-source: func(project: borrow<project>, source: data-source) -> result<_, error>; + /// Removes an owned source and its credentials; not-found for every other id. + remove-source: func(project: borrow<project>, id: string) -> result<_, error>; + /// Lists only this plugin's private ids, never connection settings or credentials. + sources: func(project: borrow<project>) -> result<list<string>, error>; + /// Writes an owned source's password directly to the Database secure store. Never readable. + set-credential: func(project: borrow<project>, id: string, password: string) -> result<_, error>; + /// Requests the project's usual connection/introspection in the invoking window. + /// Invalid outside a command/event of that project's window. + connect: func(project: borrow<project>, id: string) -> result<_, error>; +} diff --git a/sdk/rust/openidea-plugin/wit/decorations.wit b/sdk/rust/openidea-plugin/wit/decorations.wit new file mode 100644 index 000000000..1c5a033e6 --- /dev/null +++ b/sdk/rust/openidea-plugin/wit/decorations.wit @@ -0,0 +1,22 @@ +package openidea:plugin@1.4.0; + +/// Project-scoped decorations (API 1.4). Requires project.read; paths must be inside +/// an open project. Sets belong to the plugin and disappear when it stops. +/// Styles: highlight, search, added, modified, removed, warning, error, info, +/// underline, strikethrough, muted. Unknown styles are ignored. Theme tokens only. +interface decorations { + use types.{error, range}; + /// An editor annotation; after is plain text shown after the range. + record decoration { range: range, style: string, hover: option<string>, after: option<string> } + /// A project-tree annotation; badge is at most two Unicode characters. + record file-decoration { badge: option<string>, style: string, tooltip: option<string> } + /// Replace this set for a file. At most 10,000 items/file and 50 editor sets/plugin. + /// Metadata is bounded to 4 MiB per plugin. Unknown styles are ignored. + set: func(path: string, set-id: string, items: list<decoration>) -> result<_, error>; + /// Remove this editor set from every file/project. + clear: func(set-id: string) -> result<_, error>; + /// Replace this set's tree annotation for a file; at most 50 tree sets/plugin. + set-file: func(path: string, set-id: string, item: file-decoration) -> result<_, error>; + /// Remove this tree set from every file/project. + clear-file: func(set-id: string) -> result<_, error>; +} diff --git a/sdk/rust/openidea-plugin/wit/details.wit b/sdk/rust/openidea-plugin/wit/details.wit index 9e889851a..f7342991d 100644 --- a/sdk/rust/openidea-plugin/wit/details.wit +++ b/sdk/rust/openidea-plugin/wit/details.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// What the pane beside the Services tree shows for a plugin's node. interface detail-types { diff --git a/sdk/rust/openidea-plugin/wit/diagnostics.wit b/sdk/rust/openidea-plugin/wit/diagnostics.wit index d5d62de72..499335fde 100644 --- a/sdk/rust/openidea-plugin/wit/diagnostics.wit +++ b/sdk/rust/openidea-plugin/wit/diagnostics.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-diagnostics`): problems in the current, /// possibly unsaved document, for the languages declared with the diff --git a/sdk/rust/openidea-plugin/wit/endpoints.wit b/sdk/rust/openidea-plugin/wit/endpoints.wit index 95302fcb0..f4a446fe0 100644 --- a/sdk/rust/openidea-plugin/wit/endpoints.wit +++ b/sdk/rust/openidea-plugin/wit/endpoints.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// HTTP endpoints found in a project (`[[contributes.endpoints]]`). interface endpoints-provider { diff --git a/sdk/rust/openidea-plugin/wit/events.wit b/sdk/rust/openidea-plugin/wit/events.wit index 8851d18a7..f52030971 100644 --- a/sdk/rust/openidea-plugin/wit/events.wit +++ b/sdk/rust/openidea-plugin/wit/events.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Workbench events, delivered in batches to `event-handler.handle`. interface events { @@ -29,6 +29,9 @@ interface events { /// Delivers `kind` events from now on, for the rest of the session (also /// across idle reclaim: an event activates the plugin again). Declare /// `activation = ["on-event:<kind>"]` to get them before first use. + /// API 1.4 also delivers `file-closed` (JSON path string), `settings-changed` + /// (JSON array of the plugin's changed setting keys), and `input-mode-changed` + /// (JSON {path, mode, sub}) through `other`. The last needs `events.input`. /// Kinds: `file-opened`, `file-saved`, `files-changed` (batched disk changes /// as `other`, with created/modified/removed path lists in JSON), `editor-focused` (the focused editor /// changed), `vcs-changed` (a repository's state changed: commit, diff --git a/sdk/rust/openidea-plugin/wit/exports.wit b/sdk/rust/openidea-plugin/wit/exports.wit index 67d7c6060..434b29ceb 100644 --- a/sdk/rust/openidea-plugin/wit/exports.wit +++ b/sdk/rust/openidea-plugin/wit/exports.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; interface lifecycle { use types.{context, error}; diff --git a/sdk/rust/openidea-plugin/wit/file-actions.wit b/sdk/rust/openidea-plugin/wit/file-actions.wit index 477809180..12b4aec17 100644 --- a/sdk/rust/openidea-plugin/wit/file-actions.wit +++ b/sdk/rust/openidea-plugin/wit/file-actions.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-file-actions`): quick fixes and /// refactorings that create, rename or delete files, next to the text edits diff --git a/sdk/rust/openidea-plugin/wit/fs.wit b/sdk/rust/openidea-plugin/wit/fs.wit index 7cd07fd71..c1a96cf83 100644 --- a/sdk/rust/openidea-plugin/wit/fs.wit +++ b/sdk/rust/openidea-plugin/wit/fs.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Files outside the open projects: the configuration of the program a /// plugin integrates with, a tool installed elsewhere, a licence file. diff --git a/sdk/rust/openidea-plugin/wit/host.wit b/sdk/rust/openidea-plugin/wit/host.wit index 91c3e12fc..c5ce20fbe 100644 --- a/sdk/rust/openidea-plugin/wit/host.wit +++ b/sdk/rust/openidea-plugin/wit/host.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Writes to the IDE log, tagged with the plugin id. interface log { @@ -53,6 +53,11 @@ interface ui { /// Shows or updates the plugin's status bar item `id` in every window. set-status-item: func(id: string, item: status-item); remove-status-item: func(id: string); + /// Overrides the global item in the invoking window only (API 1.4). + /// Invalid outside a command or event of a window; removed on window close or plugin stop. + set-window-status-item: func(id: string, item: status-item) -> result<_, error>; + /// Removes this window's override, falling back to the global item. + remove-window-status-item: func(id: string) -> result<_, error>; /// Sets the plugin's context key `key`, which `when` clauses test as /// `plugin.<id>.<key>`, in the window the current command or event came /// from (every window when there is none). A window's value takes @@ -104,3 +109,23 @@ interface storage { /// Keys starting with `prefix`, sorted. keys: func(prefix: string) -> result<list<string>, error>; } + +/// Executes stable IDE action ids or `plugin.<plugin-id>.<command-id>` (API 1.4). +interface command-runner { + use types.{error}; + /// Only in commands/events of a window. Returns a plugin command's value, + /// or none for IDE actions; unknown ids are not-found. Nested calls are bounded. + /// Requires high-risk `ide.commands`, except for this plugin's own commands. + execute: func(id: string, arg: option<string>) -> result<option<string>, error>; +} + +/// Per-plugin named, read-only Output tabs (API 1.4), with a 1 MiB UTF-8 ring buffer. +interface output { + use types.{error}; + /// Appends text to a channel (at most 32 channels, names 1-128 bytes). + append: func(channel: string, text: string) -> result<_, error>; + /// Clears a channel's buffer. + clear: func(channel: string) -> result<_, error>; + /// Reveals the channel in the invoking window; only commands/events of a window. + reveal: func(channel: string) -> result<_, error>; +} diff --git a/sdk/rust/openidea-plugin/wit/hover.wit b/sdk/rust/openidea-plugin/wit/hover.wit index a95c22758..5ddde792e 100644 --- a/sdk/rust/openidea-plugin/wit/hover.wit +++ b/sdk/rust/openidea-plugin/wit/hover.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-hover`): Markdown added to the hover of /// documents of the languages declared with the `hover` provider kind, diff --git a/sdk/rust/openidea-plugin/wit/indexing.wit b/sdk/rust/openidea-plugin/wit/indexing.wit index 4ee08d64b..227aab726 100644 --- a/sdk/rust/openidea-plugin/wit/indexing.wit +++ b/sdk/rust/openidea-plugin/wit/indexing.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Cross-file associations a plugin publishes for the files it declares under /// `[[contributes.documentIndexers]]`. The host calls `index` in cancellable diff --git a/sdk/rust/openidea-plugin/wit/injections.wit b/sdk/rust/openidea-plugin/wit/injections.wit new file mode 100644 index 000000000..5f9d7c2eb --- /dev/null +++ b/sdk/rust/openidea-plugin/wit/injections.wit @@ -0,0 +1,12 @@ +package openidea:plugin@1.4.0; + +/// Embedded languages. This host routes plugin providers, not embedded LSP servers. +interface injections { + /// A versioned document snapshot. Coordinates are zero-based UTF-16. + record document { path: string, language: string, version: u64, text: string } + /// A host-document range containing the named language. + record injection { range: range, language: string } + use types.{range}; + /// Embedded ranges, refreshed after edits under the editor-provider budget. + injections: func(doc: document) -> list<injection>; +} diff --git a/sdk/rust/openidea-plugin/wit/inlay.wit b/sdk/rust/openidea-plugin/wit/inlay.wit index ab00b0c9b..1098869a5 100644 --- a/sdk/rust/openidea-plugin/wit/inlay.wit +++ b/sdk/rust/openidea-plugin/wit/inlay.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-inlay-hints`): inline hints in documents of /// the languages declared with the `inlay-hints` provider kind, including diff --git a/sdk/rust/openidea-plugin/wit/io.wit b/sdk/rust/openidea-plugin/wit/io.wit index cffd883be..b27e48d62 100644 --- a/sdk/rust/openidea-plugin/wit/io.wit +++ b/sdk/rust/openidea-plugin/wit/io.wit @@ -1,8 +1,13 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Problems in files, shown in the editor and the Problems view. interface diagnostics { use types.{error, range, location}; + /// Problems reported by a source for a file in this window's project. + record file-problems { path: string, source: string, diagnostics: list<diagnostic> } + /// Current Problems view snapshot, optionally filtered to a project file. + /// Requires project.read and an invoking window. Empty for remote projects. + problems: func(path: option<string>) -> result<list<file-problems>, error>; enum severity { error, @@ -35,7 +40,7 @@ interface diagnostics { publish: func(path: string, diagnostics: list<diagnostic>) -> result<_, error>; } -/// HTTP(S) to the hosts granted with `net:<host>`. +/// HTTPS with `net:<host>`; unencrypted HTTP with `net.http:<host>`. interface net { use types.{error}; @@ -46,7 +51,8 @@ interface net { record request { method: string, - /// `https://…`; plain `http://` only to `localhost` / `127.0.0.1`. + /// `https://…` requires `net:<host>`. Plain `http://…` requires + /// `net.http:<host>` (API 1.4), except loopback, which uses `net:<host>`. url: string, /// `{{secret:<name>}}` in a value is replaced by the plugin's secret /// `name` on the way out (requires `secrets`); the plugin never sees it. @@ -72,6 +78,11 @@ interface net { /// OS keychain). Plugins only use them through `{{secret:<name>}}` in /// `net` requests. Requires `secrets`. interface secrets { + use types.{error}; + /// Stores a declared secret without ever exposing it back to plugin code (API 1.4). + set: func(name: string, value: string) -> result<_, error>; + /// Deletes a declared secret (API 1.4). + delete: func(name: string) -> result<_, error>; is-set: func(name: string) -> bool; } @@ -144,3 +155,38 @@ interface process { /// also ends its processes. start: func(cmd: command, cwd: option<borrow<project>>) -> result<child, error>; } + +/// Local sockets on the project machine, including the server for remote projects. +/// Requires `ipc:<path>`; host-expanded `~/`, `$TMPDIR/`, `$XDG_RUNTIME_DIR/` +/// and Windows `\\.\pipe\name` are accepted. API 1.4. +interface ipc { + use types.{error}; + /// A byte stream. At most eight per plugin; dropping closes it, as does stopping the plugin. + resource connection { + /// Writes up to 1 MiB, subject to the current call's budget and cancellation. + write: func(bytes: list<u8>) -> result<_, error>; + /// Reads up to 1 MiB; empty on EOF or timeout. Zero timeout only checks. + read: func(max-bytes: u32, timeout-ms: u32) -> result<list<u8>, error>; + /// Closes the stream; subsequent reads and writes fail. + close: func() -> result<_, error>; + } + /// Connects to an absolute Unix socket or Windows named pipe; never TCP. + connect: func(name: string) -> result<connection, error>; +} + +/// OAuth browser redirects received on the desktop, even for remote projects (API 1.4). +/// No capability needed; only commands/events of a window may use this interface. +interface auth { + use types.{error}; + /// A one-shot IPv4 loopback listener. At most two open per plugin. + resource loopback { + /// `http://127.0.0.1:<random-port>/<path>` for the authorization URL. + redirect-uri: func() -> string; + /// Returns the first matching request's raw query string, or none on timeout. + /// Waiting is excluded from the call budget. The listener serves a short + /// completion page and closes; resource drop and plugin stop also close it. + wait: func(timeout-ms: u32) -> result<option<string>, error>; + } + /// Binds 127.0.0.1 only. `path` has no query or fragment. + begin-loopback: func(path: string) -> result<loopback, error>; +} diff --git a/sdk/rust/openidea-plugin/wit/language-features.wit b/sdk/rust/openidea-plugin/wit/language-features.wit new file mode 100644 index 000000000..c47c4b621 --- /dev/null +++ b/sdk/rust/openidea-plugin/wit/language-features.wit @@ -0,0 +1,114 @@ +package openidea:plugin@1.4.0; + +/// Additional language features, called only for declared API 1.4 provider kinds. +/// Editor calls have a 50 ms soft / 500 ms hard budget; formatting has 5 seconds. +interface language-features { + use types.{error, position, range, text-edit, location}; + /// A versioned document snapshot. Coordinates are zero-based UTF-16. + record document { + path: string, + language: string, + version: u64, + text: string, + } + /// A parameter label, either literal text or UTF-16 offsets within the signature label. + variant parameter-label { text(string), offsets(tuple<u32, u32>) } + /// Indentation options selected by the editor. + record format-options { + tab-size: u32, + insert-spaces: bool, + } + /// One parameter, with optional Markdown documentation. + record parameter { + label: parameter-label, + documentation: option<string>, + } + /// One callable signature; documentation is Markdown. + record signature { + label: string, + documentation: option<string>, + parameters: list<parameter>, + } + /// Callable signatures and zero-based active signature/parameter indices. + record signature-help-result { + signatures: list<signature>, + active-signature: u32, + active-parameter: u32, + } + /// An occurrence of the symbol. Kind: text, read or write; unknown is text. + record highlight { + range: range, + kind: string, + } + /// An absolute path, project-relative path or HTTP(S) URL to follow on click. + record document-link { + range: range, + target: string, + } + /// An inline action. Command must be this plugin's declared command id. + record code-lens { + range: range, + title: string, + command: string, + argument: option<string>, + } + /// A multi-line fold. Kinds: region, comment or imports; unknown is region. + record fold-range { + start-line: u32, + end-line: u32, + kind: string, + } + /// A color literal. All channels are finite floats in the inclusive range 0–1. + record color-info { + range: range, + red: f32, + green: f32, + blue: f32, + alpha: f32, + } + /// An outline node. Parent indexes this list, precedes the child and contains it. + /// Kinds are LSP-style names (e.g. class, method, enum-member); unknown is neutral. + record document-symbol { + name: string, + detail: string, + kind: string, + range: range, + selection-range: range, + parent: option<u32>, + } + /// A line action for this plugin's own declared command. Icons follow line-marker + /// aliases, registered icon names or declared manifest marker SVG paths. + record gutter-action { + line: u32, + icon: option<string>, + tooltip: string, + command: string, + argument: option<string>, + } + /// Format the whole document or the selected range. + format: func(doc: document, range: option<range>, options: format-options) -> result<list<text-edit>, error>; + /// Signature help when the language server has no signatures. + signature-help: func(doc: document, at: position) -> option<signature-help-result>; + /// Occurrences at the caret, appended to language-server highlights. + document-highlights: func(doc: document, at: position) -> list<highlight>; + /// Navigable ranges, appended to language-server document links. + document-links: func(doc: document) -> list<document-link>; + /// Clicks run only this plugin's declared commands. + code-lenses: func(doc: document) -> list<code-lens>; + /// Nested ranges, innermost to outermost. + selection-ranges: func(doc: document, at: position) -> list<range>; + /// Folds to merge with tree-sitter, for any declared language. + folding: func(doc: document) -> list<fold-range>; + /// Color literals to show in the existing color-preview gutter and picker. + colors: func(doc: document) -> list<color-info>; + /// Replacement strings for the selected color range. + color-presentations: func(doc: document, color: color-info) -> list<string>; + /// Used only when no LSP document symbols are available. + document-symbols: func(doc: document) -> list<document-symbol>; + /// Implementation locations, merged with language-server results. + implementation: func(doc: document, at: position) -> list<location>; + /// Type-declaration locations, merged with language-server results. + type-definition: func(doc: document, at: position) -> list<location>; + /// Clicks run only this plugin's declared commands. + gutter-actions: func(doc: document) -> list<gutter-action>; +} diff --git a/sdk/rust/openidea-plugin/wit/mcp.wit b/sdk/rust/openidea-plugin/wit/mcp.wit new file mode 100644 index 000000000..e250f5496 --- /dev/null +++ b/sdk/rust/openidea-plugin/wit/mcp.wit @@ -0,0 +1,16 @@ +package openidea:plugin@1.4.0; + +/// Discovery of the IDE's built-in MCP server; never exposes its URL or token. +interface mcp { + /// Stdio bridge argv for the invoking project; none without a window/project or while off. + stdio-command: func() -> option<list<string>>; +} + +/// Optional tools declared by contributes.mcpTools, under the plugin's grants and command budget. +interface mcp-provider { + use types.{error}; + use invocation.{invocation}; + /// Calls the declared local tool name with JSON arguments and the project's window context. + /// Returns text; tools may encode structured data as JSON text. + call: func(tool: string, arguments: string, ctx: borrow<invocation>) -> result<string, error>; +} diff --git a/sdk/rust/openidea-plugin/wit/plugin.wit b/sdk/rust/openidea-plugin/wit/plugin.wit index 8d1451ac3..04c707d20 100644 --- a/sdk/rust/openidea-plugin/wit/plugin.wit +++ b/sdk/rust/openidea-plugin/wit/plugin.wit @@ -8,7 +8,7 @@ /// `fs` and `workspace`, and everything else goes through the imports below, /// gated by the capabilities granted in `plugin.toml` (a missing one returns /// `error::denied`). -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; world plugin { import log; @@ -17,6 +17,7 @@ world plugin { import fs; import workspace; import editor; + import decorations; import invocation; import vcs; import commit; @@ -26,7 +27,15 @@ world plugin { import net; import secrets; import process; + import ipc; + import output; + import auth; + import command-runner; import views; + import run; + import run-targets; + import database; + import mcp; import settings; export lifecycle; @@ -168,3 +177,26 @@ world plugin-diagnostics { world editor-diagnostics { export diagnostics-provider; } + +/// A plugin that exports project run configurations. +world plugin-run-configurations { include plugin; export run-configurations; } +/// Export-only configurations world, composed with plugin by SDK macros. +world configurations-provider { export run-configurations; } +/// A plugin that parses its test output. +world plugin-test-results { include plugin; export test-results; } +/// Export-only test parser world, composed with plugin by SDK macros. +world test-results-provider { export test-results; } + +/// A plugin with additional API 1.4 language features. +world plugin-language-features { include plugin; export language-features; } +/// Export-only language features, composed with the base plugin world. +world editor-language-features { export language-features; } +/// A plugin that identifies embedded language ranges. +world plugin-injections { include plugin; export injections; } +/// Export-only injections, composed with the base plugin world. +world language-injections { export injections; } + +/// A plugin contributing tools to the built-in MCP server. +world plugin-mcp { include plugin; export mcp-provider; } +/// Export-only MCP tools, composed with the base plugin world. +world mcp-tools { export mcp-provider; } diff --git a/sdk/rust/openidea-plugin/wit/providers.wit b/sdk/rust/openidea-plugin/wit/providers.wit index 317eb81d1..8c0713ec6 100644 --- a/sdk/rust/openidea-plugin/wit/providers.wit +++ b/sdk/rust/openidea-plugin/wit/providers.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Editor features a plugin provides for the languages it declares under /// `[[contributes.providers]]` (the host calls only declared kinds). They run diff --git a/sdk/rust/openidea-plugin/wit/run-targets.wit b/sdk/rust/openidea-plugin/wit/run-targets.wit new file mode 100644 index 000000000..66a41e700 --- /dev/null +++ b/sdk/rust/openidea-plugin/wit/run-targets.wit @@ -0,0 +1,36 @@ +package openidea:plugin@1.4.0; + +/// Project-scoped targets owned by the registering plugin (API 1.4). +interface run-targets { + use types.{error}; + use workspace.{project}; + + /// An argv wrapper using a granted, pinned process program. No shell is used. + /// Normally: program + args + workdir-args + command + command arguments. + /// An override matches wrap::program_name(command) and replaces args AND command: + /// program + override + workdir-args + command arguments. + /// DDEV: program="ddev", args=["exec"], commands=[("composer", ["composer"])]: + /// `ddev exec php x.php` and `ddev composer install`. + /// Each workdir-args element may contain {cwd}, mapped under container-root. + /// All workdir-args are omitted without container-root or for cwd outside the project. + /// Without workdir-args the wrapper manages its container directory itself. + /// The wrapper always starts in the configuration's LOCAL working directory. + /// Environment reaches the wrapper; forwarding into a container depends on its conventions. + /// Registration never selects the target. Debugging plugin targets is invalid. + record run-target { + id: string, + name: string, + program: string, + args: list<string>, + workdir-args: list<string>, + container-root: option<string>, + commands: list<tuple<string, list<string>>>, + } + + /// Registers/replaces this plugin's target in an open, trusted project. + /// Targets survive idle reclamation, and disappear on unregister or plugin stop. + /// Remote project registration and execution take place on the server. + register: func(project: borrow<project>, target: run-target) -> result<_, error>; + /// Removes only this plugin's target from the project; missing ids are harmless. + unregister: func(project: borrow<project>, id: string); +} diff --git a/sdk/rust/openidea-plugin/wit/run.wit b/sdk/rust/openidea-plugin/wit/run.wit new file mode 100644 index 000000000..c1bc79cf9 --- /dev/null +++ b/sdk/rust/openidea-plugin/wit/run.wit @@ -0,0 +1,50 @@ +package openidea:plugin@1.4.0; + +/// Launches this plugin's configurations in the invoking window's Run tool window. +interface run { + use types.{error}; + /// Starts an owned configuration, appending args. Debug requires a debugger. + /// Only from a window command/event. The returned id receives run-exited. + start: func(config: string, args: list<string>, debug: bool) -> result<string, error>; +} + +/// Optional dynamic configurations, refreshed alongside run detectors. +interface run-configurations { + /// A configuration running an approved process in the project sandbox. + record run-configuration { + id: string, + name: string, + program: string, + args: list<string>, + cwd: option<string>, + test: bool, + framework: option<string>, + } + /// Returns the complete set for this project root. + configurations: func(project-root: string) -> list<run-configuration>; +} + +/// Optional parser for test configurations whose framework is plugin. +interface test-results { + use types.{location, extension}; + /// A suite or test; parents must precede children and ids remain stable for a run. + record test-start { id: string, parent: option<string>, name: string, location: option<location> } + /// A successful test and optional elapsed milliseconds. + record test-passed { id: string, duration-ms: option<u32> } + /// A failed test, comparison and optional declaration location. + record test-failed { + id: string, duration-ms: option<u32>, message: string, + expected: option<string>, actual: option<string>, location: option<location>, + } + /// A skipped test and its explanation. + record test-skipped { id: string, reason: option<string> } + /// Output attached to a test or suite. + record test-output { id: string, text: string } + /// Incremental updates to the IDE's test tree. Unknown extensions are ignored. + variant test-event { + suite-started(test-start), test-started(test-start), passed(test-passed), + failed(test-failed), skipped(test-skipped), output(test-output), other(extension), + } + /// Parses one bounded output batch; failures fall back to built-in parsers. + parse: func(config: string, chunk: string) -> list<test-event>; +} diff --git a/sdk/rust/openidea-plugin/wit/semantic.wit b/sdk/rust/openidea-plugin/wit/semantic.wit index ec89fe405..586c324e6 100644 --- a/sdk/rust/openidea-plugin/wit/semantic.wit +++ b/sdk/rust/openidea-plugin/wit/semantic.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-semantic-tokens`): semantic colouring of /// documents of the languages declared with the `semantic-tokens` provider diff --git a/sdk/rust/openidea-plugin/wit/types.wit b/sdk/rust/openidea-plugin/wit/types.wit index 8114a2db0..4192e783c 100644 --- a/sdk/rust/openidea-plugin/wit/types.wit +++ b/sdk/rust/openidea-plugin/wit/types.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Shared types. Positions are 0-based lines and UTF-16 code unit columns, /// as in the editor and LSP. diff --git a/sdk/rust/openidea-plugin/wit/views.wit b/sdk/rust/openidea-plugin/wit/views.wit index 78da35354..a34be2d46 100644 --- a/sdk/rust/openidea-plugin/wit/views.wit +++ b/sdk/rust/openidea-plugin/wit/views.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Declarative views: tool windows declared /// under `[[contributes.views]]` and custom editors under @@ -124,6 +124,31 @@ interface view-types { interface views { use types.{error}; use detail-types.{process-log}; + use process.{command}; + + /// An editor toolbar action; at most one action is the Save handler. + record content-action { id: string, label: string, icon: option<string>, primary: bool, save: bool } + /// A plugin-owned content editor, scoped by key to the invoking window. + record content-tab { + key: string, title: string, file-name: option<string>, language: option<string>, + content: string, read-only: bool, actions: list<content-action>, + } + /// A comparison opened by a content action reply. + record content-diff { left: string, right: string, left-label: option<string>, right-label: option<string>, title: option<string> } + /// Optional updates to an outstanding action or Save request. + record content-tab-reply { + content: option<string>, saved: option<bool>, read-only: option<bool>, + status: option<string>, error: option<string>, diff: option<content-diff>, + } + /// Opens or focuses a running session with this key; env is append-only. + /// Only from a window command/event; the id receives one terminal-exited. + open-terminal-session: func(key: string, title: string, cmd: command) -> result<string, error>; + /// Writes to this plugin's running keyed terminal in the invoking window. + write-terminal: func(key: string, text: string) -> result<_, error>; + /// Opens the existing content editor protocol without a web view. + open-content-tab: func(tab: content-tab) -> result<_, error>; + /// Answers content-tab-action in its originating window. + reply-content-tab: func(request-id: string, reply: content-tab-reply) -> result<_, error>; /// The view (or every custom editor of that id) shows outdated content: /// the IDE renders it again. diff --git a/sdk/rust/openidea-plugin/wit/workspace.wit b/sdk/rust/openidea-plugin/wit/workspace.wit index 556dd0da8..7c1e9dd19 100644 --- a/sdk/rust/openidea-plugin/wit/workspace.wit +++ b/sdk/rust/openidea-plugin/wit/workspace.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; interface workspace { use types.{error, location}; @@ -39,10 +39,28 @@ interface workspace { location: location, } + /// Text search options. Include/exclude are project-relative globs, as in Find in Files. + record text-query { + pattern: string, + regex: bool, + case-sensitive: bool, + whole-word: bool, + %include: list<string>, + exclude: list<string>, + } + /// A match with zero-based lines and UTF-16 columns, and its displayed line. + record text-match { location: location, line: string } + /// Bounded search results; limit-hit says there may be more matches. + record text-search { matches: list<text-match>, limit-hit: bool } + /// A project open in the IDE. resource project { /// Absolute path of the project root. Requires `project.read`. root: func() -> result<string, error>; + /// Find in Files with unsaved documents and project excludes. Requires project.read. + /// Cancellable, at most 10,000 matches. Zero limit returns no matches. + /// Line previews are capped at 4096 UTF-8 bytes, results at 4 MiB (limit-hit). + search-text: func(query: text-query, limit: u32) -> result<text-search, error>; /// Reads a UTF-8 text file inside the project (absolute, or relative /// to the root); paths leaving the root are refused. Unsaved editor /// contents win over the disk. A missing path is `not-found`. @@ -94,10 +112,22 @@ interface editor { text: string, } + /// An editor in the invoking window. + record open-editor { path: string, language: string, dirty: bool, active: bool } + /// Only from commands/events of a window. Requires editor.read. + open-editors: func() -> result<list<open-editor>, error>; + /// Open a project file (project.read) or a file in plugin-dir, selecting a range. + /// Only from commands/events of a window. Paths follow views.open-location. + open: func(path: string, selection: option<range>) -> result<_, error>; + /// A file open in an editor. resource text-editor { /// Absolute path of the file. path: func() -> string; + /// Replace selections in this window's editor. No capability; commands/events only. + set-selections: func(selections: list<range>) -> result<_, error>; + /// Scroll a range into view in this window's editor. No capability; commands/events only. + reveal: func(range: range) -> result<_, error>; /// Language id (`rust`, `solidity`, …). language: func() -> string; /// Current version and text. Requires `editor.read`. @@ -151,6 +181,22 @@ interface vcs { changed-files: u32, } + /// A commit in repository history; date is Unix seconds. + record commit-info { + id: string, short-id: string, author: string, author-email: string, + date: s64, subject: string, body: string, parents: list<string>, + } + /// One zero-based document line's attribution; date is Unix seconds. + record blame-line { line: u32, commit: string, author: string, date: s64, summary: string } + /// Newest commits first, at most 1000. Trusted projects only; requires vcs.read. + log: func(project: borrow<project>, path: option<string>, limit: u32) -> result<list<commit-info>, error>; + /// Includes unsaved text, with changed lines attributed to uncommitted changes. + /// Trusted projects only; requires vcs.read. Paths must be inside the project. + blame: func(project: borrow<project>, path: string) -> result<list<blame-line>, error>; + /// Working tree versus HEAD or against. None outside a repository. Disk text only. + /// Fails above 4 MiB. Trusted projects only; requires vcs.read. + diff: func(project: borrow<project>, path: option<string>, against: option<string>) -> result<option<string>, error>; + /// `none` when the project is not in a repository. Requires `vcs.read`. status: func(project: borrow<project>) -> result<option<repository-status>, error>; /// Configured remotes (`name`, fetch `url`), in order. Requires `vcs.read`. diff --git a/src-tauri/src/commands/lsp.rs b/src-tauri/src/commands/lsp.rs index 14a51a69f..baf2cdc6f 100644 --- a/src-tauri/src/commands/lsp.rs +++ b/src-tauri/src/commands/lsp.rs @@ -460,3 +460,29 @@ pub async fn lsp_execute_code_action( ) -> IdeResult<Vec<ide_core::IpcPath>> { ide_host::ops::lsp::lsp_execute_code_action(&state.host, window.label(), request, doc, action, answers).await } + +#[tauri::command] +#[specta::specta] +pub async fn plugin_language_visuals( + window: Window, + state: State<'_, AppState>, + doc: DocId, +) -> IdeResult<ide_host::ops::lsp::PluginLanguageVisuals> { + ide_host::ops::lsp::plugin_language_visuals(&state.host, window.label(), doc).await +} +#[tauri::command] +#[specta::specta] +pub async fn plugin_color_presentations( + window: Window, + state: State<'_, AppState>, + doc: DocId, + plugin: String, + color: ide_plugin::language_features::ColorInfo, +) -> IdeResult<Vec<String>> { + ide_host::ops::lsp::plugin_color_presentations(&state.host, window.label(), doc, plugin, color).await +} +#[tauri::command] +#[specta::specta] +pub fn plugin_formatters() -> IdeResult<Vec<ide_host::plugins::language_features::FormatterChoice>> { + Ok(ide_host::ops::lsp::plugin_formatters()) +} diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 7c159e924..5f30d329c 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -27,6 +27,7 @@ pub fn apply(state: &AppState) { running.stop(); } ide_agent::set_mcp_executable(None); + state.host.set_mcp_executable(None); *state.mcp.error.lock() = None; return; } @@ -41,6 +42,7 @@ pub fn apply(state: &AppState) { ) { Ok(started) => { let exe = std::env::current_exe().unwrap_or_default(); + state.host.set_mcp_executable(Some(exe.clone())); ide_agent::set_mcp_executable(Some(exe)); *server = Some(started); *state.mcp.error.lock() = None; @@ -54,6 +56,7 @@ pub fn apply(state: &AppState) { /// Stops the server on exit (its discovery file goes with it). pub fn stop(state: &AppState) { + state.host.set_mcp_executable(None); ide_agent::set_mcp_executable(None); if let Some(server) = state.mcp.server.lock().take() { server.stop(); diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 3fdfcaa79..bcfc8a1e4 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -224,6 +224,9 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> { search::replace_in_files, search::replace_preview, search::search_validate, + lsp::plugin_language_visuals, + lsp::plugin_color_presentations, + lsp::plugin_formatters, lsp::lsp_completion, lsp::lsp_inline_completion, lsp::lsp_resolve_completion, @@ -500,10 +503,20 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> { plugin::plugin_post_events, plugin::plugin_event_kinds, plugin::plugin_status_items, + plugin::plugin_decoration_sets, plugin::plugin_context_keys, plugin::plugin_cancel_task, plugin::plugin_answer_prompt, plugin::plugin_answer_commit, + plugin::plugin_answer_command, + plugin::plugin_finish_operation, + plugin::plugin_prepare_debug_run, + plugin::plugin_terminal_session_spawn, + plugin::plugin_terminal_session_close, + plugin::plugin_parse_test_results, + plugin::plugin_content_tab_action, + plugin::plugin_choose_program, + plugin::plugin_program, plugin::plugin_commit_checks, plugin::plugin_view_render, plugin::plugin_view_event, @@ -521,6 +534,8 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> { plugin::plugin_navigation_goto, plugin::plugin_navigation_markers, plugin::plugin_endpoints, + plugin::plugin_run_targets, + plugin::plugin_answer_run_target, plugin::plugin_market, plugin::plugin_market_url, plugin::plugin_set_market_url, diff --git a/src-tauri/src/commands/plugin.rs b/src-tauri/src/commands/plugin.rs index 1fd07a381..6e288d8f2 100644 --- a/src-tauri/src/commands/plugin.rs +++ b/src-tauri/src/commands/plugin.rs @@ -104,6 +104,7 @@ pub fn create_host(app: &AppHandle, data_dir: &Path, project_host: &Arc<ide_host deliver(client, event); }; let host = Arc::new(PluginHost::new(data_dir.join("plugins"), Arc::new(sink), Limits::default())?); + project_host.set_plugin_host(&host); // Plugin languages join detection, LSP and DAP of every project (forks share the store), // and the syntax of the ones without a built-in grammar comes from their plugin. project_host.languages.attach_plugins(host.language_store().clone()); @@ -338,10 +339,12 @@ pub async fn plugin_run_command( editor: Option<PluginEditor>, paths: Option<Vec<PathBuf>>, settings_scope: Option<SettingsScope>, + command_chain: Option<Vec<String>>, ) -> IdeResult<Option<String>> { let host = state.plugins.clone(); let project = state.project(window.label()).ok(); let invocation = Invocation { + command_chain: command_chain.unwrap_or_default(), client: Some(window.label().to_owned()), workspace_root: project.as_ref().map(|p| p.root.clone()), editor: editor.map(PluginEditor::context), @@ -354,8 +357,13 @@ pub async fn plugin_run_command( /// Workbench events from this window (only the kinds of `plugin_event_kinds`). #[tauri::command] #[specta::specta] -pub fn plugin_post_events(window: Window, state: State<'_, AppState>, events: Vec<WorkbenchEvent>) { - state.plugins.post_events(Some(window.label()), events); +pub fn plugin_post_events( + window: Window, + state: State<'_, AppState>, + events: Vec<WorkbenchEvent>, + editor: Option<PluginEditor>, +) { + state.plugins.post_window_events(window.label(), events, editor); } /// Kinds of workbench events plugins want (re-fetch on `subscriptionsChanged`). @@ -375,13 +383,8 @@ pub async fn plugin_event_kinds(app: tauri::AppHandle) -> Vec<EventKind> { /// Every plugin status bar item (later changes arrive as `statusItemChanged`). #[tauri::command] #[specta::specta] -pub fn plugin_status_items(state: State<'_, AppState>) -> Vec<PluginStatusItem> { - state - .plugins - .status_items() - .into_iter() - .map(|(plugin_id, id, item)| PluginStatusItem { plugin_id, id, item }) - .collect() +pub fn plugin_status_items(window: Window, state: State<'_, AppState>) -> Vec<PluginStatusItem> { + state.plugins.status_snapshot(window.label()) } /// Plugin context keys in effect for this window (later changes arrive as `contextChanged`). @@ -876,3 +879,173 @@ pub async fn plugin_navigation_markers( ide_host::plugins::index_ops::plugin_navigation_markers(&state.host, state.plugins.clone(), window.label(), paths) .await } + +/// The invoking window's reply to a plugin command request. +#[tauri::command] +#[specta::specta] +pub fn plugin_answer_command( + window: Window, + state: State<'_, AppState>, + request_id: String, + answer: ide_host::plugins::CommandAnswer, +) -> IdeResult<bool> { + Ok(state.plugins.answer_command(window.label(), &request_id, answer)) +} + +/// Re-approves a plugin's executable, globally or for this window's project. +#[tauri::command] +#[specta::specta] +pub async fn plugin_choose_program( + window: Window, + state: State<'_, AppState>, + plugin_id: String, + capability: String, + scope: SettingsScope, + path: Option<PathBuf>, +) -> IdeResult<PluginInfo> { + let host = state.plugins.clone(); + let root = match scope { + SettingsScope::Global => None, + SettingsScope::Project => Some(state.project(window.label())?.root.clone()), + }; + blocking(move || host.choose_program(&plugin_id, &capability, root.as_deref(), path.as_deref())).await +} + +/// The currently approved executable in the selected scope, after revalidating its pin. +#[tauri::command] +#[specta::specta] +pub async fn plugin_program( + window: Window, + state: State<'_, AppState>, + plugin_id: String, + program: String, + scope: SettingsScope, +) -> IdeResult<ide_core::IpcPath> { + let host = state.plugins.clone(); + let root = match scope { + SettingsScope::Global => None, + SettingsScope::Project => Some(state.project(window.label())?.root.clone()), + }; + blocking(move || host.program_for_project(&plugin_id, &program, root.as_deref()).map(Into::into)).await +} + +/// Project-scoped decoration snapshots; the window filters to its current root. +#[tauri::command] +#[specta::specta] +pub fn plugin_decoration_sets( + state: State<'_, AppState>, +) -> std::collections::BTreeMap<String, Vec<ide_host::plugins::DecorationSet>> { + state.plugins.decoration_sets() +} + +/// Reports exactly one completion from the operation's launching window. +#[tauri::command] +#[specta::specta] +pub fn plugin_finish_operation( + window: Window, + state: State<'_, AppState>, + id: String, + status: Option<i32>, + stopped: bool, +) -> IdeResult<bool> { + Ok(state.plugins.finish_operation(window.label(), &id, status, stopped)) +} + +/// Dispatches a wasm content editor action through the owning plugin's event handler. +#[tauri::command] +#[specta::specta] +pub fn plugin_content_tab_action( + window: Window, + state: State<'_, AppState>, + plugin_id: String, + action: ide_plugin::ContentTabAction, +) -> IdeResult<()> { + state.plugins.content_tab_action(window.label(), &plugin_id, action) +} + +/// Parses one batch of an owned test configuration's output. +#[tauri::command] +#[specta::specta] +pub async fn plugin_parse_test_results( + window: Window, + state: State<'_, AppState>, + config: String, + chunk: String, +) -> IdeResult<Option<Vec<ide_plugin::PluginTestEvent>>> { + let plugins = state.plugins.clone(); + let client = window.label().to_owned(); + tokio::task::spawn_blocking(move || plugins.parse_test_results(&client, &config, chunk)) + .await + .map_err(|e| ide_core::IdeError::internal(e.to_string()))? +} + +/// Starts only the pinned command recorded for this window's terminal operation. +#[tauri::command] +#[specta::specta] +pub async fn plugin_terminal_session_spawn( + window: Window, + state: State<'_, AppState>, + operation: String, + cols: u16, + rows: u16, + on_output: Channel<String>, +) -> IdeResult<ide_terminal::TerminalInfo> { + let (plugins, host, window) = (state.plugins.clone(), state.host.clone(), window.label().to_owned()); + tokio::task::spawn_blocking(move || { + plugins.spawn_terminal_session( + &host, + &window, + &operation, + cols, + rows, + Arc::new(move |chunk| { + let _ = on_output.send(chunk); + }), + ) + }) + .await + .map_err(|e| IdeError::internal(e.to_string()))? +} + +/// Marks a keyed tab closed before ending its process. +#[tauri::command] +#[specta::specta] +pub fn plugin_terminal_session_close(window: Window, state: State<'_, AppState>, operation: String) -> IdeResult<()> { + state.plugins.close_terminal_session(window.label(), &operation) +} + +/// Revalidates a plugin run's executable before debugging, including reruns. +#[tauri::command] +#[specta::specta] +pub async fn plugin_prepare_debug_run( + window: Window, + state: State<'_, AppState>, + config: ide_run::RunConfig, +) -> IdeResult<ide_run::RunConfig> { + let (plugins, client) = (state.plugins.clone(), window.label().to_owned()); + tokio::task::spawn_blocking(move || plugins.prepare_debug_run(&client, config)) + .await + .map_err(|e| IdeError::internal(e.to_string()))? +} + +#[tauri::command] +#[specta::specta] +pub async fn plugin_run_targets( + window: Window, + state: State<'_, AppState>, +) -> IdeResult<Vec<ide_host::plugins::run_targets::RegisteredRunTarget>> { + ide_host::plugins::run_targets::list(&state.host, &state.plugins, window.label()) +} +#[tauri::command] +#[specta::specta] +pub async fn plugin_answer_run_target( + window: Window, + state: State<'_, AppState>, + id: String, + use_target: bool, +) -> IdeResult<ide_core::settings::Settings> { + let host = state.host.clone(); + let plugins = state.plugins.clone(); + let window = window.label().to_owned(); + state.host.blocking(move || ide_host::plugins::run_targets::answer(&host, &plugins, &window, &id, use_target)).await +} diff --git a/src-tauri/src/remote_dev.rs b/src-tauri/src/remote_dev.rs index 81dc86bc9..595012e0a 100644 --- a/src-tauri/src/remote_dev.rs +++ b/src-tauri/src/remote_dev.rs @@ -237,7 +237,9 @@ impl Handler for ClientHandler { } fn on_request(&self, method: &str, params: Value) -> Result<Value, (i64, String)> { - use ide_host::plugins::remote_host::{SECRET_REQUEST, SecretRequest}; + use ide_host::plugins::remote_host::{ + AUTH_REQUEST, AuthRequest, SECRET_REQUEST, SECRET_WRITE, SecretRequest, SecretWrite, + }; match method { // A remote plugin's request header needs a secret from this machine's keychain. SECRET_REQUEST => { @@ -246,6 +248,18 @@ impl Handler for ClientHandler { let state = self.app.state::<AppState>(); Ok(serde_json::to_value(state.plugins.declared_secret(&r.plugin, &r.name)).unwrap_or(Value::Null)) } + SECRET_WRITE => { + let request: SecretWrite = + serde_json::from_value(params).map_err(|e| (ide_jsonrpc::codes::INVALID_PARAMS, e.to_string()))?; + Ok(serde_json::to_value(self.app.state::<AppState>().plugins.remote_secret_write(request)) + .unwrap_or(Value::Null)) + } + AUTH_REQUEST => { + let request: AuthRequest = + serde_json::from_value(params).map_err(|e| (ide_jsonrpc::codes::INVALID_PARAMS, e.to_string()))?; + Ok(serde_json::to_value(self.app.state::<AppState>().plugins.remote_auth(&self.window, request)) + .unwrap_or(Value::Null)) + } _ => Err((ide_jsonrpc::codes::METHOD_NOT_FOUND, format!("unhandled method {method}"))), } } @@ -537,8 +551,11 @@ pub async fn remote_connect( state.remote_dev.0.lock().insert(label.clone(), connection.clone()); let weak = Arc::downgrade(&connection); let peer = connection.peer.clone(); + let auth_plugins = state.plugins.clone(); + let auth_window = label.clone(); tauri::async_runtime::spawn(async move { peer.closed().await; + auth_plugins.forget_remote_loopbacks(&auth_window); if let Some(connection) = weak.upgrade() { connection.forwards.lock().clear(); } diff --git a/src/bindings.ts b/src/bindings.ts index 85e0f2333..1ac0544a4 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -407,6 +407,9 @@ export const commands = { replacePreview: (query: SearchQuery, replacement: string, preserveCase: boolean, path: string, only: Pos[] | null) => typedError<ReplacePreview, IdeError>(__TAURI_INVOKE("replace_preview", { query, replacement, preserveCase, path, only })), /** Validates a query (e.g. regex syntax) while the user types. */ searchValidate: (query: SearchQuery) => typedError<null, IdeError>(__TAURI_INVOKE("search_validate", { query })), + pluginLanguageVisuals: (doc: DocId) => typedError<PluginLanguageVisuals, IdeError>(__TAURI_INVOKE("plugin_language_visuals", { doc })), + pluginColorPresentations: (doc: DocId, plugin: string, color: ColorInfo) => typedError<string[], IdeError>(__TAURI_INVOKE("plugin_color_presentations", { doc, plugin, color })), + pluginFormatters: () => typedError<FormatterChoice[], IdeError>(__TAURI_INVOKE("plugin_formatters")), lspCompletion: (request: RequestId, doc: DocId, pos: Pos, trigger: string | null) => typedError<CompletionList, IdeError>(__TAURI_INVOKE("lsp_completion", { request, doc, pos, trigger })), /** Inline (ghost text) suggestions at `pos`; `automatic` when triggered by typing. */ lspInlineCompletion: (request: RequestId, doc: DocId, pos: Pos, automatic: boolean) => typedError<InlineCompletionItem[], IdeError>(__TAURI_INVOKE("lsp_inline_completion", { request, doc, pos, automatic })), @@ -1062,7 +1065,7 @@ export const commands = { /** Reads the profile a profiled run left behind. */ profilerCollect: (spec: ProfileSpec) => typedError<Profile, IdeError>(__TAURI_INVOKE("profiler_collect", { spec })), /** Routes this window's plugin events to `on_event` (replaces an earlier subscription). */ - pluginSubscribe: (onEvent: Channel<PluginEvent>) => __TAURI_INVOKE<void>("plugin_subscribe", { onEvent }), + pluginSubscribe: (onEvent: Channel<PluginEvent_Deserialize>) => __TAURI_INVOKE<void>("plugin_subscribe", { onEvent }), /** * Installed and linked plugins plus the project plugins of this window's * project (`<project>/.openidea/plugins/*`, rescanned on every call), with @@ -1103,13 +1106,19 @@ export const commands = { path: string, language: string, selections: Range[], -} | null, paths: string[] | null, settingsScope: "global" | "project" | null) => typedError<string | null, IdeError>(__TAURI_INVOKE("plugin_run_command", { pluginId, commandId, arg, editor, paths, settingsScope })), +} | null, paths: string[] | null, settingsScope: "global" | "project" | null, commandChain: string[] | null) => typedError<string | null, IdeError>(__TAURI_INVOKE("plugin_run_command", { pluginId, commandId, arg, editor, paths, settingsScope, commandChain })), /** Workbench events from this window (only the kinds of `plugin_event_kinds`). */ - pluginPostEvents: (events: WorkbenchEvent[]) => __TAURI_INVOKE<void>("plugin_post_events", { events }), + pluginPostEvents: (events: WorkbenchEvent[], editor: { + path: string, + language: string, + selections: Range[], +} | null) => __TAURI_INVOKE<void>("plugin_post_events", { events, editor }), /** Kinds of workbench events plugins want (re-fetch on `subscriptionsChanged`). */ pluginEventKinds: () => __TAURI_INVOKE<EventKind[]>("plugin_event_kinds"), /** Every plugin status bar item (later changes arrive as `statusItemChanged`). */ pluginStatusItems: () => __TAURI_INVOKE<PluginStatusItem[]>("plugin_status_items"), + /** Project-scoped decoration snapshots; the window filters to its current root. */ + pluginDecorationSets: () => __TAURI_INVOKE<{ [key in string]: DecorationSet[] }>("plugin_decoration_sets"), /** Plugin context keys in effect for this window (later changes arrive as `contextChanged`). */ pluginContextKeys: () => __TAURI_INVOKE<PluginContextKey[]>("plugin_context_keys"), /** Cancels a plugin task (`taskStarted` with `cancellable`). */ @@ -1124,6 +1133,24 @@ export const commands = { * longer waits. */ pluginAnswerCommit: (requestId: string, answer: CommitAnswer) => __TAURI_INVOKE<boolean>("plugin_answer_commit", { requestId, answer }), + /** The invoking window's reply to a plugin command request. */ + pluginAnswerCommand: (requestId: string, answer: CommandAnswer) => typedError<boolean, IdeError>(__TAURI_INVOKE("plugin_answer_command", { requestId, answer })), + /** Reports exactly one completion from the operation's launching window. */ + pluginFinishOperation: (id: string, status: number | null, stopped: boolean) => typedError<boolean, IdeError>(__TAURI_INVOKE("plugin_finish_operation", { id, status, stopped })), + /** Revalidates a plugin run's executable before debugging, including reruns. */ + pluginPrepareDebugRun: (config: RunConfig_Deserialize) => typedError<RunConfig_Serialize, IdeError>(__TAURI_INVOKE("plugin_prepare_debug_run", { config })), + /** Starts only the pinned command recorded for this window's terminal operation. */ + pluginTerminalSessionSpawn: (operation: string, cols: number, rows: number, onOutput: Channel<string>) => typedError<TerminalInfo, IdeError>(__TAURI_INVOKE("plugin_terminal_session_spawn", { operation, cols, rows, onOutput })), + /** Marks a keyed tab closed before ending its process. */ + pluginTerminalSessionClose: (operation: string) => typedError<null, IdeError>(__TAURI_INVOKE("plugin_terminal_session_close", { operation })), + /** Parses one batch of an owned test configuration's output. */ + pluginParseTestResults: (config: string, chunk: string) => typedError<PluginTestEvent[] | null, IdeError>(__TAURI_INVOKE("plugin_parse_test_results", { config, chunk })), + /** Dispatches a wasm content editor action through the owning plugin's event handler. */ + pluginContentTabAction: (pluginId: string, action: ContentTabAction) => typedError<null, IdeError>(__TAURI_INVOKE("plugin_content_tab_action", { pluginId, action })), + /** Re-approves a plugin's executable, globally or for this window's project. */ + pluginChooseProgram: (pluginId: string, capability: string, scope: SettingsScope, path: string | null) => typedError<PluginInfo, IdeError>(__TAURI_INVOKE("plugin_choose_program", { pluginId, capability, scope, path })), + /** The currently approved executable in the selected scope, after revalidating its pin. */ + pluginProgram: (pluginId: string, program: string, scope: SettingsScope) => typedError<IpcPath, IdeError>(__TAURI_INVOKE("plugin_program", { pluginId, program, scope })), /** * Runs plugins' before-commit checks on what `selection` commits, until * `request` is cancelled. @@ -1184,6 +1211,8 @@ export const commands = { } | null, IdeError>(__TAURI_INVOKE("plugin_navigation_goto", { doc, pos })), pluginNavigationMarkers: (paths: string[]) => typedError<FileMarkers[], IdeError>(__TAURI_INVOKE("plugin_navigation_markers", { paths })), pluginEndpoints: (pluginId: string) => typedError<PluginEndpoint[], IdeError>(__TAURI_INVOKE("plugin_endpoints", { pluginId })), + pluginRunTargets: () => typedError<RegisteredRunTarget[], IdeError>(__TAURI_INVOKE("plugin_run_targets")), + pluginAnswerRunTarget: (id: string, useTarget: boolean) => typedError<Settings_Serialize, IdeError>(__TAURI_INVOKE("plugin_answer_run_target", { id, useTarget })), /** The marketplace's plugins: read again when `refresh` or when none was read yet, else the last index read. */ pluginMarket: (refresh: boolean) => typedError<MarketPlugin[], IdeError>(__TAURI_INVOKE("plugin_market", { refresh })), pluginMarketUrl: () => __TAURI_INVOKE<string | null>("plugin_market_url"), @@ -1302,8 +1331,8 @@ export const commands = { * edited (not yet applied); the response status, or why there was none. */ proxyCheckConnection: (proxy: ProxySettings, url: string) => typedError<string, IdeError>(__TAURI_INVOKE("proxy_check_connection", { proxy, url })), - dbSources: () => typedError<DataSource[], IdeError>(__TAURI_INVOKE("db_sources")), - dbSaveSources: (sources: DataSource[]) => typedError<null, IdeError>(__TAURI_INVOKE("db_save_sources", { sources })), + dbSources: () => typedError<DataSource_Serialize[], IdeError>(__TAURI_INVOKE("db_sources")), + dbSaveSources: (sources: DataSource_Deserialize[]) => typedError<null, IdeError>(__TAURI_INVOKE("db_save_sources", { sources })), dbSetPassword: (id: string, password: string | null, save: boolean) => typedError<null, IdeError>(__TAURI_INVOKE("db_set_password", { id, password, save })), dbSetSshSecret: (id: string, secret: string | null, save: boolean) => typedError<null, IdeError>(__TAURI_INVOKE("db_set_ssh_secret", { id, secret, save })), dbTrustSshHost: (id: string) => typedError<null, IdeError>(__TAURI_INVOKE("db_trust_ssh_host", { id })), @@ -1401,7 +1430,7 @@ export const commands = { * (and the project's `remoteEnv` on a remote host); running ones keep theirs. */ envReload: () => typedError<EnvReloaded, IdeError>(__TAURI_INVOKE("env_reload")), - dbTest: (source: DataSource, password: string | null, sshSecret: string | null) => typedError<string, IdeError>(__TAURI_INVOKE("db_test", { source, password, sshSecret })), + dbTest: (source: DataSource_Deserialize, password: string | null, sshSecret: string | null) => typedError<string, IdeError>(__TAURI_INVOKE("db_test", { source, password, sshSecret })), dbConnected: () => typedError<string[], IdeError>(__TAURI_INVOKE("db_connected")), dbConnect: (id: string) => typedError<null, IdeError>(__TAURI_INVOKE("db_connect", { id })), dbDisconnect: (id: string) => typedError<null, IdeError>(__TAURI_INVOKE("db_disconnect", { id })), @@ -2423,12 +2452,24 @@ export type CodeStyleSettings_Serialize = { formatterTagsRegexp: boolean, }; +/** ColorInfo data. */ +export type ColorInfo = { + range: Range, + red: number | null, + green: number | null, + blue: number | null, + alpha: number | null, +}; + export type Command = { title: string, command: string, arguments: string[], }; +/** The invoking window's response to a command request. */ +export type CommandAnswer = { kind: "value"; value: string | null } | { kind: "notFound" } | { kind: "failed"; value: string }; + export type CommandForm = { command: Command, formFields: FormField[], @@ -2715,6 +2756,53 @@ export type ConsoleSettings = { export type ContainerOp = "start" | "stop" | "restart" | "pause" | "unpause" | "kill"; +/** ContentAction data exchanged with the IDE. */ +export type ContentAction = { + id: string, + label: string, + icon: string | null, + primary: boolean, + save: boolean, +}; + +/** ContentDiff data exchanged with the IDE. */ +export type ContentDiff = { + left: string, + right: string, + leftLabel: string | null, + rightLabel: string | null, + title: string | null, +}; + +/** ContentTab data exchanged with the IDE. */ +export type ContentTab = { + key: string, + title: string, + fileName: string | null, + language: string | null, + content: string, + readOnly: boolean, + actions: ContentAction[], +}; + +/** ContentTabAction data exchanged with the IDE. */ +export type ContentTabAction = { + requestId: string, + key: string, + action: string, + content: string, +}; + +/** ContentTabReply data exchanged with the IDE. */ +export type ContentTabReply = { + content: string | null, + saved: boolean | null, + readOnly: boolean | null, + status: string | null, + error: string | null, + diff: ContentDiff | null, +}; + /** Number of declarations of one extension point. */ export type ContributionCount = { point: string, @@ -2820,7 +2908,9 @@ export type DataBreakpointInfo = { accessTypes: string[], }; -export type DataSource = { +export type DataSource = DataSource_Serialize | DataSource_Deserialize; + +export type DataSource_Deserialize = { id?: string, name?: string, driver?: DbDriver, @@ -2856,6 +2946,52 @@ export type DataSource = { * so not written to it. */ global?: boolean, + /** Plugin id owning this source; absent for user-managed sources. */ + pluginOwner?: string | null, + /** Id private to the owner, never used as a connection target. */ + pluginSourceId?: string | null, +}; + +export type DataSource_Serialize = { + id: string, + name: string, + driver: DbDriver, + host: string, + port: number | null, + database: string, + user: string, + /** SQLite database file (absolute, or relative to the project root). */ + file: IpcPath | null, + ssl: DbSslMode, + /** Keep the password in the saved passwords. */ + savePassword: boolean, + /** Refuse statements other than queries (enforced by the database session where possible). */ + readOnly: boolean, + /** SSH tunnel settings (`None`: never configured; kept but unused when not `enabled`). */ + ssh: DbSsh | null, + /** Wire-compatible product preset (`cockroachdb`, `tidb`…; empty: the driver itself). */ + vendor: string, + color: DbColor | null, + comment: string, + /** Connect with [`DataSource::url`] instead of the host / port / database fields. */ + urlOnly: boolean, + url: string, + /** PEM files; `~/` is the home directory. */ + sslCaFile: string, + sslCertFile: string, + sslKeyFile: string, + options: DbOptions, + introspection: DbIntrospection, + properties: DbProperty[], + /** + * Stored app-wide and listed in every project. Implied by the file it is read from, + * so not written to it. + */ + global: boolean, + /** Plugin id owning this source; absent for user-managed sources. */ + pluginOwner?: string | null, + /** Id private to the owner, never used as a connection target. */ + pluginSourceId?: string | null, }; /** System Settings › Date Formats (Git log, Local History, blame). */ @@ -3186,6 +3322,24 @@ export type DebuggerSettings = { python?: PythonDebuggerSettings, }; +/** An editor decoration. */ +export type Decoration = { + range: Range, + style: string, + hover: string | null, + after: string | null, +}; + +/** A plugin-owned editor or project-tree decoration set for one file. */ +export type DecorationSet = { + root: IpcPath, + path: IpcPath, + setId: string, + tree: boolean, + items: Decoration[], + file: FileDecoration | null, +}; + /** A deleted local branch, enough to restore it (IDEA's "Restore" notification action). */ export type DeletedBranch = { name: string, @@ -3785,7 +3939,7 @@ export type ErrorKind = "notFound" | "alreadyExists" | "permissionDenied" | "inv "unavailable" | "io" | "protocol" | "internal"; /** Kinds of workbench events a plugin can subscribe to (`on-event:<kind>`). */ -export type EventKind = "file-opened" | "file-saved" | "files-changed" | "editor-focused" | "action-performed" | "vcs-changed" | "scheduled"; +export type EventKind = "file-closed" | "settings-changed" | "input-mode-changed" | "run-exited" | "terminal-exited" | "content-tab-action" | "file-opened" | "file-saved" | "files-changed" | "editor-focused" | "action-performed" | "vcs-changed" | "scheduled"; /** An exception breakpoint filter offered by an adapter (`exceptionBreakpointFilters`). */ export type ExceptionFilter = { @@ -3893,6 +4047,13 @@ export type FileCoverage = { branchesCovered: number, }; +/** A tree decoration. */ +export type FileDecoration = { + badge: string | null, + style: string, + tooltip: string | null, +}; + export type FileEdit = { kind: "edit"; path: IpcPath; edits: TextEdit[]; /** LSP TextDocumentEdit version; null/unversioned edits use content preconditions. */ version?: number | null } | { kind: "create"; path: IpcPath; overwrite: boolean } | { kind: "rename"; from: IpcPath; to: IpcPath } | { kind: "delete"; path: IpcPath; recursive: boolean }; @@ -4127,6 +4288,12 @@ export type FormatOptions = { tool?: ExternalFormatter | null, }; +export type FormatterChoice = { + id: string, + name: string, + languages: string[], +}; + /** A port of the remote host reachable on this machine at `localhost:local_port`. */ export type ForwardedPort = { remotePort: number, @@ -4400,6 +4567,15 @@ export type GradleTask = { description: string | null, }; +/** GutterAction data. */ +export type GutterAction = { + line: number, + icon: string | null, + tooltip: string, + command: string, + argument: string | null, +}; + /** HEAD before and after a history rewrite, for its Undo. */ export type HeadChange = { before: string, @@ -4587,7 +4763,9 @@ export type IdeError = { data?: ErrorData | null, }; -export type IdeEvent = { type: "mcpRequest"; id: number; request: McpUiRequest } | { type: "mcpRequestClosed"; id: number } | +export type IdeEvent = +/** Project data source settings changed; reload the Database tree. */ +{ type: "dbSourcesChanged" } | { type: "mcpRequest"; id: number; request: McpUiRequest } | { type: "mcpRequestClosed"; id: number } | /** Project window membership or selection changed. */ { type: "windowGroupsChanged" } | /** A window entered or left full screen, itself or as a member of a full-screen window group. */ @@ -4746,6 +4924,12 @@ export type IndexedMarker = { icon: string | null, }; +/** Injection data. */ +export type Injection = { + range: Range, + language: string, +}; + export type InlayHint = { position: Pos, label: string, @@ -4808,6 +4992,13 @@ export type InlineCompletionSettings = { disabledLanguages?: string[], }; +/** The focused editor's input mode and optional visual sub-mode. */ +export type InputMode = { + path: string, + mode: string, + sub: string | null, +}; + export type InspectEvent = /** Files about to be inspected. */ { type: "started"; total: number } | @@ -5234,6 +5425,8 @@ export type LanguageServerSettings_Serialize = { }; export type LanguagesSettings = { + /** Explicit plugin formatter, by language id. Empty entries use the normal formatter policy. */ + pluginFormatters?: { [key in string]: string }, python?: PythonSettings, node?: NodeSettings, php?: PhpSettings, @@ -5841,6 +6034,8 @@ export type NodeAt = { export type NodeSettings = { /** `node` executable; empty = the one on `PATH`. */ interpreter?: string, + /** Run PHP/Composer or Node/package-manager commands in this target; empty = project default, `local` = local. */ + runTarget?: string, /** `npm` | `pnpm` | `yarn`; empty = detect from the lock file / `packageManager`. */ packageManager?: string, }; @@ -6038,6 +6233,8 @@ export type PhpLevelSource = export type PhpSettings = { /** `php` CLI executable; empty = the one on `PATH`. */ interpreter?: string, + /** Run PHP/Composer or Node/package-manager commands in this target; empty = project default, `local` = local. */ + runTarget?: string, /** * PHP version the language server checks the code against (`7.4`); empty = from * `composer.json` (`config.platform.php`, else the lowest version `require.php` allows). @@ -6122,78 +6319,195 @@ export type PluginEndpoint = { }; /** Backend → UI notifications (Tauri event `ide://plugin`). */ -export type PluginEvent = +export type PluginEvent = PluginEvent_Serialize | PluginEvent_Deserialize; + +/** Backend → UI notifications (Tauri event `ide://plugin`). */ +export type PluginEvent_Deserialize = +/** Project target registrations or remembered prompt answers changed. */ +({ type: "runTargetsChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** A window-owned operation; an empty request id queues an asynchronous run. */ +({ type: "windowRequest"; pluginId: string; requestId: string; request: WindowRequest_Deserialize }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Reply to a content action in this window. */ +({ type: "contentTabReply"; pluginId: string; requestId: string; reply: ContentTabReply }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; request?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** Plugins, their status or their contributions changed: re-fetch `plugin_list`. */ -{ type: "commandsChanged" } | +({ type: "commandsChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Complete replacement of one plugin's project-scoped decorations. */ +({ type: "decorationsChanged"; pluginId: string; sets: DecorationSet[] }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Open a file in the invoking window. */ +({ type: "editorOpen"; path: IpcPath; selection: Range | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Change selection or scrolling only in this window's already open editor. */ +({ type: "editorRanges"; path: IpcPath; ranges: Range[]; reveal: boolean }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; reply?: never; request?: never; requestId?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** A bounded output channel snapshot, or withdrawal when the plugin stops. */ +({ type: "outputChanged"; pluginId: string; channel: string; text: string | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Reveal a read-only Output tab in the receiving window. */ +({ type: "revealOutput"; pluginId: string; channel: string; text: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Execute an action through the receiving window and answer over IPC. */ +({ type: "executeCommand"; requestId: string; id: string; arg: string | null; chain: string[] }) & { actions?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; channel?: never; column?: never; cwd?: never; done?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** Cached document associations were disposed (reload, disable or failure). */ -{ type: "documentIndexesChanged" } | +({ type: "documentIndexesChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** * The routes `plugin_id` reports for the project `root` may have changed: * ask `plugin_endpoints` again. */ -{ type: "endpointsChanged"; pluginId: string; root: IpcPath } | +({ type: "endpointsChanged"; pluginId: string; root: IpcPath }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** * The marketplace offers newer versions of installed plugins (announced * once per set of versions). */ -{ type: "updatesAvailable"; updates: PluginUpdate[] } | { type: "notification"; pluginId: string; level: NotificationLevel; title: string; body: string; +({ type: "updatesAvailable"; updates: PluginUpdate[] }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; url?: never; value?: never; view?: never } | ({ type: "notification"; pluginId: string; level: NotificationLevel; title: string; body: string; /** Buttons running the plugin's own commands (`ui.notify`). */ -actions: NotificationAction[] } | +actions: NotificationAction[] }) & { arg?: never; args?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** A plugin's status bar item was set (`item`) or removed. */ -{ type: "statusItemChanged"; pluginId: string; id: string; item: StatusItem | null } | +({ type: "statusItemChanged"; pluginId: string; id: string; item: StatusItem | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** An override in the receiving window; removal exposes the global item. */ +({ type: "windowStatusItemChanged"; pluginId: string; id: string; item: StatusItem | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** * A plugin's context key was set (`value`: JSON text) or removed, in the * emitted window (every window when the client is `None`). */ -{ type: "contextChanged"; pluginId: string; key: string; value: string | null } | +({ type: "contextChanged"; pluginId: string; key: string; value: string | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; view?: never } | /** A plugin began a task: show it with the background tasks. */ -{ type: "taskStarted"; pluginId: string; taskId: string; title: string; cancellable: boolean } | { type: "taskProgress"; pluginId: string; taskId: string; done: number; total: number | null; message: string | null } | { type: "taskEnded"; pluginId: string; taskId: string } | +({ type: "taskStarted"; pluginId: string; taskId: string; title: string; cancellable: boolean }) & { actions?: never; arg?: never; args?: never; body?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; text?: never; theme?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | ({ type: "taskProgress"; pluginId: string; taskId: string; done: number; total: number | null; message: string | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; text?: never; theme?: never; title?: never; updates?: never; url?: never; value?: never; view?: never } | ({ type: "taskEnded"; pluginId: string; taskId: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** * The kinds of workbench events plugins want changed: re-fetch * `plugin_event_kinds` (the UI reports only those). */ -{ type: "subscriptionsChanged" } | { type: "themeChanged"; theme: ActiveTheme | null } | +({ type: "subscriptionsChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | ({ type: "themeChanged"; theme: ActiveTheme | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** * A plugin needs capabilities the user has not granted yet (first use * after migrating from 0.1): show its consent dialog. */ -{ type: "consentRequested"; pluginId: string; capabilities: CapabilityInfo[] } | +({ type: "consentRequested"; pluginId: string; capabilities: CapabilityInfo[] }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** A view (or the custom editors) of a plugin must be rendered again. */ -{ type: "viewChanged"; pluginId: string; view: string } | +({ type: "viewChanged"; pluginId: string; view: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never } | /** For the script of a view's `html` body. */ -{ type: "viewMessage"; pluginId: string; view: string; message: string } | +({ type: "viewMessage"; pluginId: string; view: string; message: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never } | /** Open a terminal running `program` (already approved and verified). */ -{ type: "openTerminal"; pluginId: string; title: string; program: IpcPath; args: string[]; cwd: IpcPath } | { type: "openLocation"; pluginId: string; path: IpcPath; line: number; column: number } | +({ type: "openTerminal"; pluginId: string; title: string; program: IpcPath; args: string[]; cwd: IpcPath }) & { actions?: never; arg?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | ({ type: "openLocation"; pluginId: string; path: IpcPath; line: number; column: number }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; log?: never; message?: never; newWindow?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** * Open (or focus) the log tab `key` following `log` (`views.open-log-tab`); * it starts the program with `plugin_log_tab_spawn`. */ -{ type: "openLogTab"; pluginId: string; key: string; title: string; log: ServiceLog } | +({ type: "openLogTab"; pluginId: string; key: string; title: string; log: ServiceLog }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; level?: never; line?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** * Plugin languages, or the servers they may start, changed: re-fetch * `languages` (the host re-detects open documents: [`crate::Host::plugin_languages_changed`]). */ -{ type: "languagesChanged" } | +({ type: "languagesChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** Copy `text` to the system clipboard. */ -{ type: "copyToClipboard"; pluginId: string; text: string } | +({ type: "copyToClipboard"; pluginId: string; text: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** Open `url` (`http(s)://`) in the system browser. */ -{ type: "openUrl"; pluginId: string; url: string } | +({ type: "openUrl"; pluginId: string; url: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; value?: never; view?: never } | /** A window command/event requests the normal File > Open flow. */ -{ type: "openProject"; pluginId: string; path: string; newWindow: boolean } | +({ type: "openProject"; pluginId: string; path: string; newWindow: boolean }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** * A command waits for the user's answer: show `prompt`, then report it with * `plugin_answer_prompt(prompt_id, …)`. */ -{ type: "promptRequested"; pluginId: string; promptId: string; prompt: Prompt } | +({ type: "promptRequested"; pluginId: string; promptId: string; prompt: Prompt }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** The call that asked gave up (cancelled, or its plugin stopped): close the prompt. */ -{ type: "promptClosed"; promptId: string } | +({ type: "promptClosed"; promptId: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | /** Bring the tool window of the plugin's view `view` to the front. */ -{ type: "revealView"; pluginId: string; view: string } | +({ type: "revealView"; pluginId: string; view: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never } | /** * A command needs the window's Commit view: answer with * `plugin_answer_commit(request_id, …)`. Closed like a prompt (`PromptClosed`). */ -{ type: "commitRequested"; pluginId: string; requestId: string; request: CommitRequest }; +({ type: "commitRequested"; pluginId: string; requestId: string; request: CommitRequest }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never }; + +/** Backend → UI notifications (Tauri event `ide://plugin`). */ +export type PluginEvent_Serialize = +/** Project target registrations or remembered prompt answers changed. */ +({ type: "runTargetsChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** A window-owned operation; an empty request id queues an asynchronous run. */ +({ type: "windowRequest"; pluginId: string; requestId: string; request: WindowRequest_Serialize }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Reply to a content action in this window. */ +({ type: "contentTabReply"; pluginId: string; requestId: string; reply: ContentTabReply }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; request?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Plugins, their status or their contributions changed: re-fetch `plugin_list`. */ +({ type: "commandsChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Complete replacement of one plugin's project-scoped decorations. */ +({ type: "decorationsChanged"; pluginId: string; sets: DecorationSet[] }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Open a file in the invoking window. */ +({ type: "editorOpen"; path: IpcPath; selection: Range | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Change selection or scrolling only in this window's already open editor. */ +({ type: "editorRanges"; path: IpcPath; ranges: Range[]; reveal: boolean }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; reply?: never; request?: never; requestId?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** A bounded output channel snapshot, or withdrawal when the plugin stops. */ +({ type: "outputChanged"; pluginId: string; channel: string; text: string | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Reveal a read-only Output tab in the receiving window. */ +({ type: "revealOutput"; pluginId: string; channel: string; text: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Execute an action through the receiving window and answer over IPC. */ +({ type: "executeCommand"; requestId: string; id: string; arg: string | null; chain: string[] }) & { actions?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; channel?: never; column?: never; cwd?: never; done?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Cached document associations were disposed (reload, disable or failure). */ +({ type: "documentIndexesChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** + * The routes `plugin_id` reports for the project `root` may have changed: + * ask `plugin_endpoints` again. + */ +({ type: "endpointsChanged"; pluginId: string; root: IpcPath }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** + * The marketplace offers newer versions of installed plugins (announced + * once per set of versions). + */ +({ type: "updatesAvailable"; updates: PluginUpdate[] }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; url?: never; value?: never; view?: never } | ({ type: "notification"; pluginId: string; level: NotificationLevel; title: string; body: string; +/** Buttons running the plugin's own commands (`ui.notify`). */ +actions: NotificationAction[] }) & { arg?: never; args?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** A plugin's status bar item was set (`item`) or removed. */ +({ type: "statusItemChanged"; pluginId: string; id: string; item: StatusItem | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** An override in the receiving window; removal exposes the global item. */ +({ type: "windowStatusItemChanged"; pluginId: string; id: string; item: StatusItem | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** + * A plugin's context key was set (`value`: JSON text) or removed, in the + * emitted window (every window when the client is `None`). + */ +({ type: "contextChanged"; pluginId: string; key: string; value: string | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; view?: never } | +/** A plugin began a task: show it with the background tasks. */ +({ type: "taskStarted"; pluginId: string; taskId: string; title: string; cancellable: boolean }) & { actions?: never; arg?: never; args?: never; body?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; text?: never; theme?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | ({ type: "taskProgress"; pluginId: string; taskId: string; done: number; total: number | null; message: string | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; text?: never; theme?: never; title?: never; updates?: never; url?: never; value?: never; view?: never } | ({ type: "taskEnded"; pluginId: string; taskId: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** + * The kinds of workbench events plugins want changed: re-fetch + * `plugin_event_kinds` (the UI reports only those). + */ +({ type: "subscriptionsChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | ({ type: "themeChanged"; theme: ActiveTheme | null }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** + * A plugin needs capabilities the user has not granted yet (first use + * after migrating from 0.1): show its consent dialog. + */ +({ type: "consentRequested"; pluginId: string; capabilities: CapabilityInfo[] }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** A view (or the custom editors) of a plugin must be rendered again. */ +({ type: "viewChanged"; pluginId: string; view: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never } | +/** For the script of a view's `html` body. */ +({ type: "viewMessage"; pluginId: string; view: string; message: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never } | +/** Open a terminal running `program` (already approved and verified). */ +({ type: "openTerminal"; pluginId: string; title: string; program: IpcPath; args: string[]; cwd: IpcPath }) & { actions?: never; arg?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | ({ type: "openLocation"; pluginId: string; path: IpcPath; line: number; column: number }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; log?: never; message?: never; newWindow?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** + * Open (or focus) the log tab `key` following `log` (`views.open-log-tab`); + * it starts the program with `plugin_log_tab_spawn`. + */ +({ type: "openLogTab"; pluginId: string; key: string; title: string; log: ServiceLog }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; level?: never; line?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** + * Plugin languages, or the servers they may start, changed: re-fetch + * `languages` (the host re-detects open documents: [`crate::Host::plugin_languages_changed`]). + */ +({ type: "languagesChanged" }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Copy `text` to the system clipboard. */ +({ type: "copyToClipboard"; pluginId: string; text: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Open `url` (`http(s)://`) in the system browser. */ +({ type: "openUrl"; pluginId: string; url: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; value?: never; view?: never } | +/** A window command/event requests the normal File > Open flow. */ +({ type: "openProject"; pluginId: string; path: string; newWindow: boolean }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** + * A command waits for the user's answer: show `prompt`, then report it with + * `plugin_answer_prompt(prompt_id, …)`. + */ +({ type: "promptRequested"; pluginId: string; promptId: string; prompt: Prompt }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** The call that asked gave up (cancelled, or its plugin stopped): close the prompt. */ +({ type: "promptClosed"; promptId: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; pluginId?: never; program?: never; prompt?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never } | +/** Bring the tool window of the plugin's view `view` to the front. */ +({ type: "revealView"; pluginId: string; view: string }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; request?: never; requestId?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never } | +/** + * A command needs the window's Commit view: answer with + * `plugin_answer_commit(request_id, …)`. Closed like a prompt (`PromptClosed`). + */ +({ type: "commitRequested"; pluginId: string; requestId: string; request: CommitRequest }) & { actions?: never; arg?: never; args?: never; body?: never; cancellable?: never; capabilities?: never; chain?: never; channel?: never; column?: never; cwd?: never; done?: never; id?: never; item?: never; key?: never; level?: never; line?: never; log?: never; message?: never; newWindow?: never; path?: never; program?: never; prompt?: never; promptId?: never; ranges?: never; reply?: never; reveal?: never; root?: never; selection?: never; sets?: never; taskId?: never; text?: never; theme?: never; title?: never; total?: never; updates?: never; url?: never; value?: never; view?: never }; /** A plugin's own icon (`icon`, `icon-dark` of its manifest): sanitised SVG markup. */ export type PluginIcon = { @@ -6233,12 +6547,39 @@ export type PluginInfo = { previousVersion: string | null, }; +/** + * Commands of this module the remote server answers. + * Versioned plugin visuals, refreshed after the editor synchronizes its buffer. + */ +export type PluginLanguageVisuals = { + version: number, + injections: Injection[], + colors: ([string, ColorInfo])[], + gutterActions: ([string, GutterAction])[], +}; + +export type PluginLocation = { + path: string, + range: Range, +}; + /** A line a view's `process` body printed. */ export type PluginOutputLine = { stderr: boolean, text: string, }; +/** A plugin-owned argv wrapper for project runs (API 1.4). */ +export type PluginRunTarget = { + id: string, + name: string, + program: string, + args: string[], + workdirArgs: string[], + containerRoot: string | null, + commands: ([string, string[]])[], +}; + /** A secret a plugin declares, and whether the user stored it. */ export type PluginSecretState = { name: string, @@ -6285,11 +6626,28 @@ export type PluginStatus = /** A plugin's status bar item. */ export type PluginStatusItem = { + /** Whether this item overrides the global item in the requesting window. */ + window: boolean, pluginId: string, id: string, item: StatusItem, }; +/** A test update, carried by the optional test-results export. */ +export type PluginTestEvent = { + type: "suiteStarted", +} & TestStart | { + type: "testStarted", +} & TestStart | { + type: "passed", +} & TestPassed | { + type: "failed", +} & TestFailed | { + type: "skipped", +} & TestSkipped | { + type: "output", +} & TestOutput | { type: "other"; kind: string; data: string }; + /** A newer version of an installed plugin in the marketplace. */ export type PluginUpdate = { id: string, @@ -6764,6 +7122,15 @@ export type ReferencesIncomplete = /** A native query failed; indexed anchors are still available. */ "serverFailed"; +export type RegisteredRunTarget = { + /** Opaque selector id, namespaced by the host. */ + id: string, + pluginId: string, + pluginName: string, + target: PluginRunTarget, + prompt: boolean, +}; + /** A registry agent this machine can run (or has installed). */ export type RegistryAgent = { id: string, @@ -7055,6 +7422,14 @@ export type RunConfig_Serialize = { allowParallel?: boolean, } & RunExtras_Serialize; +/** RunExited data exchanged with the IDE. */ +export type RunExited = { + id: string, + config: string, + status: number | null, + killed: boolean, +}; + export type RunExtras = RunExtras_Serialize | RunExtras_Deserialize; export type RunExtras_Deserialize = { @@ -7188,6 +7563,8 @@ export type RunInfo = { /** Run/Debug configuration behaviour. */ export type RunSettings = { + /** Target for configurations without an explicit target. Empty = local. */ + defaultTarget?: string, confirmRerun?: boolean, /** Ask before closing a Run tab whose process is still running. */ confirmTerminate?: boolean, @@ -7962,6 +8339,14 @@ export type TableSort = { /** A long-running background task shown in the status bar. */ export type TaskId = number; +/** TerminalExited data exchanged with the IDE. */ +export type TerminalExited = { + id: string, + key: string, + status: number | null, + closed: boolean, +}; + /** An integrated terminal session. */ export type TerminalId = number; @@ -8031,6 +8416,42 @@ export type TestCaseReport = { export type TestCaseStatus = "passed" | "failed" | "skipped"; +/** Incremental test tree data. */ +export type TestFailed = { + id: string, + durationMs: number | null, + message: string, + expected: string | null, + actual: string | null, + location: PluginLocation | null, +}; + +/** Incremental test tree data. */ +export type TestOutput = { + id: string, + text: string, +}; + +/** Incremental test tree data. */ +export type TestPassed = { + id: string, + durationMs: number | null, +}; + +/** Incremental test tree data. */ +export type TestSkipped = { + id: string, + reason: string | null, +}; + +/** Incremental test tree data. */ +export type TestStart = { + id: string, + parent: string | null, + name: string, + location: PluginLocation | null, +}; + /** Editor › Color Scheme override of one text attribute. */ export type TextAttributesOverride = TextAttributesOverride_Serialize | TextAttributesOverride_Deserialize; @@ -8475,6 +8896,23 @@ export type WebReferenceKind = /** `var(--name)`: the custom property's declarations. */ "cssVariable"; +/** A request that the invoking window acknowledges through plugin_answer_command. */ +export type WindowRequest = WindowRequest_Serialize | WindowRequest_Deserialize; + +/** A request that the invoking window acknowledges through plugin_answer_command. */ +export type WindowRequest_Deserialize = +/** Connect and introspect using the normal Database action in this window. */ +({ kind: "databaseConnect"; id: string }) & { args?: never; config?: never; cwd?: never; debug?: never; env?: never; key?: never; operation?: never; program?: never; tab?: never; title?: never } | +/** Open and acknowledge validation by the shared content editor. */ +({ kind: "contentTab"; tab: ContentTab }) & { args?: never; config?: never; cwd?: never; debug?: never; env?: never; id?: never; key?: never; operation?: never; program?: never; title?: never } | ({ kind: "run"; operation: string; config: RunConfig_Deserialize; debug: boolean }) & { args?: never; cwd?: never; env?: never; id?: never; key?: never; program?: never; tab?: never; title?: never } | ({ kind: "terminal"; operation: string; key: string; title: string; program: IpcPath; args: string[]; env: { [key in string]: string }; cwd: IpcPath }) & { config?: never; debug?: never; id?: never; tab?: never }; + +/** A request that the invoking window acknowledges through plugin_answer_command. */ +export type WindowRequest_Serialize = +/** Connect and introspect using the normal Database action in this window. */ +({ kind: "databaseConnect"; id: string }) & { args?: never; config?: never; cwd?: never; debug?: never; env?: never; key?: never; operation?: never; program?: never; tab?: never; title?: never } | +/** Open and acknowledge validation by the shared content editor. */ +({ kind: "contentTab"; tab: ContentTab }) & { args?: never; config?: never; cwd?: never; debug?: never; env?: never; id?: never; key?: never; operation?: never; program?: never; title?: never } | ({ kind: "run"; operation: string; config: RunConfig_Serialize; debug: boolean }) & { args?: never; cwd?: never; env?: never; id?: never; key?: never; program?: never; tab?: never; title?: never } | ({ kind: "terminal"; operation: string; key: string; title: string; program: IpcPath; args: string[]; env: { [key in string]: string }; cwd: IpcPath }) & { config?: never; debug?: never; id?: never; tab?: never }; + export type WindowTab = { label: string, name: string, @@ -8483,7 +8921,7 @@ export type WindowTab = { }; /** A workbench event delivered to plugins. */ -export type WorkbenchEvent = { kind: "fileOpened"; value: string } | { kind: "fileSaved"; value: string } | { kind: "filesChanged"; value: FilesChanged } | { kind: "editorFocused"; value: string | null } | { kind: "actionPerformed"; value: ActionPerformed } | { kind: "vcsChanged"; value: string } | { kind: "scheduled" }; +export type WorkbenchEvent = { kind: "runExited"; value: RunExited } | { kind: "terminalExited"; value: TerminalExited } | { kind: "contentTabAction"; value: ContentTabAction } | { kind: "fileClosed"; value: string } | { kind: "settingsChanged"; value: string[] } | { kind: "inputModeChanged"; value: InputMode } | { kind: "fileOpened"; value: string } | { kind: "fileSaved"; value: string } | { kind: "filesChanged"; value: FilesChanged } | { kind: "editorFocused"; value: string | null } | { kind: "actionPerformed"; value: ActionPerformed } | { kind: "vcsChanged"; value: string } | { kind: "scheduled" }; export type WorkingTreeChange = { path: string, diff --git a/src/editor/api.ts b/src/editor/api.ts index 5206a786b..6be4d04eb 100644 --- a/src/editor/api.ts +++ b/src/editor/api.ts @@ -456,6 +456,7 @@ export function _provideNavigationHistory(provider: () => NavigationHistorySnaps // --- editor events ------------------------------------------------------------ export type EditorEvent = + | { type: "inputModeChanged"; path: string; mode: string; sub: string | null } | { type: "docOpened"; doc: number; path: string; language: string } | { type: "docClosed"; doc: number; path: string } /** After the backend wrote the file. */ @@ -465,6 +466,11 @@ export type EditorEvent = /** Caret moved in the active text editor (throttled to animation frames). 0-based. */ | { type: "caretMoved"; doc: number; path: string; line: number; col: number; offset: number }; +/** Reports the focused editor's input mode from an editor contribution. */ +export function reportInputMode(path: string, mode: string, sub: string | null) { + _emitEditorEvent({ type: "inputModeChanged", path, mode, sub }); +} + type EditorEventListener = (e: EditorEvent) => void; const editorEventListeners = new Set<EditorEventListener>(); diff --git a/src/editor/core/view/EditorArea.tsx b/src/editor/core/view/EditorArea.tsx index 13e22533d..cbf6f7354 100644 --- a/src/editor/core/view/EditorArea.tsx +++ b/src/editor/core/view/EditorArea.tsx @@ -1,3 +1,4 @@ +import { ContributedToolbar } from "@/workbench/ContributedToolbar"; import { useFilePresentationIcons } from "@/platform/filePresentation"; /** * The editor area: split layout of panes, each with IDEA-style tabs (preview @@ -474,6 +475,7 @@ function EditorTabActions({ paneId, tab }: { paneId: string; tab: Tab }) { const moreRef = useRef<HTMLButtonElement>(null); return ( <div className={styles.tabActions}> + <ContributedToolbar slot="editor.title" /> {LayoutActions && <LayoutActions path={tab.input.path!} tabId={tab.id} />} <IconButton icon="split" diff --git a/src/editor/lsp/index.ts b/src/editor/lsp/index.ts index 6f1e8c170..f3c634471 100644 --- a/src/editor/lsp/index.ts +++ b/src/editor/lsp/index.ts @@ -1,3 +1,4 @@ +import { serverCodeLenses } from "./serverCodeLens"; /** * LSP-backed editor features: completion, diagnostics, hover / quick docs, * parameter info, navigation, document highlights, intentions, rename, @@ -46,6 +47,7 @@ import { HierarchyDialog } from "@/editor/refactor"; export function lspExtensions(ctx: EditorDocContext): Extension[] { return [ lspDoc.of(ctx), + serverCodeLenses(ctx), lspTheme, hintExtension(), diagnosticsExtension(), diff --git a/src/editor/lsp/pluginVisuals.ts b/src/editor/lsp/pluginVisuals.ts new file mode 100644 index 000000000..1771ac7c7 --- /dev/null +++ b/src/editor/lsp/pluginVisuals.ts @@ -0,0 +1,16 @@ +/** One versioned request shared by plugin highlighting, gutter actions and color previews. */ +import type { Text } from "@codemirror/state"; +import type { EditorView } from "@codemirror/view"; +import { whenSynced, type EditorDocContext } from "@/editor/api"; +import { call, commands, type PluginLanguageVisuals } from "@/platform/ipc"; +import { navigationEpoch } from "@/editor/navigationProviders"; +const pending = new WeakMap<Text, { epoch: number; doc: number; value: Promise<PluginLanguageVisuals> }>(); +export function pluginVisuals(view: EditorView, ctx: EditorDocContext): Promise<PluginLanguageVisuals> { + const text = view.state.doc; + const epoch = navigationEpoch(); + const cached = pending.get(text); + if (cached?.epoch === epoch && cached.doc === ctx.doc) return cached.value; + const value = whenSynced(view).then(() => call(commands.pluginLanguageVisuals(ctx.doc))); + pending.set(text, { epoch, doc: ctx.doc, value }); + return value; +} diff --git a/src/editor/lsp/serverCodeLens.test.ts b/src/editor/lsp/serverCodeLens.test.ts new file mode 100644 index 000000000..7d554e6b5 --- /dev/null +++ b/src/editor/lsp/serverCodeLens.test.ts @@ -0,0 +1,40 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { codeLenses } from "@/editor/api"; +const ipc = vi.hoisted(() => ({ + lspCodeLens: vi.fn(), + lspExecuteCommand: vi.fn(async () => ({ status: "ok", data: null })), +})); +vi.mock("@/platform/ipc", async (original) => ({ + ...(await original<typeof import("@/platform/ipc")>()), + commands: { ...(await original<typeof import("@/platform/ipc")>()).commands, ...ipc }, +})); +const { serverCodeLenses } = await import("./serverCodeLens"); +const views: EditorView[] = []; +afterEach(() => { + views.splice(0).forEach((v) => v.destroy()); + vi.useRealTimers(); + vi.clearAllMocks(); +}); +it("renders LSP and plugin lens commands in the shared facet and invokes their commands", async () => { + vi.useFakeTimers(); + const command = { title: "Run fixture", command: "openidea.plugin-language:p:run", arguments: ['"lens"'] }; + ipc.lspCodeLens.mockResolvedValue({ + status: "ok", + data: [{ range: { start: { line: 0, col: 0 }, end: { line: 0, col: 1 } }, command }], + }); + const view = new EditorView({ + state: EditorState.create({ + doc: "x", + extensions: serverCodeLenses({ doc: 1, path: "/p/a", language: "text", uiMode: null, readOnly: false }), + }), + parent: document.body, + }); + views.push(view); + await vi.advanceTimersByTimeAsync(400); + const lenses = view.state.facet(codeLenses); + expect(lenses.map((l) => l.title)).toEqual(["Run fixture"]); + lenses[0]!.run!(view, new MouseEvent("click")); + expect(ipc.lspExecuteCommand).toHaveBeenCalledWith(1, command); +}); diff --git a/src/editor/lsp/serverCodeLens.ts b/src/editor/lsp/serverCodeLens.ts new file mode 100644 index 000000000..c7ed3d7c7 --- /dev/null +++ b/src/editor/lsp/serverCodeLens.ts @@ -0,0 +1,81 @@ +/** Language-server and plugin lenses in the shared editor code-vision renderer. */ +import { StateEffect, StateField, type Extension } from "@codemirror/state"; +import { ViewPlugin, type ViewUpdate } from "@codemirror/view"; +import { codeLenses, whenSynced, type CodeLens, type EditorDocContext } from "@/editor/api"; +import { call, commands } from "@/platform/ipc"; +import { onNavigationProvidersChanged } from "@/editor/navigationProviders"; +import { posToOffset } from "./positions"; +import { onLspRefresh, onServerSettled } from "./availability"; +const set = StateEffect.define<CodeLens[]>(); +const field = StateField.define<CodeLens[]>({ + create: () => [], + update(value, tr) { + for (const e of tr.effects) if (e.is(set)) return e.value; + return tr.docChanged ? [] : value; + }, + provide: (f) => codeLenses.from(f), +}); +export function serverCodeLenses(ctx: EditorDocContext): Extension { + if (ctx.doc < 0) return []; + return [ + field, + ViewPlugin.define( + (view) => + new (class { + timer: ReturnType<typeof setTimeout> | undefined; + generation = 0; + off: () => void; + constructor() { + const offs = [ + onNavigationProvidersChanged(() => this.schedule()), + onLspRefresh("codeLens", () => this.schedule()), + onServerSettled(() => this.schedule()), + ]; + this.off = () => offs.forEach((f) => f()); + this.schedule(); + } + schedule() { + this.generation++; + clearTimeout(this.timer); + this.timer = setTimeout(() => void this.run(), 400); + } + update(u: ViewUpdate) { + if (u.docChanged) this.schedule(); + } + async run() { + const gen = this.generation; + const text = view.state.doc; + try { + await whenSynced(view); + const result = await call(commands.lspCodeLens(ctx.doc)); + if (gen !== this.generation || text !== view.state.doc || !view.dom.isConnected) return; + view.dispatch({ + effects: set.of( + result.flatMap((l) => + l.command + ? [ + { + pos: posToOffset(text, l.range.start), + title: l.command.title, + run: () => { + void call(commands.lspExecuteCommand(ctx.doc, l.command!)).catch(console.warn); + }, + }, + ] + : [], + ), + ), + }); + } catch { + if (gen === this.generation && view.dom.isConnected) view.dispatch({ effects: set.of([]) }); + } + } + destroy() { + this.generation++; + clearTimeout(this.timer); + this.off(); + } + })(), + ), + ]; +} diff --git a/src/features/code-insight/ProblemsView.tsx b/src/features/code-insight/ProblemsView.tsx index abcde0838..924368e14 100644 --- a/src/features/code-insight/ProblemsView.tsx +++ b/src/features/code-insight/ProblemsView.tsx @@ -1,3 +1,4 @@ +import { menuGroupItems } from "@/workbench/contributions"; /** Problems tool window: "File" (current editor) and "Project Errors" tabs. */ import { useCallback, useMemo, useRef, useState, type KeyboardEvent } from "react"; import { create } from "zustand"; @@ -387,6 +388,7 @@ export function ProblemsView() { run: () => copy(n), }, ...inspectionMenu(n.kind === "diag" ? diagInspection(map, n.path, n.d) : null), + ...menuGroupItems("problems", { paths: [t.path], line: t.d.range.start.line, column: t.d.range.start.col }), ]); }} activateExpandable={false} diff --git a/src/features/color-preview/index.ts b/src/features/color-preview/index.ts index 834401bd3..552824967 100644 --- a/src/features/color-preview/index.ts +++ b/src/features/color-preview/index.ts @@ -1,7 +1,12 @@ /** Color swatches in the editor gutter for color literals (Settings › Editor › General › Gutter Icons). */ import { registerEditorContribution } from "@/editor/api"; +import { pluginColorPreview } from "./pluginColors"; import { colorPreviewGutter } from "./gutter"; export function activate(): () => void { - return registerEditorContribution({ id: "colorPreview.gutter", order: 59, extensions: colorPreviewGutter }); + return registerEditorContribution({ + id: "colorPreview.gutter", + order: 59, + extensions: (ctx) => [colorPreviewGutter(ctx), pluginColorPreview(ctx)], + }); } diff --git a/src/features/color-preview/pluginColors.test.ts b/src/features/color-preview/pluginColors.test.ts new file mode 100644 index 000000000..ce093d352 --- /dev/null +++ b/src/features/color-preview/pluginColors.test.ts @@ -0,0 +1,55 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +const ipc = vi.hoisted(() => ({ + pluginLanguageVisuals: vi.fn(), + pluginColorPresentations: vi.fn(async () => ({ status: "ok", data: ["red"] })), +})); +vi.mock("@/platform/ipc", async (original) => ({ + ...(await original<typeof import("@/platform/ipc")>()), + commands: ipc, +})); +const { pluginColorPreview } = await import("./pluginColors"); +const views: EditorView[] = []; +afterEach(() => { + views.splice(0).forEach((v) => v.destroy()); + vi.useRealTimers(); + vi.clearAllMocks(); + document.querySelectorAll('input[type="color"]').forEach((n) => n.remove()); +}); +it("uses plugin presentations to replace precisely the reported range", async () => { + vi.useFakeTimers(); + Object.defineProperty(HTMLInputElement.prototype, "showPicker", { value: vi.fn(), configurable: true }); + ipc.pluginLanguageVisuals.mockResolvedValue({ + status: "ok", + data: { + version: 1, + injections: [], + gutterActions: [], + colors: [ + ["p", { range: { start: { line: 0, col: 1 }, end: { line: 0, col: 2 } }, red: 1, green: 0, blue: 0, alpha: 1 }], + ], + }, + }); + const view = new EditorView({ + state: EditorState.create({ + doc: "xyz", + extensions: pluginColorPreview({ doc: 1, path: "/p/a", language: "text", uiMode: null, readOnly: false }), + }), + parent: document.body, + }); + views.push(view); + await vi.advanceTimersByTimeAsync(300); + const swatch = view.dom.querySelector<HTMLButtonElement>("button")!; + swatch.click(); + const picker = document.querySelector<HTMLInputElement>('input[type="color"]')!; + picker.value = "#00ff00"; + picker.dispatchEvent(new Event("change")); + await vi.advanceTimersByTimeAsync(0); + expect(view.state.doc.toString()).toBe("xredz"); + expect(ipc.pluginColorPresentations).toHaveBeenCalledWith( + 1, + "p", + expect.objectContaining({ red: 0, green: 1, blue: 0, alpha: 1 }), + ); +}); diff --git a/src/features/color-preview/pluginColors.ts b/src/features/color-preview/pluginColors.ts new file mode 100644 index 000000000..afc1f4dcc --- /dev/null +++ b/src/features/color-preview/pluginColors.ts @@ -0,0 +1,153 @@ +/** Plugin colors use the same native picker and provider-supplied replacement strings. */ +import { RangeSet, StateEffect, StateField, type Extension } from "@codemirror/state"; +import { GutterMarker, ViewPlugin, gutter, type EditorView, type ViewUpdate } from "@codemirror/view"; +import { type EditorDocContext } from "@/editor/api"; +import { rangeToOffsets } from "@/editor/lsp/positions"; +import { pluginVisuals } from "@/editor/lsp/pluginVisuals"; +import { onNavigationProvidersChanged } from "@/editor/navigationProviders"; +import { call, commands, type ColorInfo } from "@/platform/ipc"; +import { showContextMenu } from "@/ui"; +import styles from "./ColorPreview.module.css"; +const setColors = StateEffect.define<[string, ColorInfo][]>(); +const colors = StateField.define<[string, ColorInfo][]>({ + create: () => [], + update(v, tr) { + for (const e of tr.effects) if (e.is(setColors)) return e.value; + return tr.docChanged ? [] : v; + }, +}); +class Swatch extends GutterMarker { + constructor( + readonly plugin: string, + readonly color: ColorInfo, + readonly view: EditorView, + readonly ctx: EditorDocContext, + ) { + super(); + } + override eq(other: GutterMarker) { + return ( + other instanceof Swatch && + this.plugin === other.plugin && + JSON.stringify(this.color) === JSON.stringify(other.color) + ); + } + override toDOM() { + const el = document.createElement("button"); + el.type = "button"; + el.className = styles.swatch!; + el.title = `rgba(${this.color.red}, ${this.color.green}, ${this.color.blue}, ${this.color.alpha})`; + el.setAttribute("aria-label", el.title); + const fill = document.createElement("span"); + fill.style.background = `rgba(${(this.color.red ?? 0) * 255}, ${(this.color.green ?? 0) * 255}, ${(this.color.blue ?? 0) * 255}, ${this.color.alpha})`; + el.append(fill); + el.onclick = () => { + if (this.view.state.readOnly) return; + const rect = el.getBoundingClientRect(); + pick(this.view, this.ctx, this.plugin, this.color, rect.left, rect.bottom + 2); + }; + return el; + } +} +function pick(view: EditorView, ctx: EditorDocContext, plugin: string, color: ColorInfo, x: number, y: number) { + const text = view.state.doc; + const { from, to } = rangeToOffsets(text, color.range); + const input = document.createElement("input"); + input.type = "color"; + input.className = styles.picker!; + input.style.left = `${x}px`; + input.style.top = `${y}px`; + input.value = + "#" + + [color.red, color.green, color.blue] + .map((c) => + Math.round((c ?? 0) * 255) + .toString(16) + .padStart(2, "0"), + ) + .join(""); + document.body.append(input); + input.onchange = () => { + input.remove(); + const hex = input.value; + const chosen = { + ...color, + red: parseInt(hex.slice(1, 3), 16) / 255, + green: parseInt(hex.slice(3, 5), 16) / 255, + blue: parseInt(hex.slice(5, 7), 16) / 255, + }; + void call(commands.pluginColorPresentations(ctx.doc, plugin, chosen)) + .then((list) => { + const apply = (insert: string) => { + if (view.state.doc === text && !view.state.readOnly) + view.dispatch({ changes: { from, to, insert }, userEvent: "input.color" }); + }; + if (view.state.doc !== text) return; + if (list.length === 1) apply(list[0]!); + else if (list.length) + showContextMenu( + { x, y }, + list.map((label) => ({ label, verbatim: true, run: () => apply(label) })), + ); + }) + .catch(console.warn); + }; + input.oncancel = () => input.remove(); + try { + input.showPicker(); + } catch { + input.click(); + } +} +export function pluginColorPreview(ctx: EditorDocContext): Extension { + if (ctx.doc < 0) return []; + return [ + colors, + gutter({ + class: `${styles.gutter} cm-plugin-color-preview`, + markers: (view) => + RangeSet.of( + view.state + .field(colors) + .map(([p, c]) => new Swatch(p, c, view, ctx).range(view.state.doc.line(c.range.start.line + 1).from)), + true, + ), + }), + ViewPlugin.define( + (view) => + new (class { + timer: ReturnType<typeof setTimeout> | undefined; + generation = 0; + off: () => void; + constructor() { + this.off = onNavigationProvidersChanged(() => this.schedule()); + this.schedule(); + } + schedule() { + this.generation++; + clearTimeout(this.timer); + this.timer = setTimeout(() => void this.run(), 300); + } + update(u: ViewUpdate) { + if (u.docChanged) this.schedule(); + } + async run() { + const gen = this.generation; + const text = view.state.doc; + try { + const r = await pluginVisuals(view, ctx); + if (gen === this.generation && view.state.doc === text && view.dom.isConnected) + view.dispatch({ effects: setColors.of(r.colors) }); + } catch { + if (gen === this.generation && view.dom.isConnected) view.dispatch({ effects: setColors.of([]) }); + } + } + destroy() { + this.generation++; + clearTimeout(this.timer); + this.off(); + } + })(), + ), + ]; +} diff --git a/src/features/database/DatabaseView.tsx b/src/features/database/DatabaseView.tsx index afd2627d8..3af50928e 100644 --- a/src/features/database/DatabaseView.tsx +++ b/src/features/database/DatabaseView.tsx @@ -292,7 +292,7 @@ export function DatabaseView() { const label = (n: Node): string => { switch (n.kind) { case "source": - return n.source.name ?? ""; + return n.source.pluginOwner ? `${n.source.name} (${n.source.pluginOwner})` : (n.source.name ?? ""); case "database": return n.name; case "schema": diff --git a/src/features/database/SourceDialog.tsx b/src/features/database/SourceDialog.tsx index a30749ca2..2952388d2 100644 --- a/src/features/database/SourceDialog.tsx +++ b/src/features/database/SourceDialog.tsx @@ -1110,7 +1110,12 @@ export function SourceDialog({ /> {/* IDEA's Make Global: stored with the IDE and listed in every project. */} <span title={tx("Available in all projects")}> - <Checkbox checked={!!s.global} onChange={(global) => set({ global })} label="Global" /> + <Checkbox + checked={!!s.global} + disabled={!!s.pluginOwner} + onChange={(global) => set({ global })} + label="Global" + /> </span> </div> <span /> diff --git a/src/features/database/index.ts b/src/features/database/index.ts index 842478fba..c6a0653d7 100644 --- a/src/features/database/index.ts +++ b/src/features/database/index.ts @@ -6,6 +6,8 @@ */ import { lazyComponent, lazyWhen } from "@/ui"; import { activeEditor, lazyEditorExtension, registerEditorContribution } from "@/editor/api"; +import { tx } from "@/platform/i18n"; +import { onIdeEvent } from "@/platform/ipc"; import { registerActions } from "@/platform/actions"; import { activeTableEditor, installTableEditorFocus } from "./tableEditorFocus"; import { useProject } from "@/platform/project"; @@ -27,6 +29,11 @@ import { consoleSource, consoleTints, loadSources, + connect, + loadSchemas, + loadDatabases, + listsDatabases, + sourceById, openConsole, openHistory, openSourceDialog, @@ -38,6 +45,9 @@ const useDialogOpen = () => useDatabase((s) => !!s.dialog); export function activate(): () => void { const disposers = [ + onIdeEvent("dbSourcesChanged", () => { + void loadSources(); + }), registerToolWindow({ id: DB_TOOL_WINDOW, title: "Database", @@ -109,6 +119,23 @@ export function activate(): () => void { installTableEditorFocus(), registerDatabaseTreeActions(), registerActions([ + { + id: "db.pluginConnect", + title: "Connect Plugin Data Source", + category: "Tools", + when: "projectOpen", + run: async (e) => { + if (!useProject.getState().project?.trusted) + throw new Error(tx("Trust this project to connect plugin data sources")); + const id = e.arg; + if (typeof id !== "string") throw new Error(tx("Invalid data source id")); + await loadSources(); + toolWindowService().show(DB_TOOL_WINDOW, true); + if (!(await connect(id))) throw new Error(tx("Database connection failed")); + if (listsDatabases(sourceById(id))) await loadDatabases(id); + else await loadSchemas(id); + }, + }, { id: "db.showToolWindow", title: "Database", diff --git a/src/features/database/store.ts b/src/features/database/store.ts index e651f116a..4adef497f 100644 --- a/src/features/database/store.ts +++ b/src/features/database/store.ts @@ -2,6 +2,7 @@ * Database tools state: data sources, connection status, the introspection * cache of the tree, query consoles (file → data source) and their results. */ +import { useProject } from "@/platform/project"; import { create } from "zustand"; import { IpcError, @@ -243,11 +244,20 @@ export function connectedTargets(id: string): string[] { return [...useDatabase.getState().connected].filter((t) => splitTarget(t).id === id); } +let sourcesRequest = 0; + export async function loadSources(): Promise<void> { + const request = ++sourcesRequest; + const root = useProject.getState().project?.root; try { const [sources, connected] = await Promise.all([call(commands.dbSources()), call(commands.dbConnected())]); + if (request !== sourcesRequest || useProject.getState().project?.root !== root) return; + for (const old of useDatabase.getState().sources) { + if (JSON.stringify(old) !== JSON.stringify(sources.find((s) => s.id === old.id))) forget(old.id ?? ""); + } useDatabase.setState({ sources, connected: new Set(connected), loaded: true }); } catch (e) { + if (request !== sourcesRequest || useProject.getState().project?.root !== root) return; useDatabase.setState({ loaded: true }); notifications().notify({ level: "error", title: "Cannot read data sources", body: message(e), key: "db-sources" }); } @@ -299,7 +309,10 @@ export async function duplicateSource(id: string): Promise<void> { let n = 2; while (names.has(`${src.name} [${n}]`)) n++; try { - await saveSource({ ...src, id: newSourceId(), name: `${src.name} [${n}]` }, undefined); + await saveSource( + { ...src, id: newSourceId(), name: `${src.name} [${n}]`, pluginOwner: null, pluginSourceId: null }, + undefined, + ); } catch (e) { notifications().notify({ level: "error", title: "Cannot duplicate the data source", body: message(e) }); } diff --git a/src/features/debugger/actions.ts b/src/features/debugger/actions.ts index a87e18827..75acdf27a 100644 --- a/src/features/debugger/actions.ts +++ b/src/features/debugger/actions.ts @@ -323,6 +323,7 @@ async function startConfig(arg: unknown) { await startJavaConfig(config, root, origin ?? config, show); return; } + if (config.kind === "plugin") config = await call(commands.pluginPrepareDebugRun(config)); const dotnet = config.kind === "dotnet" && config.debugAdapter === "netcoredbg"; const prepared = dotnet ? await call(commands.runDotnetResolve(config, true)) : config; // php-debug starts the PHP chosen in Settings unless the command names one. @@ -387,6 +388,45 @@ export function registerDebugActions(): () => void { }); return registerActions([ + { + id: "debug.pluginStart", + title: "Debug Plugin Configuration", + category: "Debug", + hidden: true, + when: "projectOpen", + run: async ({ arg }) => { + const a = arg as { config: RunConfig; onExit: (code: number | null, stopped: boolean) => void }; + const config = await call(commands.pluginPrepareDebugRun(a.config)); + const root = useProject.getState().project?.root; + if (!root) throw new Error("No project"); + const launch = launchConfigFor(config, root); + if (!launch) throw new Error("This configuration has no debugger"); + let id: number | null = null; + let stopped = false; + const observe = () => { + if (id === null) return; + const session = useDebug.getState().sessions[id]; + if (session?.status === "stopping") stopped = true; + if (!session || session.status === "terminated") { + off(); + a.onExit(session?.exitCode ?? null, stopped); + } + }; + const off = useDebug.subscribe(observe); + try { + await maybeEditorService()?.saveAll(); + id = await startSession(launch, { runConfig: config }); + if (id === null) { + off(); + throw new Error("Debug session did not start"); + } + observe(); + } catch (error) { + off(); + throw error; + } + }, + }, { id: "debug.startConfig", title: "Debug…", diff --git a/src/features/mcp/index.ts b/src/features/mcp/index.ts index ce7163ef2..a64faeef5 100644 --- a/src/features/mcp/index.ts +++ b/src/features/mcp/index.ts @@ -30,7 +30,7 @@ export function activate(): () => void { let timer: ReturnType<typeof setTimeout> | null = null; const report = () => { timer = null; - if (isRemoteWindow() || !useProject.getState().project) return; + if (!useProject.getState().project) return; const paths = openEditorPaths(); const editor = activeEditor(); const focused = document.hasFocus(); diff --git a/src/features/mcp/reporters.test.ts b/src/features/mcp/reporters.test.ts index 2c1598c0f..4741eb2c7 100644 --- a/src/features/mcp/reporters.test.ts +++ b/src/features/mcp/reporters.test.ts @@ -106,10 +106,11 @@ describe("MCP editor reports", () => { expect(state.reportOpen).toHaveBeenCalledTimes(2); expect(state.reportFocused).toHaveBeenCalledTimes(2); }); - it("does not report remote windows or after disposal", async () => { + it("forwards remote editor reports and stops after disposal", async () => { state.remote = true; await vi.advanceTimersByTimeAsync(300); - expect(state.reportOpen).not.toHaveBeenCalled(); + expect(state.reportOpen).toHaveBeenCalledWith(["/p/a.txt"]); + state.reportOpen.mockClear(); state.remote = false; state.registryChanged(); stop(); diff --git a/src/features/plugins/PluginsPage.module.css b/src/features/plugins/PluginsPage.module.css index 305ad6483..4eb73a89a 100644 --- a/src/features/plugins/PluginsPage.module.css +++ b/src/features/plugins/PluginsPage.module.css @@ -334,6 +334,41 @@ } /* Permission dialog and notices */ +.capabilityItem { + min-width: 0; + padding: 8px 10px; +} +.capabilityItem + .capabilityItem { + border-top: 1px solid var(--border); +} +.capabilityHeader { + display: flex; + align-items: center; + gap: 10px; +} +.capabilityHeader > .mono { + flex: 1; + min-width: 0; + overflow-wrap: anywhere; +} +.capabilityDescription { + margin: 4px 0 0 26px; + color: var(--fg); + overflow-wrap: anywhere; +} +.programPermission { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + min-width: 0; + margin: 6px 0 0 26px; +} +.programPath { + flex-basis: 100%; + min-width: 0; + overflow-wrap: anywhere; +} .consent { display: flex; flex-direction: column; diff --git a/src/features/plugins/PluginsPage.tsx b/src/features/plugins/PluginsPage.tsx index ec1f9546c..4ad771cbc 100644 --- a/src/features/plugins/PluginsPage.tsx +++ b/src/features/plugins/PluginsPage.tsx @@ -31,7 +31,7 @@ import { type MenuItem, T, } from "@/ui"; -import { RISK_LABEL, sortByRisk } from "./consent"; +import { RISK_LABEL, sortByRisk, capabilityDescription } from "./consent"; import { pluginIconName } from "./pluginIcon"; import { localizeContributions, usePluginCatalogs, usePluginT } from "./pluginText"; import { itemsOf, uiPoint } from "./projection"; @@ -59,6 +59,8 @@ import { openExternalUrl } from "@/platform/openUrl"; import { importIntellijTheme, showInstalledIntellijThemes } from "./intellijImport"; import { promptInput } from "@/workbench/inputDialogs"; import styles from "./PluginsPage.module.css"; +import { pluginsBackend } from "./backend"; +import { confirm } from "@/workbench/messages"; import { tx } from "@/platform/i18n"; const STATUS: Record<PluginStatus, { label: string; tone: "success" | "accent" | "neutral" | "danger" | "warning" }> = { @@ -293,17 +295,86 @@ function PointSection({ plugin, point }: { plugin: PluginInfo; point: string }) ); } -function CapabilityRow({ cap, children }: { cap: CapabilityInfo; children?: ReactNode }) { +function CapabilityRow({ cap, children, program }: { cap: CapabilityInfo; children?: ReactNode; program?: ReactNode }) { return ( - <li className={styles.item}> - {children} - <span className={styles.itemMain}>{tx(cap.description)}</span> - <span className={styles.mono}>{cap.id}</span> - <Badge tone={RISK_LABEL[cap.risk].tone}>{RISK_LABEL[cap.risk].label}</Badge> + <li className={styles.capabilityItem}> + <div className={styles.capabilityHeader}> + {children} + <span className={styles.mono}>{cap.id}</span> + <Badge tone={RISK_LABEL[cap.risk].tone}>{RISK_LABEL[cap.risk].label}</Badge> + </div> + <p className={styles.capabilityDescription}>{capabilityDescription(cap)}</p> + {program} </li> ); } +function ProgramPermission({ plugin, cap, disabled }: { plugin: PluginInfo; cap: CapabilityInfo; disabled: boolean }) { + const project = useProject((s) => s.project); + const [scope, setScope] = useState<"global" | "project">("global"); + const [path, setPath] = useState(""); + const [error, setError] = useState(""); + const [working, setWorking] = useState(false); + useEffect(() => { + let live = true; + void pluginsBackend() + .program(plugin.id, cap.id.slice(8), scope) + .then((p) => { + if (live) setPath(p); + }) + .catch(() => { + if (live) setPath(""); + }); + return () => { + live = false; + }; + }, [plugin, cap.id, scope, working]); + const choose = async (reset: boolean) => { + setError(""); + setWorking(true); + try { + const selected = reset ? null : await pluginsBackend().pickProgram(); + if (!reset && !selected) return; + const message = reset + ? tx("Revalidate the default executable? If it is unavailable, this permission will be revoked.") + : tx`Allow ${plugin.name} to run ${selected}? This is equivalent to running arbitrary code on this machine.`; + if (!(await confirm({ title: tx("Approve plugin executable"), message, ok: tx("Allow"), icon: "warning" }))) + return; + await pluginsBackend().chooseProgram(plugin.id, cap.id, scope, selected); + await refreshPlugins(); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setWorking(false); + } + }; + return ( + <div className={styles.programPermission}> + {path && <span className={clsx(styles.mono, styles.programPath)}>{path}</span>} + <Select + aria-label={tx("Executable scope")} + value={scope} + onChange={(value) => setScope(value as "global" | "project")} + options={[ + { value: "global", label: tx("Global") }, + ...(project ? [{ value: "project", label: tx("Project") }] : []), + ]} + /> + <Button disabled={disabled || working} onClick={() => void choose(false)}> + {tx("Choose executable…")} + </Button> + <Button disabled={disabled || working} onClick={() => void choose(true)}> + {tx("Reset executable")} + </Button> + {error && ( + <span role="alert" className={styles.error}> + {error} + </span> + )} + </div> + ); +} + function CapabilityList({ plugin, busy }: { plugin: PluginInfo; busy: boolean }) { if (plugin.capabilities.length === 0) { return ( @@ -316,7 +387,13 @@ function CapabilityList({ plugin, busy }: { plugin: PluginInfo; busy: boolean }) return ( <ul className={styles.items} aria-label={tx("Permissions")}> {sortByRisk(plugin.capabilities).map((c) => ( - <CapabilityRow key={c.id} cap={c}> + <CapabilityRow + key={c.id} + cap={c} + program={ + c.id.startsWith("process:") && <ProgramPermission plugin={plugin} cap={c} disabled={busy || !confirmed} /> + } + > <Checkbox checked={c.granted} disabled={busy || !confirmed} diff --git a/src/features/plugins/__tests__/decorations.test.ts b/src/features/plugins/__tests__/decorations.test.ts new file mode 100644 index 000000000..1f8460f03 --- /dev/null +++ b/src/features/plugins/__tests__/decorations.test.ts @@ -0,0 +1,93 @@ +import { afterEach, beforeEach, expect, it } from "vitest"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import type { DecorationSet } from "@/bindings"; +import { _addEditor, _removeEditor, type EditorDocContext, type TextEditorHandle } from "@/editor/api"; +import { useProject } from "@/platform/project"; +import { getFileDecoration } from "@/workbench/contributions"; +import { decorationExtension, setPluginDecorations } from "../decorations"; +import { capabilityDescription } from "../consent"; +import { handleRuntimeEvent } from "../runtimeUi"; + +const ctx: EditorDocContext = { doc: 1, path: "/p/a.txt", language: "text", uiMode: null, readOnly: false }; +let editor: TextEditorHandle; +const range = { start: { line: 0, col: 2 }, end: { line: 0, col: 4 } }; +const set = (root = "/p"): DecorationSet => ({ + root, + path: "/p/a.txt", + setId: "test", + tree: false, + items: [{ range, style: "highlight", hover: "hover", after: "<plain>" }], + file: null, +}); +beforeEach(() => { + useProject.setState({ project: { root: "/p", trusted: true } as never }); + editor = { + ctx, + view: new EditorView({ + state: EditorState.create({ + doc: "😀text", + extensions: [EditorState.allowMultipleSelections.of(true), decorationExtension(ctx)], + }), + parent: document.body, + }), + }; + _addEditor(editor); +}); +afterEach(() => { + setPluginDecorations("test", []); + _removeEditor(editor); + editor.view.destroy(); + useProject.setState({ project: null }); +}); + +it("renders plain annotations, maps them through edits, and withdraws them", () => { + setPluginDecorations("test", [set()]); + expect(editor.view.dom.textContent).toContain("<plain>"); + expect(editor.view.dom.querySelector('[title="hover"]')?.textContent).toBe("te"); + expect(editor.view.dom.querySelector("plain")).toBeNull(); + editor.view.dispatch({ changes: { from: 0, insert: "x" } }); + expect(editor.view.dom.querySelector('[title="hover"]')?.textContent).toBe("te"); + setPluginDecorations("test", []); + expect(editor.view.dom.textContent).not.toContain("<plain>"); +}); + +it("filters project roots and unknown styles and replaces tree badges", () => { + setPluginDecorations("test", [set("/other")]); + expect(editor.view.dom.textContent).not.toContain("<plain>"); + const unknown = set(); + unknown.items[0]!.style = "unknown"; + setPluginDecorations("test", [unknown]); + expect(editor.view.dom.textContent).not.toContain("<plain>"); + const tree = { ...set(), tree: true, items: [], file: { style: "modified", badge: "M", tooltip: "changed" } }; + setPluginDecorations("test", [tree]); + expect(getFileDecoration("/p/a.txt")?.badge).toBe("M"); + setPluginDecorations("test", []); + expect(getFileDecoration("/p/a.txt")?.badge).toBeUndefined(); +}); + +it("selects UTF-16 ranges and reveals without changing text", () => { + handleRuntimeEvent({ type: "editorRanges", path: ctx.path, ranges: [range], reveal: false }); + expect(editor.view.state.selection.main.from).toBe(2); + expect(editor.view.state.selection.main.to).toBe(4); + handleRuntimeEvent({ type: "editorRanges", path: ctx.path, ranges: [range], reveal: true }); + expect(editor.view.state.doc.toString()).toBe("😀text"); + handleRuntimeEvent({ + type: "editorRanges", + path: "/other/a.txt", + ranges: [{ start: { line: 0, col: 0 }, end: { line: 0, col: 1 } }], + reveal: false, + }); + expect(editor.view.state.selection.main.from).toBe(2); +}); + +it("describes ide.commands explicitly in consent", () => { + const description = capabilityDescription({ + id: "ide.commands", + description: "", + optional: false, + granted: false, + risk: "high", + }); + expect(description).toContain("including running programs, committing and pushing"); +}); diff --git a/src/features/plugins/__tests__/logEditor.test.tsx b/src/features/plugins/__tests__/logEditor.test.tsx index da0293c34..d25a5c8da 100644 --- a/src/features/plugins/__tests__/logEditor.test.tsx +++ b/src/features/plugins/__tests__/logEditor.test.tsx @@ -8,6 +8,7 @@ import { useContributions } from "@/workbench/contributions"; import { usePlugins } from "../store"; import { startViewUi } from "../uiPoints"; import { registerLogActions } from "../views/logActions"; +import { setPluginOutput, usePluginOutput } from "../views/outputStore"; import { PluginLogEditor } from "../views/logEditor"; import { boundedLog, logTabData, type PluginLogEditorData } from "../views/logTab"; import { logTabPtyBackend, webPtyBackend } from "../views/ptyBackend"; @@ -71,6 +72,33 @@ function setup(extra: Partial<PluginLogEditorData> = {}) { } describe("plugin streaming log tabs", () => { + it("shows output channel snapshots in the read-only log editor without starting a process", () => { + vi.mocked(webPtyBackend).mockClear(); + vi.mocked(logTabPtyBackend).mockClear(); + usePluginOutput.setState({ channels: new Map() }); + setPluginOutput("probe", "build", "first output"); + const data: PluginLogEditorData = { + pluginId: "probe", + view: "", + key: "build", + title: "Build output", + program: "", + args: [], + toggles: [], + origin: "output", + }; + const rendered = render(<PluginLogEditor input={{ kind: "pluginLog", key: "output", data }} />); + const view = EditorView.findFromDOM(rendered.container.querySelector(".cm-content") as HTMLElement)!; + expect(view.state.doc.toString()).toBe("first output"); + expect(view.state.readOnly).toBe(true); + act(() => setPluginOutput("probe", "build", "next output")); + expect(view.state.doc.toString()).toBe("next output"); + act(() => setPluginOutput("probe", "build", "")); + expect(view.state.doc.length).toBe(0); + expect(webPtyBackend).not.toHaveBeenCalled(); + expect(logTabPtyBackend).not.toHaveBeenCalled(); + }); + it("rejects malformed requests and exact-grant failures instead of rewriting commands", () => { usePlugins.setState({ plugins: [plugin] }); for (const bad of [ diff --git a/src/features/plugins/__tests__/plugins.test.tsx b/src/features/plugins/__tests__/plugins.test.tsx index 3a79818ca..b51f5f60d 100644 --- a/src/features/plugins/__tests__/plugins.test.tsx +++ b/src/features/plugins/__tests__/plugins.test.tsx @@ -502,6 +502,60 @@ describe("bridge events", () => { expect(useTasks.getState().tasks.find((t) => t.id === "plugin:7")?.done).toBe(true); }); + it("keeps a window status override above global updates and falls back on removal", () => { + const global = { text: "global", tooltip: null, command: null }; + const local = { text: "window", tooltip: null, command: null }; + handleRuntimeEvent({ type: "statusItemChanged", pluginId: "scope", id: "state", item: global }); + handleRuntimeEvent({ type: "windowStatusItemChanged", pluginId: "scope", id: "state", item: local }); + handleRuntimeEvent({ + type: "statusItemChanged", + pluginId: "scope", + id: "state", + item: { ...global, text: "updated" }, + }); + expect(usePluginStatusItems.getState().items.get("scope/state")?.item.text).toBe("window"); + handleRuntimeEvent({ type: "windowStatusItemChanged", pluginId: "scope", id: "state", item: null }); + expect(usePluginStatusItems.getState().items.get("scope/state")?.item.text).toBe("updated"); + handleRuntimeEvent({ type: "statusItemChanged", pluginId: "scope", id: "state", item: null }); + }); + + it("executes host command requests through actions and returns plugin command results", async () => { + const answerCommand = vi.fn().mockResolvedValue(undefined); + const runCommand = vi.fn().mockResolvedValue("plugin result"); + setPluginsBackend({ ...backend, answerCommand, runCommand }); + const run = vi.fn(); + const dispose = registerAction({ id: "test.host-command", title: "Test", run }); + try { + handleRuntimeEvent({ + type: "executeCommand", + requestId: "ide", + id: "test.host-command", + arg: "argument", + chain: ["caller"], + }); + await waitFor(() => expect(answerCommand).toHaveBeenCalledWith("ide", { kind: "value", value: null })); + expect(run).toHaveBeenCalledWith(expect.objectContaining({ arg: "argument" })); + usePlugins.setState({ + ui: [{ pluginId: "other", point: "commands", key: "test", decl: { id: "test", title: "Test" } }], + }); + handleRuntimeEvent({ + type: "executeCommand", + requestId: "plugin", + id: "plugin.other.test", + arg: "argument", + chain: ["caller"], + }); + await waitFor(() => + expect(answerCommand).toHaveBeenCalledWith("plugin", { kind: "value", value: "plugin result" }), + ); + expect(runCommand).toHaveBeenCalledWith("other", "test", "argument", null, [], undefined, ["caller"]); + handleRuntimeEvent({ type: "executeCommand", requestId: "missing", id: "unknown", arg: null, chain: ["caller"] }); + await waitFor(() => expect(answerCommand).toHaveBeenCalledWith("missing", { kind: "notFound" })); + } finally { + dispose(); + } + }); + it("runs notification buttons as commands of the publishing plugin with their arguments", async () => { const runCommand = vi.fn().mockResolvedValue(null); setPluginsBackend({ ...backend, runCommand }); diff --git a/src/features/plugins/__tests__/projectUi.test.tsx b/src/features/plugins/__tests__/projectUi.test.tsx index 4edb884a5..83e44bbb7 100644 --- a/src/features/plugins/__tests__/projectUi.test.tsx +++ b/src/features/plugins/__tests__/projectUi.test.tsx @@ -74,3 +74,28 @@ describe("project UI requests", () => { } }); }); + +it("acknowledges database connection only after the normal project action succeeds", async () => { + const run = vi.fn().mockResolvedValue(undefined); + dispose = registerAction({ id: "db.pluginConnect", title: "Connect Plugin Data Source", run }); + const answerCommand = vi.fn().mockResolvedValue(true); + setPluginsBackend({ ...createMemoryPluginsBackend(), answerCommand }); + handleRuntimeEvent({ + type: "windowRequest", + pluginId: "p", + requestId: "db", + request: { kind: "databaseConnect", id: "owned" }, + }); + await waitFor(() => expect(answerCommand).toHaveBeenCalledWith("db", { kind: "value", value: null })); + expect(run).toHaveBeenCalledWith(expect.objectContaining({ arg: "owned" })); + run.mockRejectedValueOnce(new Error("offline")); + handleRuntimeEvent({ + type: "windowRequest", + pluginId: "p", + requestId: "failed", + request: { kind: "databaseConnect", id: "owned" }, + }); + await waitFor(() => + expect(answerCommand).toHaveBeenCalledWith("failed", { kind: "failed", value: "Error: offline" }), + ); +}); diff --git a/src/features/plugins/__tests__/runTargets.test.ts b/src/features/plugins/__tests__/runTargets.test.ts new file mode 100644 index 000000000..6e3f2ca39 --- /dev/null +++ b/src/features/plugins/__tests__/runTargets.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import { useProject } from "@/platform/project"; +import { usePluginRunTargets } from "@/platform/pluginRunTargets"; +import { withRunTarget } from "@/platform/runTargetSelection"; +import type { RegisteredRunTarget, RunConfig, Settings } from "@/bindings"; +const mocks = vi.hoisted(() => ({ list: vi.fn(), answer: vi.fn(), notify: vi.fn(), expire: vi.fn() })); +vi.mock("../backend", () => ({ pluginsBackend: () => ({ runTargets: mocks.list, answerRunTarget: mocks.answer }) })); +vi.mock("@/workbench/contributions", () => ({ notifications: () => ({ notify: mocks.notify, expire: mocks.expire }) })); +import { refreshRunTargets, startRunTargets } from "../runTargets"; +const target: RegisteredRunTarget = { + id: "plugin-target:4:testddev", + pluginId: "test", + pluginName: "Test", + prompt: true, + target: { + id: "ddev", + name: "DDEV", + program: "ddev", + args: ["exec"], + workdirArgs: [], + containerRoot: null, + commands: [], + }, +}; +beforeEach(() => { + vi.clearAllMocks(); + useProject.setState({ project: { root: `/project-${Math.random()}` } as never, settings: {} }); + usePluginRunTargets.setState({ targets: [] }); + mocks.list.mockResolvedValue([target]); +}); +it("loads settings-window targets after its project arrives without prompting there", async () => { + useProject.setState({ project: null }); + const stop = startRunTargets(false); + try { + useProject.setState({ project: { root: "/settings-owner" } as never }); + await vi.waitFor(() => expect(usePluginRunTargets.getState().targets).toEqual([target])); + expect(mocks.notify).not.toHaveBeenCalled(); + mocks.list.mockResolvedValue([]); + await refreshRunTargets(false); + expect(usePluginRunTargets.getState().targets).toEqual([]); + expect(mocks.answer).not.toHaveBeenCalled(); + } finally { + stop(); + } +}); +it("offers Use / Not now once without changing the selection on registration", async () => { + const original = useProject.getState().settings; + await refreshRunTargets(); + expect(useProject.getState().settings).toBe(original); + expect(mocks.answer).not.toHaveBeenCalled(); + const notification = mocks.notify.mock.calls[0]![0]; + expect(notification.body).toBe("Use DDEV for running PHP / Node / Composer in this project?"); + expect(notification.actions.map((a: { label: string }) => a.label)).toEqual(["Use", "Not now"]); + await refreshRunTargets(); + expect(mocks.notify).toHaveBeenCalledTimes(1); + mocks.answer.mockResolvedValue(original); + mocks.list.mockResolvedValue([{ ...target, prompt: false }]); + notification.actions[1].run(); + await vi.waitFor(() => expect(mocks.answer).toHaveBeenCalledWith(target.id, false)); + expect(useProject.getState().settings).toBe(original); +}); +it("Use applies only the host-returned settings and a removed target disappears", async () => { + const settings: Settings = { + run: { defaultTarget: target.id }, + languages: { php: { runTarget: target.id }, node: { runTarget: target.id } }, + }; + await refreshRunTargets(); + mocks.answer.mockResolvedValue(settings); + mocks.list.mockResolvedValue([{ ...target, prompt: false }]); + mocks.notify.mock.calls[0]![0].actions[0].run(); + await vi.waitFor(() => expect(useProject.getState().settings).toBe(settings)); + mocks.list.mockResolvedValue([]); + await refreshRunTargets(); + expect(usePluginRunTargets.getState().targets).toEqual([]); + expect(mocks.expire).toHaveBeenCalledWith(`plugin-run-target:${target.id}`); +}); +it("explicit local and per-config selections win over interpreter and project defaults", () => { + useProject.setState({ + settings: { + run: { defaultTarget: "default" }, + languages: { php: { runTarget: "php-target" }, node: { runTarget: "node-target" } }, + }, + }); + const config = { command: "php", kind: "php" } as RunConfig; + expect(withRunTarget(config).target).toBe("php-target"); + expect(withRunTarget({ ...config, command: "npm", kind: "npm" }).target).toBe("node-target"); + expect(withRunTarget({ ...config, command: "cargo", kind: "cargo" }).target).toBe("default"); + expect(withRunTarget({ ...config, target: "" }).target).toBe(""); + expect(withRunTarget({ ...config, target: "explicit" }).target).toBe("explicit"); +}); diff --git a/src/features/plugins/__tests__/views.test.tsx b/src/features/plugins/__tests__/views.test.tsx index 8f008fe1b..c4d78af60 100644 --- a/src/features/plugins/__tests__/views.test.tsx +++ b/src/features/plugins/__tests__/views.test.tsx @@ -713,3 +713,32 @@ describe("API 1.3 read-only values and table rows", () => { }, ); }); + +it("carries zero-based problem and search coordinates as JSON and selected paths", () => { + const off = registerAction({ id: "plugin.p.check", title: "Inspect", run: () => {} }); + const plugin = { id: "p", name: "P", status: "active", enabled: true } as unknown as Parameters< + typeof syncUiContributions + >[1][number]; + syncUiContributions( + ["problems.context", "searchResults.context"].map((menu) => ({ + pluginId: "p", + point: "menus", + key: menu, + decl: { menu, command: "check" }, + })), + [plugin], + ); + for (const menu of ["problems", "searchResults"] as const) { + const item = pluginMenuItems(menu, ["/p/a.sql"], { line: 3, column: 5 })[0]!; + expect(item).toMatchObject({ + kind: "action", + action: "plugin.p.check", + arg: { path: "/p/a.sql", paths: ["/p/a.sql"], line: 3, column: 5 }, + }); + expect(commandArgs("arg" in item ? item.arg : null)).toEqual([ + '{"path":"/p/a.sql","line":3,"column":5}', + ["/p/a.sql"], + ]); + } + off(); +}); diff --git a/src/features/plugins/backend.ts b/src/features/plugins/backend.ts index 4612feec1..c83ed774b 100644 --- a/src/features/plugins/backend.ts +++ b/src/features/plugins/backend.ts @@ -1,3 +1,4 @@ +import { useProject } from "@/platform/project"; /** * Plugin host backend: `plugin_*` IPC + a `plugin_subscribe` event channel in the desktop * app; an in-memory simulation (the bundled samples) in plain-browser @@ -9,6 +10,7 @@ import type { CapabilityInfo, CatalogPlugin, CommitAnswer, + CommandAnswer, CommitSelection, Consent, EventKind, @@ -35,6 +37,7 @@ import type { WorkbenchEvent, } from "@/bindings"; import { tx } from "@/platform/i18n"; +import { useRemote } from "@/platform/remote"; import { call, commands, IpcError, isCancelled, isTauri, nextRequestId } from "@/platform/ipc"; import { editorContext } from "./editorContext"; @@ -46,6 +49,14 @@ export interface PluginsBackend { installDir(path: string, consent: Consent): Promise<PluginInfo>; /** Confirms a plugin waiting for consent. */ grant(id: string, consent: Consent): Promise<PluginInfo>; + chooseProgram( + pluginId: string, + capability: string, + scope: "global" | "project", + path: string | null, + ): Promise<PluginInfo>; + program(pluginId: string, program: string, scope: "global" | "project"): Promise<string>; + pickProgram(): Promise<string | null>; setCapability(id: string, capability: string, granted: boolean): Promise<PluginInfo>; /** Validated assets (icon theme mapping and SVGs) whose id starts with `prefix`. */ assets(pluginId: string, prefix: string): Promise<[string, string][]>; @@ -63,6 +74,7 @@ export interface PluginsBackend { editor: PluginEditor | null, paths: string[], settingsScope?: "global" | "project", + commandChain?: string[], ): Promise<string | null>; /** Workbench events plugins subscribed to (see `eventKinds`). */ postEvents(events: WorkbenchEvent[]): Promise<void>; @@ -75,6 +87,17 @@ export interface PluginsBackend { /** Reports the answer to a plugin's `promptRequested`: keys, `[text]`, anything for OK, `null` when dismissed. */ answerPrompt(promptId: string, answer: string[] | null): Promise<void>; /** This window's answer to a plugin's `commitRequested`. */ + answerCommand(requestId: string, answer: CommandAnswer): Promise<void>; + parseTestResults(config: string, chunk: string): Promise<import("@/bindings").PluginTestEvent[] | null>; + closeTerminalSession(operation: string): Promise<void>; + terminalSessionSpawn( + operation: string, + cols: number, + rows: number, + output: Channel<string>, + ): Promise<import("@/bindings").TerminalInfo>; + finishOperation(id: string, status: number | null, stopped: boolean): Promise<void>; + contentTabAction(pluginId: string, action: import("@/bindings").ContentTabAction): Promise<void>; answerCommit(requestId: string, answer: CommitAnswer): Promise<void>; /** Runs plugins' before-commit checks on what `selection` commits. */ commitChecks(selection: CommitSelection, checks: PluginCommitCheckRef[]): Promise<PluginCommitCheckResult[]>; @@ -104,6 +127,8 @@ export interface PluginsBackend { /** The detail tabs of item `key` in a Services or `detail = true` view. */ serviceDetail(pluginId: string, view: string, key: string): Promise<ServiceTab[]>; /** The HTTP routes a plugin reports for this window's project (`[[contributes.endpoints]]`). */ + runTargets(): Promise<import("@/bindings").RegisteredRunTarget[]>; + answerRunTarget(id: string, useTarget: boolean): Promise<import("@/bindings").Settings>; endpoints(pluginId: string): Promise<PluginEndpoint[]>; /** * Runs the program of node `key`'s log tab `tab` (with the toggles in `on`) and streams its @@ -175,6 +200,20 @@ export const tauriPluginsBackend: PluginsBackend = { previewInstall: (path) => call(commands.pluginPreviewInstall(path)), installDir: (path, consent) => call(commands.pluginInstallDir(path, consent)), grant: (id, consent) => call(commands.pluginGrant(id, consent)), + chooseProgram: async (id, cap, scope, path) => { + const remote = !!useRemote.getState().session; + if (remote && path !== null) await call(commands.pluginSetCapability(id, cap, true)); + const chosen = await call(commands.pluginChooseProgram(id, cap, scope, path)); + if (remote && !chosen.capabilities.some((c) => c.id === cap && c.granted)) + await call(commands.pluginSetCapability(id, cap, false)); + return chosen; + }, + program: (id, program, scope) => call(commands.pluginProgram(id, program, scope)), + pickProgram: async () => { + const { open } = await import("@tauri-apps/plugin-dialog"); + const picked = await open({ directory: false, multiple: false, title: tx("Choose plugin executable") }); + return (Array.isArray(picked) ? picked[0] : picked) ?? null; + }, setCapability: (id, capability, granted) => call(commands.pluginSetCapability(id, capability, granted)), assets: (pluginId, prefix) => call(commands.pluginAssets(pluginId, prefix)), activationEvent: async (kind, value) => { @@ -185,10 +224,12 @@ export const tauriPluginsBackend: PluginsBackend = { await call(commands.pluginUninstall(id)); }, reload: (id) => call(commands.pluginReload(id)), - runCommand: (pluginId, commandId, arg, editor, paths, settingsScope) => - call(commands.pluginRunCommand(pluginId, commandId, arg, editor, paths, settingsScope ?? null)), + runCommand: (pluginId, commandId, arg, editor, paths, settingsScope, commandChain) => + call( + commands.pluginRunCommand(pluginId, commandId, arg, editor, paths, settingsScope ?? null, commandChain ?? null), + ), postEvents: async (events) => { - await commands.pluginPostEvents(events); + await commands.pluginPostEvents(events, editorContext()); }, eventKinds: () => commands.pluginEventKinds(), statusItems: () => commands.pluginStatusItems(), @@ -199,6 +240,21 @@ export const tauriPluginsBackend: PluginsBackend = { answerPrompt: async (promptId, answer) => { await commands.pluginAnswerPrompt(promptId, answer); }, + parseTestResults: (config, chunk) => call(commands.pluginParseTestResults(config, chunk)), + closeTerminalSession: async (operation) => { + await call(commands.pluginTerminalSessionClose(operation)); + }, + terminalSessionSpawn: (operation, cols, rows, output) => + call(commands.pluginTerminalSessionSpawn(operation, cols, rows, output)), + finishOperation: async (id, status, stopped) => { + await call(commands.pluginFinishOperation(id, status, stopped)); + }, + contentTabAction: async (pluginId, action) => { + await call(commands.pluginContentTabAction(pluginId, action)); + }, + answerCommand: async (requestId, answer) => { + await call(commands.pluginAnswerCommand(requestId, answer)); + }, answerCommit: async (requestId, answer) => { await commands.pluginAnswerCommit(requestId, answer); }, @@ -239,6 +295,8 @@ export const tauriPluginsBackend: PluginsBackend = { await call(commands.pluginViewEvent(pluginId, view, event, doc, doc == null ? editorContext() : null)); }, serviceDetail: (pluginId, view, key) => call(commands.pluginServiceDetail(pluginId, view, key)), + runTargets: () => call(commands.pluginRunTargets()), + answerRunTarget: (id, useTarget) => call(commands.pluginAnswerRunTarget(id, useTarget)), endpoints: (pluginId) => call(commands.pluginEndpoints(pluginId)), serviceLog: (pluginId, view, key, tab, on, onLine, onError, onExit) => { const request = nextRequestId(); @@ -520,6 +578,11 @@ export function createMemoryPluginsBackend( emit({ type: "commandsChanged" }); return { ...p }; }, + chooseProgram: async (id) => { + return { ...find(id) }; + }, + program: async (_id, program) => program, + pickProgram: async () => null, setCapability: async (id, capability, granted) => { const p = find(id); p.capabilities = p.capabilities.map((c) => (c.id === capability ? { ...c, granted } : c)); @@ -593,7 +656,17 @@ export function createMemoryPluginsBackend( }), viewEvent: async () => {}, serviceDetail: async () => [], + runTargets: async () => [], + answerRunTarget: async () => useProject.getState().settings as import("@/bindings").Settings, endpoints: async () => [], + answerCommand: async () => {}, + finishOperation: async () => {}, + closeTerminalSession: async () => {}, + terminalSessionSpawn: async () => { + throw new Error("Terminal unavailable"); + }, + parseTestResults: async () => null, + contentTabAction: async () => {}, answerCommit: async () => {}, commitChecks: async () => [], serviceLog: (pluginId, view, key, _tab, _on, _onLine, onError) => { diff --git a/src/features/plugins/bridge.ts b/src/features/plugins/bridge.ts index a0fd057ce..328b0c2e9 100644 --- a/src/features/plugins/bridge.ts +++ b/src/features/plugins/bridge.ts @@ -36,6 +36,7 @@ import { defineBuiltinUiPoints } from "./points"; import { localizeContributions, loadPluginCatalogs, usePluginCatalogs } from "./pluginText"; import { clearUiContributions, syncUiContributions } from "./projection"; import { handleRuntimeEvent, refreshEventKinds, startRuntimeUi } from "./runtimeUi"; +import { refreshRunTargets } from "./runTargets"; import { grantPlugin, runPluginCommand, @@ -330,6 +331,7 @@ export function handlePluginEvent(e: PluginEvent) { */ export function followPluginChanges(): () => void { return pluginsBackend().onEvent((e) => { + if (e.type === "runTargetsChanged") void refreshRunTargets(false).catch(() => {}); if (e.type === "commandsChanged" || e.type === "themeChanged" || e.type === "languagesChanged") handlePluginEvent(e); }); diff --git a/src/features/plugins/consent.tsx b/src/features/plugins/consent.tsx index b2f1268a6..8daf92b00 100644 --- a/src/features/plugins/consent.tsx +++ b/src/features/plugins/consent.tsx @@ -30,6 +30,26 @@ export function sortByRisk(caps: readonly CapabilityInfo[]): CapabilityInfo[] { return [...caps].sort((a, b) => RISK_ORDER[a.risk] - RISK_ORDER[b.risk] || a.id.localeCompare(b.id)); } +/** Parameterised permission text, translated independently of the granted host/path. */ +export function capabilityDescription(cap: CapabilityInfo): string { + if (cap.id === "database.sources") + return tx("Manage its own database sources and store their passwords in trusted projects"); + if (cap.id === "ide.commands") { + return tx( + "Run any IDE action or other plugins' commands as if you chose them, including running programs, committing and pushing", + ); + } + if (cap.id.startsWith("net.http:")) { + const host = cap.id.slice(9); + return tx`Connect to ${host} over unencrypted HTTP (from the machine the project is on)`; + } + if (cap.id.startsWith("ipc:")) { + const path = cap.id.slice(4); + return tx`Connect to local socket ${path} — equivalent to running arbitrary code on the project machine`; + } + return tx(cap.description); +} + function ask(req: ConsentRequest): Promise<Consent | null> { return new Promise((resolve) => usePending.setState((s) => ({ queue: [...s.queue, { req, resolve }] }))); } @@ -84,28 +104,30 @@ function ConsentDialog({ pending, onDone }: { pending: Pending; onDone: () => vo ) : ( <ul className={styles.items} aria-label={tx("Requested permissions")}> {caps.map((c) => ( - <li key={c.id} className={styles.item}> - {c.optional ? ( - <Checkbox - checked={accepted.has(c.id)} - onChange={(on) => - setAccepted((s) => { - const next = new Set(s); - if (on) next.add(c.id); - else next.delete(c.id); - return next; - }) - } - aria-label={tx`Allow ${c.id}`} - /> - ) : ( - <span className={styles.consentRequired} title={tx("Required")}> - • - </span> - )} - <span className={styles.itemMain}>{tx(c.description)}</span> - <span className={styles.mono}>{c.id}</span> - <Badge tone={RISK_LABEL[c.risk].tone}>{RISK_LABEL[c.risk].label}</Badge> + <li key={c.id} className={styles.capabilityItem}> + <div className={styles.capabilityHeader}> + {c.optional ? ( + <Checkbox + checked={accepted.has(c.id)} + onChange={(on) => + setAccepted((s) => { + const next = new Set(s); + if (on) next.add(c.id); + else next.delete(c.id); + return next; + }) + } + aria-label={tx`Allow ${c.id}`} + /> + ) : ( + <span className={styles.consentRequired} title={tx("Required")}> + • + </span> + )} + <span className={styles.mono}>{c.id}</span> + <Badge tone={RISK_LABEL[c.risk].tone}>{RISK_LABEL[c.risk].label}</Badge> + </div> + <p className={styles.capabilityDescription}>{capabilityDescription(c)}</p> </li> ))} </ul> diff --git a/src/features/plugins/decorations.module.css b/src/features/plugins/decorations.module.css new file mode 100644 index 000000000..572ea515f --- /dev/null +++ b/src/features/plugins/decorations.module.css @@ -0,0 +1,39 @@ +.highlight { + background: var(--accent-soft); +} +.search { + background: var(--bg-search-match); + color: var(--fg-search-match); +} +.added { + background: color-mix(in srgb, var(--vcs-added) 18%, transparent); +} +.modified { + background: color-mix(in srgb, var(--vcs-modified) 18%, transparent); +} +.removed { + background: color-mix(in srgb, var(--vcs-deleted) 18%, transparent); +} +.warning { + text-decoration: underline wavy var(--warning); +} +.error { + text-decoration: underline wavy var(--danger); +} +.info { + text-decoration: underline dotted var(--info); +} +.underline { + text-decoration: underline; +} +.strikethrough { + text-decoration: line-through; +} +.muted { + color: var(--fg-muted); +} +.after { + white-space: pre; + margin-inline-start: calc(var(--space-2) / 2); + color: var(--fg-muted); +} diff --git a/src/features/plugins/decorations.ts b/src/features/plugins/decorations.ts new file mode 100644 index 000000000..cdf6e439e --- /dev/null +++ b/src/features/plugins/decorations.ts @@ -0,0 +1,149 @@ +import { StateEffect, StateField, type Text } from "@codemirror/state"; +import { Decoration, EditorView, WidgetType, type DecorationSet as CmDecorations } from "@codemirror/view"; +import type { DecorationSet, Range } from "@/bindings"; +import { registerEditorContribution, useEditorRegistry, type EditorDocContext } from "@/editor/api"; +import { commands, isTauri } from "@/platform/ipc"; +import { useProject } from "@/platform/project"; +import { setFileDecorations, type FileDecoration } from "@/workbench/contributions"; +import styles from "./decorations.module.css"; + +const snapshots = new Map<string, DecorationSet[]>(); +const refresh = StateEffect.define<null>(); +const versions = new Map<string, number>(); +const colors: Record<string, string> = { + highlight: "var(--accent)", + search: "var(--fg-search-match)", + added: "var(--vcs-added)", + modified: "var(--vcs-modified)", + removed: "var(--vcs-deleted)", + warning: "var(--warning)", + error: "var(--danger)", + info: "var(--info)", + underline: "var(--fg-editor)", + strikethrough: "var(--fg-editor)", + muted: "var(--fg-muted)", +}; + +function currentSets(sets: DecorationSet[]) { + const root = useProject.getState().project?.root; + return sets.filter((s) => s.root === root); +} + +function updateTree(plugin: string, sets: DecorationSet[]) { + const items = new Map<string, FileDecoration>(); + for (const s of currentSets(sets)) + if (s.tree && s.file && Object.hasOwn(colors, s.file.style)) { + items.set(s.path, { + color: colors[s.file.style], + badge: s.file.badge ?? undefined, + tooltip: s.file.tooltip ?? undefined, + faded: s.file.style === "muted", + error: s.file.style === "error", + }); + } + setFileDecorations(`plugin:${plugin}`, items); +} + +/** Replace a plugin's snapshots. Project roots are filtered on every render. */ +export function setPluginDecorations(plugin: string, sets: DecorationSet[]) { + versions.set(plugin, (versions.get(plugin) ?? 0) + 1); + snapshots.set(plugin, sets); + updateTree(plugin, sets); + for (const editor of useEditorRegistry.getState().editors) editor.view.dispatch({ effects: refresh.of(null) }); +} + +function offset(doc: Text, pos: Range["start"]): number | null { + if (pos.line >= doc.lines) return null; + const line = doc.line(pos.line + 1); + return pos.col <= line.length ? line.from + pos.col : null; +} + +class AfterText extends WidgetType { + constructor( + readonly text: string, + readonly style: string, + readonly hover: string | null, + ) { + super(); + } + override eq(other: AfterText) { + return this.text === other.text && this.style === other.style && this.hover === other.hover; + } + override toDOM() { + const span = document.createElement("span"); + span.className = `${styles.after} ${this.style}`; + span.textContent = this.text; + if (this.hover) span.title = this.hover; + return span; + } +} + +export function decorationExtension(ctx: EditorDocContext) { + const build = (doc: Text): CmDecorations => { + const ranges = []; + for (const sets of snapshots.values()) + for (const set of currentSets(sets)) + if (!set.tree && set.path === ctx.path) { + for (const item of set.items) { + const className = Object.hasOwn(colors, item.style) ? styles[item.style] : undefined; + if (!className) continue; + const from = offset(doc, item.range.start), + to = offset(doc, item.range.end); + if (from == null || to == null || from > to) continue; + if (from < to) + ranges.push( + Decoration.mark({ class: className, attributes: item.hover ? { title: item.hover } : {} }).range( + from, + to, + ), + ); + if (item.after) + ranges.push( + Decoration.widget({ widget: new AfterText(item.after, className, item.hover), side: 1 }).range(to), + ); + } + } + return Decoration.set(ranges, true); + }; + return StateField.define<CmDecorations>({ + create: (state) => build(state.doc), + update: (value, tr) => (tr.effects.some((e) => e.is(refresh)) ? build(tr.state.doc) : value.map(tr.changes)), + provide: (field) => EditorView.decorations.from(field), + }); +} + +/** Catch up editors and project trees opened after publication. */ +export function startPluginDecorations(): () => void { + let disposed = false; + let revision = 0; + const load = () => { + const at = ++revision; + const before = new Map(versions); + if (isTauri()) + void Promise.resolve(commands.pluginDecorationSets()) + .then((all) => { + if (disposed || at !== revision) return; + for (const plugin of snapshots.keys()) + if (!all[plugin] && versions.get(plugin) === before.get(plugin)) setPluginDecorations(plugin, []); + for (const [plugin, sets] of Object.entries(all)) + if (versions.get(plugin) === before.get(plugin)) setPluginDecorations(plugin, sets); + }) + .catch(() => {}); + }; + const offProject = useProject.subscribe((s, prev) => { + if (s.project?.root === prev.project?.root) return; + for (const [plugin, sets] of snapshots) updateTree(plugin, sets); + for (const editor of useEditorRegistry.getState().editors) editor.view.dispatch({ effects: refresh.of(null) }); + load(); + }); + const off = registerEditorContribution({ id: "plugins.decorations", extensions: decorationExtension }); + load(); + return () => { + disposed = true; + offProject(); + off(); + for (const plugin of snapshots.keys()) setPluginDecorations(plugin, []); + snapshots.clear(); + versions.clear(); + }; +} diff --git a/src/features/plugins/index.ts b/src/features/plugins/index.ts index d2cf79c39..2f06e61eb 100644 --- a/src/features/plugins/index.ts +++ b/src/features/plugins/index.ts @@ -1,3 +1,4 @@ +import { pluginLanguageVisuals } from "./languageVisuals"; /** * Plugins: Settings › Plugins (installed list, enable/disable, install from * disk with a permission page, uninstall, theme selection) and the runtime @@ -33,7 +34,9 @@ import { defineBuiltinUiPoints } from "./points"; import { startViewUi } from "./uiPoints"; import { showInstalledIntellijThemes, useIntellijThemesDialog } from "./intellijThemesDialogStore"; import { startWorkspaceContains } from "./workspaceContains"; +import { startPluginDecorations } from "./decorations"; import { startIndexedMarkers } from "./indexedMarkers"; +import { startRunTargets } from "./runTargets"; import { startPluginNavigation } from "./navigation"; import { installFromDisk, refreshPlugins, reloadPlugin, SETTINGS_PAGE_ID, updateAllPlugins, usePlugins } from "./store"; @@ -62,6 +65,7 @@ export function activateSettingsPages(): () => void { provideConsentDialog(), defineBuiltinUiPoints(), followPluginChanges(), + startRunTargets(false), syncProjection(), trackPluginTheme(), trackIconTheme(), @@ -151,10 +155,12 @@ export function activate(): () => void { startPluginBridge(), startPluginNavigation(), startIndexedMarkers(), + startPluginDecorations(), // Plugin custom editors (tab kind) and context menu entries. startViewUi((input) => void editorService().openInput(input)), startWorkspaceContains(), // Gutter icons from plugins' `line-markers` providers (next to the LSP ones). + registerEditorContribution({ id: "plugins.languageFeatures", order: 58, extensions: pluginLanguageVisuals }), registerEditorContribution({ id: "plugins.lineMarkers", order: 57, extensions: pluginLineMarkerGutter }), // Dev plugins (`<project>/.openidea/plugins`) depend on the open project. useProject.subscribe((s, prev) => { diff --git a/src/features/plugins/languageVisuals.test.ts b/src/features/plugins/languageVisuals.test.ts new file mode 100644 index 000000000..5186d7349 --- /dev/null +++ b/src/features/plugins/languageVisuals.test.ts @@ -0,0 +1,72 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { EditorState } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { syntaxHighlighting, defaultHighlightStyle } from "@codemirror/language"; +const ipc = vi.hoisted(() => ({ + pluginLanguageVisuals: vi.fn(), + lspExecuteCommand: vi.fn(async () => ({ status: "ok", data: null })), +})); +vi.mock("@/platform/ipc", async (original) => ({ + ...(await original<typeof import("@/platform/ipc")>()), + commands: ipc, +})); +const { injectedHighlights, pluginLanguageVisuals } = await import("./languageVisuals"); +const ctx = { doc: 7, path: "/p/a.txt", language: "text", uiMode: null, readOnly: false }; +const views: EditorView[] = []; +afterEach(() => { + views.splice(0).forEach((v) => v.destroy()); + vi.useRealTimers(); + vi.clearAllMocks(); +}); +it("highlights only the embedded slice with its own parser and the editor theme", async () => { + const state = EditorState.create({ + doc: "host SELECT a FROM t host", + extensions: [syntaxHighlighting(defaultHighlightStyle)], + }); + const deco = await injectedHighlights(state, [ + { range: { start: { line: 0, col: 5 }, end: { line: 0, col: 20 } }, language: "sql" }, + ]); + expect(deco.size).toBeGreaterThan(0); + const cursor = deco.iter(); + while (cursor.value) { + expect(cursor.from).toBeGreaterThanOrEqual(5); + expect(cursor.to).toBeLessThanOrEqual(20); + expect(cursor.value.spec.class).toMatch(/\S/); + cursor.next(); + } +}); +it("renders owned gutter commands and drops answers to superseded buffers", async () => { + vi.useFakeTimers(); + let answer!: (v: unknown) => void; + ipc.pluginLanguageVisuals.mockImplementationOnce( + () => + new Promise((r) => { + answer = r; + }), + ); + const visual = { + version: 2, + injections: [], + colors: [], + gutterActions: [["test", { line: 0, icon: "run", tooltip: "Run test", command: "run", argument: "value" }]], + }; + ipc.pluginLanguageVisuals.mockResolvedValue({ status: "ok", data: visual }); + const view = new EditorView({ + state: EditorState.create({ doc: "before", extensions: pluginLanguageVisuals(ctx) }), + parent: document.body, + }); + views.push(view); + await vi.advanceTimersByTimeAsync(300); + view.dispatch({ changes: { from: 0, to: 6, insert: "after" } }); + answer({ status: "ok", data: visual }); + await vi.advanceTimersByTimeAsync(0); + expect(view.dom.querySelector(".cm-plugin-gutter-action")).toBeNull(); + await vi.advanceTimersByTimeAsync(300); + const button = view.dom.querySelector<HTMLButtonElement>(".cm-plugin-gutter-action")!; + expect(button.getAttribute("aria-label")).toBe("Run test"); + button.click(); + expect(ipc.lspExecuteCommand).toHaveBeenCalledWith( + 7, + expect.objectContaining({ command: "openidea.plugin-language:test:run", arguments: ['"value"'] }), + ); +}); diff --git a/src/features/plugins/languageVisuals.ts b/src/features/plugins/languageVisuals.ts new file mode 100644 index 000000000..d14440932 --- /dev/null +++ b/src/features/plugins/languageVisuals.ts @@ -0,0 +1,190 @@ +/** Embedded highlighting and command gutter actions share the editor-provider refresh. */ +import { + Compartment, + Prec, + RangeSet, + StateEffect, + StateField, + type Extension, + type EditorState, +} from "@codemirror/state"; +import { + Decoration, + EditorView, + GutterMarker, + ViewPlugin, + gutter, + type DecorationSet, + type ViewUpdate, +} from "@codemirror/view"; +import { highlightingFor } from "@codemirror/language"; +import { highlightTree } from "@lezer/highlight"; +import { type EditorDocContext } from "@/editor/api"; +import { rangeToOffsets } from "@/editor/lsp/positions"; +import { pluginVisuals } from "@/editor/lsp/pluginVisuals"; +import { findLanguageDescription, languageInfo, loadSupport } from "@/editor/core/cm/languages"; +import { onNavigationProvidersChanged } from "@/editor/navigationProviders"; +import { call, commands, type GutterAction, type Injection } from "@/platform/ipc"; +import { gutterIconMarkup } from "@/ui/icons"; +import { resolveMarkerIcon, onMarkerIconsChanged } from "./markerIcons"; +import { markerIcon } from "./lineMarkerGutter"; +import styles from "@/editor/navigation/LineMarkers.module.css"; + +export async function injectedHighlights(state: EditorState, injections: readonly Injection[]): Promise<DecorationSet> { + const ranges = await Promise.all( + injections.map(async (i) => { + const { from, to } = rangeToOffsets(state.doc, i.range); + if (from >= to) return []; + const desc = findLanguageDescription(languageInfo(i.language)?.uiMode ?? i.language, ""); + const support = desc ? await loadSupport(desc) : null; + if (!support) return []; + const marks = [] as ReturnType<Decoration["range"]>[]; + highlightTree( + support.language.parser.parse(state.sliceDoc(from, to)), + { style: (tags) => highlightingFor(state, tags) }, + (a, b, cls) => marks.push(Decoration.mark({ class: cls }).range(from + a, from + b)), + ); + return marks; + }), + ); + return Decoration.set(ranges.flat(), true); +} +const setHighlight = StateEffect.define<DecorationSet>(); +const highlights = StateField.define<DecorationSet>({ + create: () => Decoration.none, + update(value, tr) { + for (const e of tr.effects) if (e.is(setHighlight)) return e.value; + return tr.docChanged ? Decoration.none : value; + }, + provide: (f) => Prec.high(EditorView.decorations.from(f)), +}); +class ActionMarker extends GutterMarker { + constructor( + readonly plugin: string, + readonly action: GutterAction, + readonly ctx: EditorDocContext, + ) { + super(); + } + override eq(other: GutterMarker) { + return ( + other instanceof ActionMarker && + other.plugin === this.plugin && + JSON.stringify(other.action) === JSON.stringify(this.action) + ); + } + override toDOM() { + const el = document.createElement("button"); + el.type = "button"; + el.className = `${styles.marker} cm-plugin-gutter-action`; + el.title = this.action.tooltip; + el.setAttribute("aria-label", this.action.tooltip); + el.innerHTML = gutterIconMarkup(markerIcon(this.action.icon)) ?? ""; + el.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + void call( + commands.lspExecuteCommand(this.ctx.doc, { + title: this.action.tooltip, + command: `openidea.plugin-language:${this.plugin}:${this.action.command}`, + arguments: [JSON.stringify(this.action.argument)], + }), + ).catch(console.warn); + }; + return el; + } +} +const setActions = StateEffect.define<{ pos: number; marker: ActionMarker }[]>(); +const actions = StateField.define<{ pos: number; marker: ActionMarker }[]>({ + create: () => [], + update(v, tr) { + for (const e of tr.effects) if (e.is(setActions)) return e.value; + return tr.docChanged ? [] : v; + }, +}); +const column = gutter({ + class: `${styles.gutter} cm-plugin-gutter-actions`, + markers: (view) => + RangeSet.of( + view.state.field(actions).map((a) => a.marker.range(a.pos)), + true, + ), +}); +export function pluginLanguageVisuals(ctx: EditorDocContext): Extension { + if (ctx.doc < 0) return []; + const compartment = new Compartment(); + return [ + highlights, + actions, + compartment.of([]), + ViewPlugin.define( + (view) => + new (class { + timer: ReturnType<typeof setTimeout> | undefined; + generation = 0; + shown = false; + off: () => void; + constructor() { + const a = onNavigationProvidersChanged(() => this.schedule(true)); + const b = onMarkerIconsChanged(() => this.schedule()); + this.off = () => { + a(); + b(); + }; + this.schedule(); + } + schedule(clear = false) { + this.generation++; + clearTimeout(this.timer); + if (clear) + queueMicrotask(() => { + if (view.dom.isConnected) + view.dispatch({ + effects: [setHighlight.of(Decoration.none), setActions.of([]), compartment.reconfigure([])], + }); + this.shown = false; + }); + this.timer = setTimeout(() => void this.run(), 300); + } + update(u: ViewUpdate) { + if (u.docChanged) this.schedule(); + } + async run() { + const generation = this.generation; + const text = view.state.doc; + try { + const result = await pluginVisuals(view, ctx); + const deco = await injectedHighlights(view.state, result.injections); + const placed = await Promise.all( + result.gutterActions + .filter(([, a]) => a.line < text.lines) + .map(async ([plugin, a]) => ({ + pos: text.line(a.line + 1).from, + marker: new ActionMarker(plugin, { ...a, icon: await resolveMarkerIcon(plugin, a.icon) }, ctx), + })), + ); + if (generation !== this.generation || view.state.doc !== text || !view.dom.isConnected) return; + this.shown = !!placed.length; + view.dispatch({ + effects: [ + setHighlight.of(deco), + setActions.of(placed), + compartment.reconfigure(this.shown ? column : []), + ], + }); + } catch { + if (generation === this.generation && view.dom.isConnected) + view.dispatch({ + effects: [setHighlight.of(Decoration.none), setActions.of([]), compartment.reconfigure([])], + }); + } + } + destroy() { + this.generation++; + clearTimeout(this.timer); + this.off(); + } + })(), + ), + ]; +} diff --git a/src/features/plugins/pluginText.ts b/src/features/plugins/pluginText.ts index 8f9b1291c..ce28f0d64 100644 --- a/src/features/plugins/pluginText.ts +++ b/src/features/plugins/pluginText.ts @@ -82,6 +82,7 @@ const TEXT_KEYS: Record<string, readonly string[]> = { statusBar: ["text", "tooltip"], settings: ["title"], fileTemplates: ["name"], + snippets: [], iconThemes: ["label"], themes: ["name"], secrets: ["label", "description"], @@ -91,6 +92,7 @@ function localizeDecl(decl: unknown, keys: readonly string[], t: PluginTranslate if (!decl || typeof decl !== "object") return decl; const out: Record<string, unknown> = { ...(decl as Record<string, unknown>) }; for (const key of keys) if (typeof out[key] === "string") out[key] = t(out[key]); + if (Array.isArray(out.entries)) out.entries = out.entries.map((s) => localizeDecl(s, ["description"], t)); if (Array.isArray(out.properties)) out.properties = out.properties.map((p) => localizeDecl(p, ["title", "description", "group"], t)); if (Array.isArray(out.pages)) out.pages = out.pages.map((p) => localizeDecl(p, ["title"], t)); diff --git a/src/features/plugins/points.tsx b/src/features/plugins/points.tsx index 3e3f5cc2f..60007fb97 100644 --- a/src/features/plugins/points.tsx +++ b/src/features/plugins/points.tsx @@ -5,6 +5,7 @@ */ import { registerEndpointProvider, type EndpointEntry } from "@/workbench/contributions"; import type { PluginInfo, ThemeDef } from "@/bindings"; +import { pluginIconName } from "./pluginIcon"; import { registerAction } from "@/platform/actions"; import { tx } from "@/platform/i18n"; import { addContributedTheme, pluginThemeRef } from "@/platform/themes"; @@ -22,8 +23,15 @@ import styles from "./PluginsPage.module.css"; /** Unpacks an action argument: a bare path, or a menu's `{ path, paths }`. */ export function commandArgs(arg: unknown): [string | null, string[]] { if (typeof arg === "string") return [arg, []]; - const { path, paths } = (arg ?? {}) as { path?: unknown; paths?: unknown }; + const { path, paths, line, column } = (arg ?? {}) as { + path?: unknown; + paths?: unknown; + line?: unknown; + column?: unknown; + }; if (typeof path !== "string" || !Array.isArray(paths)) return [null, []]; + if (typeof line === "number" && typeof column === "number") + return [JSON.stringify({ path, line, column }), paths.filter((p): p is string => typeof p === "string")]; return [path, paths.filter((p): p is string => typeof p === "string")]; } @@ -32,6 +40,7 @@ export const pluginActionId = (pluginId: string, commandId: string) => `plugin.$ interface CommandDecl { id: string; title: string; + icon?: string | null; category?: string | null; when?: string | null; } @@ -181,24 +190,47 @@ function providePluginEndpoints(pluginId: string): () => void { /** Registers the client side of every first-party UI point. */ export function defineBuiltinUiPoints(): () => void { const disposers = [ + defineUiPoint("mcpTools", {}), defineUiPoint("endpoints", { project: ({ pluginId }) => providePluginEndpoints(pluginId) }), defineUiPoint<CommandDecl>("commands", { title: "Commands", - project: ({ pluginId, decl }, { plugin }) => - registerAction({ + project: ({ pluginId, decl }, { plugin }) => { + let disposed = false; + const action = { id: pluginActionId(pluginId, decl.id), title: decl.title, category: decl.category ?? plugin.name, - icon: "package", + icon: + decl.icon && !decl.icon.endsWith(".svg") + ? decl.icon + : plugin.icon + ? pluginIconName(pluginId, plugin.icon) + : "package", when: decl.when ?? undefined, verbatim: true, keywords: ["plugin", plugin.name, pluginId], description: tx`From plugin ${plugin.name}`, - run: async ({ arg }) => { + run: async ({ arg }: { arg?: unknown }) => { const [path, paths] = commandArgs(arg); await runPluginCommand(pluginId, decl.id, path, paths); }, - }), + }; + let off = registerAction(action); + if (decl.icon?.endsWith(".svg")) + void pluginsBackend() + .assets(pluginId, `commandIcon/${decl.id}`) + .then((assets) => { + const svg = assets.find(([id]) => id === `commandIcon/${decl.id}`)?.[1]; + if (!svg || disposed) return; + off(); + off = registerAction({ ...action, icon: pluginIconName(pluginId, { light: svg, dark: null }) }); + }) + .catch(console.warn); + return () => { + disposed = true; + off(); + }; + }, Details: CommandsDetails, }), // Offered by every theme picker while the plugin is live; `followThemeSetting` applies the selected one. diff --git a/src/features/plugins/runTargets.ts b/src/features/plugins/runTargets.ts new file mode 100644 index 000000000..761f77b86 --- /dev/null +++ b/src/features/plugins/runTargets.ts @@ -0,0 +1,79 @@ +import type { RegisteredRunTarget } from "@/bindings"; +import { tx } from "@/platform/i18n"; +import { isTauri } from "@/platform/ipc"; +import { useProject } from "@/platform/project"; +import { usePluginRunTargets } from "@/platform/pluginRunTargets"; +import { notifications } from "@/workbench/contributions"; +import { pluginsBackend } from "./backend"; + +let revision = 0; +const shown = new Set<string>(); +function clearTargets(prompts = true) { + revision++; + if (prompts) + for (const target of usePluginRunTargets.getState().targets) + notifications().expire?.(`plugin-run-target:${target.id}`); + shown.clear(); + usePluginRunTargets.setState({ targets: [] }); +} +export async function refreshRunTargets(prompts = true) { + const request = ++revision; + const root = useProject.getState().project?.root; + if (!root) { + clearTargets(prompts); + return; + } + const targets = await pluginsBackend().runTargets(); + if (request !== revision || root !== useProject.getState().project?.root) return; + for (const previous of prompts ? usePluginRunTargets.getState().targets : []) { + if (!targets.some((t) => t.id === previous.id && t.prompt)) { + notifications().expire?.(`plugin-run-target:${previous.id}`); + shown.delete(`${root}\0${previous.id}`); + } + } + usePluginRunTargets.setState({ targets }); + if (!prompts) return; + for (const target of targets) { + const key = `${root}\0${target.id}`; + if (!target.prompt || shown.has(key)) continue; + shown.add(key); + const answer = async (use: boolean) => { + if (root !== useProject.getState().project?.root) return; + try { + const settings = await pluginsBackend().answerRunTarget(target.id, use); + if (root === useProject.getState().project?.root) useProject.setState({ settings }); + await refreshRunTargets(); + } catch (error) { + shown.delete(key); + notifications().notify({ group: "plugins", level: "error", title: "Run Targets", body: String(error) }); + } + }; + notifyTarget(target, answer); + } +} +function notifyTarget(target: RegisteredRunTarget, answer: (use: boolean) => Promise<void>) { + notifications().notify({ + sticky: true, + group: "plugins", + key: `plugin-run-target:${target.id}`, + level: "info", + title: "Run Targets", + body: tx`Use ${target.target.name} for running PHP / Node / Composer in this project?`, + actions: [ + { label: "Use", run: () => void answer(true) }, + { label: "Not now", run: () => void answer(false) }, + ], + }); +} +export function startRunTargets(prompts = true): () => void { + if (isTauri()) void refreshRunTargets(prompts).catch(() => {}); + const unsubscribe = useProject.subscribe((s, old) => { + if (s.project?.root === old.project?.root) return; + clearTargets(prompts); + void refreshRunTargets(prompts).catch(() => {}); + }); + return () => { + unsubscribe(); + clearTargets(prompts); + }; +} diff --git a/src/features/plugins/runtimeUi.tsx b/src/features/plugins/runtimeUi.tsx index ecd86c00c..74c68c84a 100644 --- a/src/features/plugins/runtimeUi.tsx +++ b/src/features/plugins/runtimeUi.tsx @@ -4,9 +4,19 @@ * workbench events it subscribed to (`events.subscribe`), reported in * batches. */ +import { providePluginTestParser } from "@/workbench/contributions"; +import { + contentTabData, + contentTabKey, + handleContentTabReplyText, + PLUGIN_CONTENT_EDITOR_KIND, + WASM_CONTENT_VIEW, +} from "./views/contentTab"; import { create } from "zustand"; +import { refreshRunTargets, startRunTargets } from "./runTargets"; import type { CommitAnswer, + CommandAnswer, CommitRequest, EventKind, PluginContextKey, @@ -15,8 +25,11 @@ import type { StatusItem, WorkbenchEvent, } from "@/bindings"; -import { onEditorEvent } from "@/editor/api"; -import { executeAction, getAction, onActionExecuted } from "@/platform/actions"; +import { EditorSelection } from "@codemirror/state"; +import { EditorView } from "@codemirror/view"; +import { setPluginDecorations } from "./decorations"; +import { editorsFor, onEditorEvent } from "@/editor/api"; +import { executeAction, executeActionOrThrow, getAction, onActionExecuted } from "@/platform/actions"; import { clearContext, setContext, type ContextValue } from "@/platform/context"; import { onIdeEvent } from "@/platform/ipc"; import { openExternalUrl } from "@/platform/openUrl"; @@ -29,8 +42,10 @@ import { notifications, maybeCommitService, maybeEditorService, registerStatusIt import { pickItems, promptInput } from "@/workbench/inputDialogs"; import { confirm } from "@/workbench/messages"; import { removeTask, upsertTask, useTasks } from "@/workbench/tasks"; +import { editorContext } from "./editorContext"; import { pluginsBackend } from "./backend"; import { usePluginT } from "./pluginText"; +import { setPluginOutput } from "./views/outputStore"; import { PLUGIN_LOG_EDITOR_KIND } from "./views/logEditor"; import type { PluginLogEditorData } from "./views/logTab"; import { bumpView, deliverViewMessage } from "./views/viewStore"; @@ -45,11 +60,22 @@ export const usePluginStatusItems = create<{ items: Map<ItemKey, { pluginId: str items: new Map(), })); -function setStatusItem(pluginId: string, id: string, item: StatusItem | null) { +let statusRevision = 0; +let statusRequest = 0; +const globalStatus = new Map<ItemKey, { pluginId: string; item: StatusItem }>(); +const windowStatus = new Map<ItemKey, { pluginId: string; item: StatusItem }>(); + +function setStatusItem(pluginId: string, id: string, item: StatusItem | null, window = false) { + statusRevision++; + const key: ItemKey = `${pluginId}/${id}`; + const scope = window ? windowStatus : globalStatus; + if (item) scope.set(key, { pluginId, item }); + else scope.delete(key); + const effective = windowStatus.get(key) ?? globalStatus.get(key); usePluginStatusItems.setState((s) => { const items = new Map(s.items); - if (item) items.set(`${pluginId}/${id}`, { pluginId, item }); - else items.delete(`${pluginId}/${id}`); + if (effective) items.set(key, effective); + else items.delete(key); return { items }; }); } @@ -161,9 +187,21 @@ function projectStatusItems(): () => void { * window's project is open and trusted, so call it again when that happens. */ function refreshStatusItems() { + const request = ++statusRequest; + const revision = statusRevision; void pluginsBackend() .statusItems() - .then((items) => items.forEach((i) => setStatusItem(i.pluginId, i.id, i.item))) + .then((items) => { + if (request !== statusRequest) return; + if (revision !== statusRevision) { + refreshStatusItems(); + return; + } + globalStatus.clear(); + windowStatus.clear(); + usePluginStatusItems.setState({ items: new Map() }); + items.forEach((i) => setStatusItem(i.pluginId, i.id, i.item, i.window)); + }) .catch(() => {}); } @@ -271,7 +309,10 @@ let postTimer: ReturnType<typeof setTimeout> | null = null; export function refreshEventKinds() { void pluginsBackend() .eventKinds() - .then((kinds) => (wanted = new Set(kinds))) + .then((kinds) => { + wanted = new Set(kinds); + if (wanted.has("settings-changed")) post({ kind: "settingsChanged", value: [] }); + }) .catch(() => {}); } @@ -293,6 +334,9 @@ function trackWorkbenchEvents(): () => void { refreshEventKinds(); const offEditor = onEditorEvent((e) => { if (e.type === "docOpened" && wanted.has("file-opened")) post({ kind: "fileOpened", value: e.path }); + if (e.type === "docClosed" && wanted.has("file-closed")) post({ kind: "fileClosed", value: e.path }); + if (e.type === "inputModeChanged" && wanted.has("input-mode-changed")) + post({ kind: "inputModeChanged", value: { path: e.path, mode: e.mode, sub: e.sub } }); if (e.type === "docSaved" && wanted.has("file-saved")) post({ kind: "fileSaved", value: e.path }); }); const offActions = onActionExecuted((id, _arg, source) => { @@ -303,6 +347,9 @@ function trackWorkbenchEvents(): () => void { value: { action: id, title: action?.title ?? id, source, shortcut: shortcutLabel(id) ?? null }, }); }); + const offSettings = onIdeEvent("settingsChanged", () => { + if (wanted.has("settings-changed")) post({ kind: "settingsChanged", value: [] }); + }); const offVcs = onIdeEvent("vcsChanged", (e) => { if (wanted.has("vcs-changed")) post({ kind: "vcsChanged", value: e.root }); }); @@ -310,6 +357,7 @@ function trackWorkbenchEvents(): () => void { offEditor(); offActions(); offVcs(); + offSettings(); if (postTimer) clearTimeout(postTimer); postTimer = null; queue = []; @@ -395,12 +443,78 @@ async function answerCommit(requestId: string, request: CommitRequest) { .catch(() => {}); } +async function answerCommand(e: Extract<PluginEvent, { type: "executeCommand" }>) { + let answer: CommandAnswer; + try { + const command = usePlugins + .getState() + .ui.find((c) => c.point === "commands" && e.id === `plugin.${c.pluginId}.${(c.decl as { id: string }).id}`); + if (command) { + const value = await pluginsBackend().runCommand( + command.pluginId, + (command.decl as { id: string }).id, + e.arg, + editorContext(), + [], + undefined, + e.chain, + ); + answer = { kind: "value", value }; + } else if (getAction(e.id)) { + await executeAction(e.id, e.arg); + answer = { kind: "value", value: null }; + } else answer = { kind: "notFound" }; + } catch (error) { + answer = { kind: "failed", value: error instanceof Error ? error.message : String(error) }; + } + await pluginsBackend() + .answerCommand(e.requestId, answer) + .catch(() => {}); +} + /** Handles the runtime UI events; returns whether `e` was one. */ export function handleRuntimeEvent(e: PluginEvent): boolean { switch (e.type) { + case "runTargetsChanged": + void refreshRunTargets().catch(() => {}); + return true; + case "decorationsChanged": + setPluginDecorations(e.pluginId, e.sets); + return true; + case "editorOpen": { + const r = e.selection; + void maybeEditorService()?.openFile(e.path, { + focus: true, + selection: r ? { line: r.start.line, col: r.start.col, endLine: r.end.line, endCol: r.end.col } : undefined, + }); + return true; + } + case "editorRanges": { + for (const editor of editorsFor(e.path)) { + const doc = editor.view.state.doc; + const ranges = e.ranges.map((r) => { + if (r.start.line >= doc.lines || r.end.line >= doc.lines) return null; + const start = doc.line(r.start.line + 1), + end = doc.line(r.end.line + 1); + if (r.start.col > start.length || r.end.col > end.length) return null; + return EditorSelection.range(start.from + r.start.col, end.from + r.end.col); + }); + if (!ranges.length || ranges.some((r) => r == null)) continue; + const valid = ranges.filter((r) => r != null); + if (e.reveal) editor.view.dispatch({ effects: EditorView.scrollIntoView(valid[0]!, { y: "center" }) }); + else editor.view.dispatch({ selection: EditorSelection.create(valid) }); + } + return true; + } + case "executeCommand": + void answerCommand(e); + return true; case "contextChanged": refreshContextKeys(); return true; + case "windowStatusItemChanged": + setStatusItem(e.pluginId, e.id, e.item, true); + return true; case "statusItemChanged": setStatusItem(e.pluginId, e.id, e.item); return true; @@ -418,10 +532,43 @@ export function handleRuntimeEvent(e: PluginEvent): boolean { case "viewMessage": deliverViewMessage(e.pluginId, e.view, e.message); return true; + case "windowRequest": + void handleWindowRequest(e); + return true; + case "contentTabReply": + handleContentTabReplyText( + e.pluginId, + JSON.stringify({ + op: "content-tab-reply", + requestId: e.requestId, + ...JSON.parse(JSON.stringify(e.reply), (_k, v: unknown) => (v === null ? undefined : v)), + }), + ); + return true; case "openTerminal": // The program was approved (`process:`) and verified by the host. void executeAction("terminal.runCommand", { program: e.program, args: e.args, name: e.title, cwd: e.cwd }); return true; + case "outputChanged": + setPluginOutput(e.pluginId, e.channel, e.text); + return true; + case "revealOutput": + setPluginOutput(e.pluginId, e.channel, e.text); + void maybeEditorService()?.openInput({ + kind: PLUGIN_LOG_EDITOR_KIND, + key: `pluginOutput:${JSON.stringify([e.pluginId, e.channel])}`, + data: { + pluginId: e.pluginId, + view: "", + key: e.channel, + title: tx`Output: ${e.channel}`, + program: "", + args: [], + toggles: [], + origin: "output", + } satisfies PluginLogEditorData, + }); + return true; case "openLogTab": void maybeEditorService()?.openInput({ kind: PLUGIN_LOG_EDITOR_KIND, @@ -477,6 +624,10 @@ export function handleRuntimeEvent(e: PluginEvent): boolean { function refreshOnProjectChange(): () => void { return useProject.subscribe((s, prev) => { if (s.project === prev.project) return; + statusRevision++; + globalStatus.clear(); + windowStatus.clear(); + usePluginStatusItems.setState({ items: new Map() }); refreshEventKinds(); refreshStatusItems(); refreshContextKeys(); @@ -484,6 +635,58 @@ function refreshOnProjectChange(): () => void { } export function startRuntimeUi(): () => void { - const disposers = [projectStatusItems(), trackWorkbenchEvents(), refreshOnProjectChange()]; + const stopParser = providePluginTestParser((config, chunk) => pluginsBackend().parseTestResults(config, chunk)); + const disposers = [ + startRunTargets(), + stopParser, + projectStatusItems(), + trackWorkbenchEvents(), + refreshOnProjectChange(), + ]; return () => disposers.reverse().forEach((d) => d()); } + +async function handleWindowRequest(e: Extract<PluginEvent, { type: "windowRequest" }>) { + const r = e.request; + try { + if (r.kind === "databaseConnect") { + if (!getAction("db.pluginConnect")) throw new Error(tx("Database service unavailable")); + await executeActionOrThrow("db.pluginConnect", r.id); + await pluginsBackend().answerCommand(e.requestId, { kind: "value", value: null }); + return; + } + if (r.kind === "contentTab") { + const raw = JSON.parse(JSON.stringify(r.tab), (_k, v: unknown) => (v === null ? undefined : v)) as object; + const data = contentTabData(e.pluginId, WASM_CONTENT_VIEW, raw, JSON.stringify(raw).length); + const editor = maybeEditorService(); + if (!editor) throw new Error("Editor unavailable"); + await editor.openInput({ + kind: PLUGIN_CONTENT_EDITOR_KIND, + key: contentTabKey(e.pluginId, WASM_CONTENT_VIEW, data.key), + data, + }); + await pluginsBackend().answerCommand(e.requestId, { kind: "value", value: null }); + return; + } + const finish = (status: number | null, stopped: boolean) => { + if (r.kind === "run") void pluginsBackend().finishOperation(r.operation, status, stopped); + else if (stopped) void pluginsBackend().closeTerminalSession(r.operation); + }; + if (r.kind === "run") { + if (!getAction("run.pluginStart")) throw new Error("Run service unavailable"); + await executeActionOrThrow("run.pluginStart", { config: r.config, debug: r.debug, onExit: finish }); + } else { + if (!getAction("terminal.pluginSession")) throw new Error("Terminal service unavailable"); + await executeActionOrThrow("terminal.pluginSession", { + ...r, + onExit: finish, + spawn: (cols: number, rows: number, output: import("@tauri-apps/api/core").Channel<string>) => + pluginsBackend().terminalSessionSpawn(r.operation, cols, rows, output), + }); + } + if (e.requestId) await pluginsBackend().answerCommand(e.requestId, { kind: "value", value: null }); + } catch (error) { + if (r.kind === "run") await pluginsBackend().finishOperation(r.operation, null, false); + if (e.requestId) await pluginsBackend().answerCommand(e.requestId, { kind: "failed", value: String(error) }); + } +} diff --git a/src/features/plugins/uiPoints.tsx b/src/features/plugins/uiPoints.tsx index 9e1f1a8cd..614161e52 100644 --- a/src/features/plugins/uiPoints.tsx +++ b/src/features/plugins/uiPoints.tsx @@ -22,6 +22,7 @@ import { isMacHost, registerExtensionBindings } from "@/platform/keymap"; import { addFileIconProvider, fileIconName, registerExternalIcons, type MenuItem } from "@/ui"; import { addContributedFileTemplate } from "@/workbench/contributions"; import { + addContributedSnippets, registerEditorKind, registerFileEditorProvider, registerCommitCheckProvider, @@ -70,6 +71,10 @@ import { projectSettings, type SettingsDecl } from "./settings"; interface MenuDecl { menu: + | "editor.title" + | "mainToolbar" + | "problems.context" + | "searchResults.context" | "editor.context" | "projectView.context" | "editorTab.context" @@ -87,6 +92,8 @@ interface MenuDecl { const CONTEXT_MENUS: Record<string, ContextMenuId> = { "editor.context": "editor", + "problems.context": "problems", + "searchResults.context": "searchResults", "projectView.context": "projectView", "editorTab.context": "editorTab", }; @@ -127,7 +134,11 @@ function flattenSingleGroups(items: MenuItem[]): MenuItem[] { } /** Plugin items of a context menu: their command's action with the menu's file as argument. */ -export function pluginMenuItems(menu: ContextMenuId, paths: readonly string[]): MenuItem[] { +export function pluginMenuItems( + menu: ContextMenuId, + paths: readonly string[], + position?: { line: number; column: number }, +): MenuItem[] { const out: MenuItem[] = []; const groups = new Map<string, MenuItem[]>(); for (const { pluginId, decl } of contextItems.values()) { @@ -138,7 +149,7 @@ export function pluginMenuItems(menu: ContextMenuId, paths: readonly string[]): addGrouped(out, groups, pluginId, decl.submenu, { kind: "action", action, - arg: menu === "editor" ? undefined : { path: paths[0], paths: [...paths] }, + arg: menu === "editor" ? undefined : { path: paths[0], paths: [...paths], ...(position ?? {}) }, }); } return flattenSingleGroups(out); @@ -165,14 +176,14 @@ function projectMenu(pluginId: string, decl: MenuDecl, plugin: PluginInfo): () = const title = decl.commandTitle ?? getAction(action)?.title ?? `${plugin.name}: ${decl.command}`; return registerMenuSlotItem("pluginTools", { action, label: title, verbatim: true }); } - if (decl.menu === "commit.message") { + if (decl.menu === "commit.message" || decl.menu === "editor.title" || decl.menu === "mainToolbar") { const action = pluginActionId(pluginId, decl.command); const label = getAction(action)?.title ?? `${plugin.name}: ${decl.command}`; - return registerMenuSlotItem("commitMessage", { + return registerMenuSlotItem(decl.menu === "commit.message" ? "commitMessage" : decl.menu, { action, label, verbatim: true, - icon: pluginIconName(pluginId, plugin.icon), + icon: getAction(action)?.icon ?? pluginIconName(pluginId, plugin.icon), when: decl.when ?? null, }); } @@ -487,6 +498,24 @@ export function defineViewUiPoints(): () => void { title: "Commit Checks", project: ({ pluginId, decl }) => projectCommitCheck(pluginId, decl), }), + defineUiPoint<{ + source: string; + language: string; + entries: { name: string; prefixes: string[]; body: string; description: string }[]; + }>("snippets", { + title: "Snippets", + project: ({ pluginId, decl }, { plugin }) => + addContributedSnippets( + decl.entries.map((s, i) => ({ + id: `plugin-snippet:${pluginId}:${decl.source}:${i}`, + pluginName: plugin.name, + language: decl.language, + prefixes: s.prefixes, + body: s.body, + description: s.description, + })), + ), + }), defineUiPoint<FileTemplateDecl>("fileTemplates", { title: "File Templates", project: ({ pluginId, decl }) => @@ -539,12 +568,16 @@ export function startViewUi(openInput: (input: ReturnType<typeof customEditorInp }), registerMenuGroup({ id: "plugins", - menus: ["editor", "projectView", "editorTab"], + menus: ["editor", "projectView", "editorTab", "problems", "searchResults"], order: 90, items: (menu, ctx) => { - const items = pluginMenuItems(menu, ctx.paths); + const items = pluginMenuItems( + menu, + ctx.paths, + ctx.line === undefined ? undefined : { line: ctx.line, column: ctx.column ?? 0 }, + ); const path = ctx.paths[0]; - if (menu !== "editor" && path && !ctx.dirs?.[0]) { + if ((menu === "projectView" || menu === "editorTab") && path && !ctx.dirs?.[0]) { const editors = [...openWith.values()].filter((e) => e.matches(path)); if (editors.length) items.push({ diff --git a/src/features/plugins/views/contentTab.ts b/src/features/plugins/views/contentTab.ts index 3bef3a463..124ea6b8d 100644 --- a/src/features/plugins/views/contentTab.ts +++ b/src/features/plugins/views/contentTab.ts @@ -4,6 +4,8 @@ import { pluginsBackend } from "../backend"; import { usePlugins } from "../store"; import { onViewMessage } from "./viewStore"; +export const WASM_CONTENT_VIEW = "@wasm-content"; + export const PLUGIN_CONTENT_EDITOR_KIND = "pluginContent"; const BUILTIN_ICONS = new Set<string>(ICON_NAMES); @@ -245,7 +247,15 @@ export function dispatchContentTabAction( if (senders) { for (const send of senders) send({ text: payload }); } - if (plugin.hasCode) { + if (data.view === WASM_CONTENT_VIEW) { + void pluginsBackend() + .contentTabAction(data.pluginId, { requestId, key: data.key, action, content }) + .catch((err: unknown) => { + if (!pendingRequests.delete(requestId)) return; + clearTimeout(timer); + reject(err instanceof Error ? err : new Error(String(err))); + }); + } else if (plugin.hasCode) { let offOnce = () => {}; offOnce = onViewMessage(data.pluginId, data.view, (raw) => { if (handleContentTabReplyText(data.pluginId, raw)) offOnce(); diff --git a/src/features/plugins/views/logEditor.tsx b/src/features/plugins/views/logEditor.tsx index d36c762a7..002cd3141 100644 --- a/src/features/plugins/views/logEditor.tsx +++ b/src/features/plugins/views/logEditor.tsx @@ -24,6 +24,7 @@ import { usePlugins } from "../store"; import { bindLogControls } from "./logActions"; import { boundedLog, type PluginLogEditorData } from "./logTab"; import { logTabPtyBackend, webPtyBackend } from "./ptyBackend"; +import { outputKey, usePluginOutput } from "./outputStore"; import { ptyBridge } from "./ptyBridge"; export const PLUGIN_LOG_EDITOR_KIND = "pluginLog"; @@ -50,7 +51,7 @@ const wheelZoom = EditorView.domEventHandlers({ export function PluginLogEditor({ input }: { input: EditorInput }) { const data = input.data as PluginLogEditorData | undefined; - if (!data || !data.program) { + if (!data || (!data.program && data.origin !== "output")) { return <EmptyState title={tx("Invalid log stream configuration")} />; } return <PluginLogEditorBody input={input} data={data} />; @@ -336,6 +337,16 @@ function PluginLogEditorBody({ input, data }: { input: EditorInput; data: Plugin useEffect(() => { const view = viewRef.current; if (!view) return; + if (data.origin === "output") { + const key = outputKey(data.pluginId, data.key ?? ""); + const update = () => { + const text = usePluginOutput.getState().channels.get(key) ?? ""; + view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text } }); + if (stickRef.current) view.scrollDOM.scrollTop = view.scrollDOM.scrollHeight; + }; + update(); + return usePluginOutput.subscribe(update); + } receivedRef.current = false; view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: tx("Waiting for output…") } }); stickRef.current = true; diff --git a/src/features/plugins/views/logTab.ts b/src/features/plugins/views/logTab.ts index 0ed1dc6c9..700584912 100644 --- a/src/features/plugins/views/logTab.ts +++ b/src/features/plugins/views/logTab.ts @@ -16,7 +16,7 @@ export interface PluginLogEditorData { args: string[]; toggles: PluginLogToggleSpec[]; /** Opened by the plugin's code (`views.open-log-tab`): the host starts what it approved for `key`. */ - origin?: "code"; + origin?: "code" | "output"; } const text = (v: unknown, max: number): v is string => diff --git a/src/features/plugins/views/outputStore.ts b/src/features/plugins/views/outputStore.ts new file mode 100644 index 000000000..80c33e75f --- /dev/null +++ b/src/features/plugins/views/outputStore.ts @@ -0,0 +1,13 @@ +import { create } from "zustand"; + +/** Snapshots of the host's bounded, read-only output channels. */ +export const usePluginOutput = create<{ channels: Map<string, string> }>(() => ({ channels: new Map() })); +export const outputKey = (pluginId: string, channel: string) => JSON.stringify([pluginId, channel]); +export function setPluginOutput(pluginId: string, channel: string, text: string | null) { + usePluginOutput.setState((s) => { + const channels = new Map(s.channels); + if (text === null) channels.delete(outputKey(pluginId, channel)); + else channels.set(outputKey(pluginId, channel), text); + return { channels }; + }); +} diff --git a/src/features/run/ConfigForm.tsx b/src/features/run/ConfigForm.tsx index 61989a47c..526dc760e 100644 --- a/src/features/run/ConfigForm.tsx +++ b/src/features/run/ConfigForm.tsx @@ -4,6 +4,7 @@ * Before launch list. */ import { isJvmApplication, parseJavaArgs } from "@/platform/javaLaunch"; +import { useRunTargets } from "@/platform/pluginRunTargets"; import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from "react"; import clsx from "clsx"; import { isTauri, type BeforeLaunchTask, type BuildPolicy, type RunConfig } from "@/platform/ipc"; @@ -45,7 +46,6 @@ import { import { gradleContainsModule } from "./gradleRun"; import { EnvDialog } from "./EnvDialog"; import { useBuildModel } from "@/platform/buildModel"; -import { withTarget } from "./remote"; import { COMPOUND, formParams, terminalApplies, withParams } from "./types"; import { TomcatSettings } from "./TomcatSettings"; import styles from "./Run.module.css"; @@ -243,7 +243,7 @@ function BrowseButton({ export function ConfigForm({ config, all, type, errors, template, onChange, modifyRequest }: ConfigFormProps) { const openByDefault = useWorkflowSettings((s) => s.run.showOnStart); const externalTools = useWorkflowSettings((s) => s.externalTools); - const runTargets = useWorkflowSettings((s) => s.runTargets); + const runTargets = useRunTargets(); const toolWindows = useContributions((s) => s.toolWindows); const tx = useTx(); const txTemplate = useTxTemplate(); @@ -383,6 +383,7 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi }); const targetOptions: SelectOption[] = [ + { value: "__default", label: "Default run target" }, { value: "", label: "Local machine" }, ...runTargets.map((t) => ({ value: t.id, label: t.name.trim() || t.id })), ]; @@ -446,9 +447,14 @@ export function ConfigForm({ config, all, type, errors, template, onChange, modi } > <Select - value={config.target ?? ""} + value={config.target ?? "__default"} options={targetOptions} - onChange={(target) => onChange(withTarget(config, target))} + onChange={(target) => { + if (target === "__default") { + const { target: _target, ...rest } = config; + onChange(rest); + } else onChange({ ...config, target }); + }} aria-label="Run on" /> </Field> diff --git a/src/features/run/EditConfigsDialog.tsx b/src/features/run/EditConfigsDialog.tsx index 311f251ae..3b80a514f 100644 --- a/src/features/run/EditConfigsDialog.tsx +++ b/src/features/run/EditConfigsDialog.tsx @@ -1,9 +1,10 @@ +/** Sidebar rows: type groups, their folders (first), then loose configurations; collapsed nodes hide their children. */ +import { useRunTargets } from "@/platform/pluginRunTargets"; import { useEffect, useMemo, useRef, useState, type DragEvent, type KeyboardEvent } from "react"; import clsx from "clsx"; import { executeAction } from "@/platform/actions"; import { call, commands, isTauri, type RunConfig, type RunImportSource } from "@/platform/ipc"; import { letterOf } from "@/platform/keymap"; -import { useWorkflowSettings } from "@/platform/workflowSettings"; import { Button, Dialog, Icon, IconButton, Input, T } from "@/ui"; import { notifications, @@ -49,7 +50,6 @@ export type TreeRow = | { type: "folder"; key: string; kind: string; name: string } | { type: "config"; key: string; config: RunConfig; depth: 1 | 2 }; -/** Sidebar rows: type groups, their folders (first), then loose configurations; collapsed nodes hide their children. */ export function treeRows( configs: readonly RunConfig[], extraFolders: ReadonlyArray<{ kind: string; name: string }> = [], @@ -133,7 +133,7 @@ function EditConfigsDialog() { const [dirty, setDirty] = useState(() => useRun.getState().editDirty); const [removedDetectedIds, setRemovedDetectedIds] = useState<string[]>([]); const [saving, setSaving] = useState(false); - const runTargets = useWorkflowSettings((s) => s.runTargets); + const runTargets = useRunTargets(); const [adding, setAdding] = useState<{ x: number; y: number } | null>(null); const [modifyRequest, setModifyRequest] = useState(0); const addRef = useRef<HTMLButtonElement>(null); diff --git a/src/features/run/index.ts b/src/features/run/index.ts index 86884df1b..49ac906b8 100644 --- a/src/features/run/index.ts +++ b/src/features/run/index.ts @@ -3,7 +3,7 @@ * test runner) and run/debug/stop actions. */ import { registerEditorContribution } from "@/editor/api"; -import { executeAction, getAction, registerActions } from "@/platform/actions"; +import { executeAction, executeActionOrThrow, getAction, registerActions } from "@/platform/actions"; import { getContext, setContext, useContextKeys } from "@/platform/context"; import { call, commands, onIdeEvent, type RunConfig } from "@/platform/ipc"; import { useProject } from "@/platform/project"; @@ -590,6 +590,28 @@ export function activate(): () => void { return loadConfigs(); }, }, + { + id: "run.pluginStart", + title: "Run Plugin Configuration", + category: "Run", + hidden: true, + when: "projectOpen", + run: async ({ arg }) => { + const a = arg as { + config: import("@/bindings").RunConfig; + debug: boolean; + onExit: (code: number | null, stopped: boolean) => void; + }; + if (a.debug) { + if (!getAction("debug.pluginStart")) throw new Error("Debugger unavailable"); + await executeActionOrThrow("debug.pluginStart", a); + return; + } + const session = await startRun(a.config, undefined, undefined, { onExit: a.onExit }); + if (!session || session.status === "failed") throw new Error("Run did not start"); + if (session.runId === null && session.status === "finished") a.onExit(session.exitCode, !!session.stopped); + }, + }, { id: "run.runConfig", title: "Run Configuration", diff --git a/src/features/run/remote.test.tsx b/src/features/run/remote.test.tsx index 52dcc896a..c87304481 100644 --- a/src/features/run/remote.test.tsx +++ b/src/features/run/remote.test.tsx @@ -41,6 +41,7 @@ import { executeAction, registerActions } from "@/platform/actions"; import { setContext } from "@/platform/context"; import { IpcError, type RunConfig } from "@/platform/ipc"; import { useProject } from "@/platform/project"; +import { usePluginRunTargets } from "@/platform/pluginRunTargets"; import { DEFAULT_RUN_TARGET, type RunTargetValid } from "@/platform/remoteSettings"; import { provideNotificationService, @@ -96,6 +97,7 @@ const fail = (message: string, kind = "permissionDenied") => ({ status: "error" const notify = vi.fn(); beforeEach(() => { + usePluginRunTargets.setState({ targets: [] }); resetRuns(); for (const f of Object.values(ipc)) f.mockClear(); confirm.mockClear(); @@ -148,13 +150,46 @@ describe("run targets", () => { <ConfigForm config={cfg("app")} all={[]} type={undefined} errors={{}} onChange={onChange} modifyRequest={0} />, ); const select = screen.getByRole("button", { name: "Run on" }); - expect(select.textContent).toContain("Local machine"); + expect(select.textContent).toContain("Default run target"); fireEvent.click(select); const popup = document.querySelector("[data-autofocus]")!; fireEvent.keyDown(popup, { key: "ArrowDown" }); + fireEvent.keyDown(popup, { key: "ArrowDown" }); fireEvent.keyDown(popup, { key: "Enter" }); expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ target: "box" })); }); + it("includes plugin targets labelled with their owner in the target selector", () => { + const id = "plugin-target:9:executionwrapper"; + usePluginRunTargets.setState({ + targets: [ + { + id, + pluginId: "execution", + pluginName: "Fixture", + prompt: false, + target: { + id: "wrapper", + name: "Execution", + program: "wrapper", + args: [], + workdirArgs: [], + containerRoot: null, + commands: [], + }, + }, + ], + }); + const onChange = vi.fn(); + render( + <ConfigForm config={cfg("app")} all={[]} type={undefined} errors={{}} onChange={onChange} modifyRequest={0} />, + ); + fireEvent.click(screen.getByRole("button", { name: "Run on" })); + const popup = document.querySelector("[data-autofocus]")!; + expect(popup.textContent).toContain("Execution (Fixture)"); + fireEvent.keyDown(popup, { key: "End" }); + fireEvent.keyDown(popup, { key: "Enter" }); + expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ target: id })); + }); }); describe("remote debugging", () => { diff --git a/src/features/run/remote.ts b/src/features/run/remote.ts index 513b4e17f..1ceb48da4 100644 --- a/src/features/run/remote.ts +++ b/src/features/run/remote.ts @@ -3,11 +3,13 @@ * SSH or in a Docker / Compose container. Labels, the debug and executor guards, * and the connection remedies of `runStart`. */ +import { withRunTarget } from "@/platform/runTargetSelection"; +import { useRunTargets, runTargets as allRunTargets, type RuntimeRunTarget } from "@/platform/pluginRunTargets"; import { executeAction } from "@/platform/actions"; import { tx, txTemplate } from "@/platform/i18n"; import { call, commands, IpcError, type RunConfig } from "@/platform/ipc"; -import { inContainer, type RunTargetValid, type SshConfigValid } from "@/platform/remoteSettings"; -import { useWorkflowSettings, workflowSettings } from "@/platform/workflowSettings"; +import { inContainer, type SshConfigValid } from "@/platform/remoteSettings"; +import { workflowSettings } from "@/platform/workflowSettings"; import { notifications } from "@/workbench/contributions"; import { confirm } from "@/workbench/messages"; import { promptSecret } from "./dialogs"; @@ -15,16 +17,17 @@ import { promptSecret } from "./dialogs"; /** The run target `config` runs on; undefined when it runs locally or the target is gone. */ export function targetOf( config: Pick<RunConfig, "target">, - targets: readonly RunTargetValid[] = workflowSettings().runTargets, -): RunTargetValid | undefined { + targets: readonly RuntimeRunTarget[] = allRunTargets(), +): RuntimeRunTarget | undefined { return config.target ? targets.find((t) => t.id === config.target) : undefined; } /** Display name of the target `config` runs on (its id when missing); null when local. */ export function targetName( config: Pick<RunConfig, "target">, - targets: readonly RunTargetValid[] = workflowSettings().runTargets, + targets: readonly RuntimeRunTarget[] = allRunTargets(), ): string | null { + if ("command" in config) config = withRunTarget(config as RunConfig); if (!config.target) return null; const t = targetOf(config, targets); return t ? t.name.trim() || t.id : config.target; @@ -33,7 +36,7 @@ export function targetName( /** `name @ target` for configurations on a run target, else the name. */ export function remoteLabel( config: Pick<RunConfig, "name" | "target">, - targets: readonly RunTargetValid[] = workflowSettings().runTargets, + targets: readonly RuntimeRunTarget[] = allRunTargets(), ): string { const t = targetName(config, targets); return t ? `${config.name} @ ${t}` : config.name; @@ -41,20 +44,20 @@ export function remoteLabel( /** `remoteLabel` that re-renders when the run targets change. */ export function useRemoteLabel(): (config: Pick<RunConfig, "name" | "target">) => string { - const targets = useWorkflowSettings((s) => s.runTargets); + const targets = useRunTargets(); return (config) => remoteLabel(config, targets); } /** `config` running on `target` (empty: this machine, the key is dropped). */ export function withTarget(config: RunConfig, target: string): RunConfig { const { target: _previous, ...rest } = config; - return target ? { ...rest, target } : rest; + return target || workflowSettings().run.defaultTarget ? { ...rest, target } : rest; } /** A Python configuration on a Docker / Compose target: debugged in its container (`debugInContainer`). */ export function containerDebuggable( config: RunConfig, - targets: readonly RunTargetValid[] = workflowSettings().runTargets, + targets: readonly RuntimeRunTarget[] = allRunTargets(), ): boolean { const target = targetOf(config, targets); return !!target && inContainer(target) && config.debugAdapter === "debugpy"; @@ -64,10 +67,8 @@ export function containerDebuggable( * Debugging on run targets isn't supported, except Python in containers: tells so * (offering Run) and returns true when `config` can't be debugged where it runs. */ -export function remoteDebugBlocked( - config: RunConfig, - targets: readonly RunTargetValid[] = workflowSettings().runTargets, -): boolean { +export function remoteDebugBlocked(config: RunConfig, targets: readonly RuntimeRunTarget[] = allRunTargets()): boolean { + config = withRunTarget(config); if (!config.target || containerDebuggable(config, targets)) return false; const target = targetName(config, targets) ?? config.target; const container = targetOf(config, targets); @@ -96,7 +97,7 @@ export function remoteDebugBlocked( */ export function containerExecutorBlocked( config: RunConfig, - targets: readonly RunTargetValid[] = workflowSettings().runTargets, + targets: readonly RuntimeRunTarget[] = allRunTargets(), ): boolean { const target = targetOf(config, targets); if (!target || !inContainer(target)) return false; @@ -132,7 +133,7 @@ export interface RemedyDeps { askSecret: (message: string, save: boolean) => Promise<{ secret: string; save: boolean } | null>; trustHost: (sshConfig: string) => Promise<unknown>; setSecret: (sshConfig: string, secret: string, save: boolean) => Promise<unknown>; - targets: () => readonly RunTargetValid[]; + targets: () => readonly RuntimeRunTarget[]; sshConfigs: () => readonly SshConfigValid[]; } @@ -147,7 +148,7 @@ const defaultDeps: RemedyDeps = { askSecret: (message, save) => promptSecret(message, save), trustHost: (id) => call(commands.sshTrustHost(id)), setSecret: (id, secret, save) => commands.sshSetSecret(id, secret, save), - targets: () => workflowSettings().runTargets, + targets: () => allRunTargets(), sshConfigs: () => workflowSettings().ssh.configs, }; diff --git a/src/features/run/runtimes.test.ts b/src/features/run/runtimes.test.ts index 44ef2f6d1..8aa033d0a 100644 --- a/src/features/run/runtimes.test.ts +++ b/src/features/run/runtimes.test.ts @@ -13,9 +13,11 @@ vi.mock("@/platform/ipc", () => ({ sdkLocateTools: async () => ({ compileDir: "/tmp/run", tools: [] }), }, })); +import { useProject } from "@/platform/project"; import { clearRuntimeCache, rootMarkers, toolEnv } from "./runtimes"; beforeEach(() => { + useProject.setState({ settings: null }); entries.clear(); pythonNear.mockReset().mockResolvedValue("/env/python"); clearRuntimeCache(); @@ -41,3 +43,14 @@ it("the production Python runtime query includes the source directory", async () expect(pythonNear).toHaveBeenCalledWith("/repo/apps/python/src"); expect(env.python).toBe("/env/python"); }); + +it("target PHP and Node runs do not require locally installed interpreters", async () => { + useProject.setState({ + settings: { languages: { php: { runTarget: "plugin-target:php" }, node: { runTarget: "plugin-target:node" } } }, + }); + const env = await toolEnv(["php", "node", "npx", "tsx"], "/repo/main.php", "/repo", false, true); + expect(env.php).toBe("php"); + expect(env.tool("node")).toBeUndefined(); + expect(env.tool("npx")).toBeUndefined(); + expect(env.tool("tsx")).toBeUndefined(); +}); diff --git a/src/features/run/runtimes.ts b/src/features/run/runtimes.ts index be2deb15b..3ee88a2dd 100644 --- a/src/features/run/runtimes.ts +++ b/src/features/run/runtimes.ts @@ -4,6 +4,7 @@ * minute per directory. Without that command (an older backend) Node.js and * Rust come from SDK discovery and everything else resolves at launch. */ +import { withRunTarget } from "@/platform/runTargetSelection"; import { call, commands, isTauri } from "@/platform/ipc"; import { isWindows } from "./jvmTypes"; import type { LocatedTool, ToolEnv } from "./producers"; @@ -121,10 +122,23 @@ export async function toolEnv( python: boolean, php = false, ): Promise<ToolEnv> { + const targetFor = (command: string) => + withRunTarget({ command, kind: command } as import("@/bindings").RunConfig).target; + const targetTools = new Set( + names.filter( + (n) => + ["php", "node", "npm", "npx", "pnpm", "yarn", "bun", "tsx", "ts-node"].includes(n) && + targetFor(n === "tsx" || n === "ts-node" ? "node" : n), + ), + ); + const phpTarget = php && targetFor("php"); const [tools, py, phpInterpreter, markers] = await Promise.all([ - locateTools(names, file), + locateTools( + names.filter((n) => !targetTools.has(n)), + file, + ), python ? projectPython(file) : Promise.resolve(null), - php ? projectPhp() : Promise.resolve(null), + phpTarget ? Promise.resolve("php") : php ? projectPhp() : Promise.resolve(null), names.includes("deno") || names.includes("bun") ? rootMarkers(root, file) : Promise.resolve({ deno: false, bun: false }), diff --git a/src/features/run/store.ts b/src/features/run/store.ts index e2f87a2aa..c66968919 100644 --- a/src/features/run/store.ts +++ b/src/features/run/store.ts @@ -3,6 +3,7 @@ * (one Run tool window tab each). Output streams through a Channel into a * per-session `ConsoleModel`, batched per animation frame. */ +import { withRunTarget } from "@/platform/runTargetSelection"; import { create } from "zustand"; import { Channel } from "@tauri-apps/api/core"; import { @@ -42,6 +43,7 @@ import { type TestEvent, type TestOutputParser, } from "./testing"; +import { PluginTestOutput } from "./testing/plugin"; import { GoTestOutput } from "./testing/go"; import { moveToReportedClasses } from "./testing/jvm"; import { RubyTestOutput } from "./testing/ruby"; @@ -96,6 +98,7 @@ interface SessionRuntime { console: ConsoleModel; tests: TestResults | null; parser: TestOutputParser | null; + pluginTests: PluginTestOutput | null; /** Machine-readable stdout (`go test -json`, `--teamcity`, the Ruby reporter): fed to the parser, shown decoded. */ stdout: TestStdout | null; queue: OutputChunk[]; @@ -616,7 +619,15 @@ export function applyConsoleSettings() { function newRuntime(config: RunConfig): SessionRuntime { const tests = config.isTest ? new TestResults() : null; - const parser = config.isTest ? createTestParser(config) : null; + const builtIn = config.isTest ? createTestParser(config) : null; + const pluginTests = + tests && builtIn && config.params?.framework === "plugin" + ? new PluginTestOutput(config.id, builtIn, (events) => { + tests.apply(events); + for (const [id, rt] of runtimes) if (rt.tests === tests) bump(id); + }) + : null; + const parser = pluginTests ? null : builtIn; const feed = (line: string) => { if (tests && parser) tests.apply(parser.feed(line)); }; @@ -635,9 +646,11 @@ function newRuntime(config: RunConfig): SessionRuntime { stdout, tests, parser, + pluginTests, console: new ConsoleModel({ maxChars: consoleCharLimit(), onLine: (text, kind) => { + if (pluginTests && kind !== "system") pluginTests.feed(text); if (parser && tests && kind !== "system" && !(kind === "stdout" && stdout)) tests.apply(parser.feed(text)); if (kind !== "system") for (const l of runtime.lineListeners) l(text); }, @@ -712,6 +725,7 @@ export async function startRun( chain?: readonly string[]; } = {}, ): Promise<RunSession | null> { + config = withRunTarget(config); if (!options.launch && isCMakeBuild(config)) { await buildCMake(config.id, options.show !== false); return null; @@ -1005,8 +1019,10 @@ function finishSession(sessionId: string, code: number | null) { if (text) rt.console.append({ kind: "stdout", text }); } rt.console.flush(); - if (rt.tests && rt.parser) { - rt.tests.apply(rt.parser.end?.() ?? []); + await rt.pluginTests?.end(); + if (runtimes.get(sessionId) !== rt) return; + if (rt.tests && (rt.parser || rt.pluginTests)) { + rt.tests.apply(rt.parser?.end?.() ?? []); // Surefire / Gradle print only failures and class totals: per-test results come from the XML reports. if (frameworkFor(session.config) === "jvm") { const reports = await jvmReports(session); diff --git a/src/features/run/testing/plugin.test.ts b/src/features/run/testing/plugin.test.ts new file mode 100644 index 000000000..fcf5375ef --- /dev/null +++ b/src/features/run/testing/plugin.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from "vitest"; +import { providePluginTestParser } from "@/workbench/contributions"; +import { PluginTestEvents, PluginTestOutput } from "./plugin"; +import { TestResults } from "./results"; + +describe("plugin test events", () => { + it("maps stable ids, nested paths, failures, output and UTF-16 locations to the existing tree", () => { + const mapping = new PluginTestEvents(); + const location = { path: "/project/test.php", range: { start: { line: 2, col: 4 }, end: { line: 2, col: 6 } } }; + const events = mapping.map([ + { type: "suiteStarted", id: "suite", parent: null, name: "Suite", location: null }, + { type: "testStarted", id: "t", parent: "suite", name: "case", location }, + { type: "failed", id: "t", durationMs: 7, message: "mismatch", expected: "one", actual: "two", location: null }, + ]); + expect(events).toContainEqual({ + type: "location", + path: ["Suite", "case"], + location: { path: location.path, line: 3, col: 5, resolve: "path" }, + }); + const tree = new TestResults(); + tree.apply(events); + expect(tree.counts().failed).toBe(1); + expect(mapping.map([{ type: "output", id: "t", text: "later" }])).toEqual([ + { type: "output", path: ["Suite", "case"], line: "later" }, + ]); + }); + it("rejects an invalid parent without corrupting the last valid mapping", () => { + const mapping = new PluginTestEvents(); + expect(() => + mapping.map([{ type: "testStarted", id: "x", parent: "missing", name: "bad", location: null }]), + ).toThrow(); + expect(() => mapping.map([{ type: "passed", id: "x", durationMs: null }])).toThrow(); + }); + it("serializes bounded batches and falls back when a parser fails", async () => { + const chunks: string[] = []; + const dispose = providePluginTestParser(async (_id, chunk) => { + chunks.push(chunk); + return null; + }); + const feed = vi.fn(() => [{ type: "start" as const, path: ["fallback"] }]); + const apply = vi.fn(); + const parser = new PluginTestOutput("plugin:fixture:tests", { feed }, apply); + parser.feed("x".repeat(40000)); + await parser.end(); + expect(chunks[0]!.length).toBeLessThanOrEqual(16000); + expect(apply).toHaveBeenCalledWith([{ type: "start", path: ["fallback"] }]); + dispose(); + }); +}); diff --git a/src/features/run/testing/plugin.ts b/src/features/run/testing/plugin.ts new file mode 100644 index 000000000..f11c23aeb --- /dev/null +++ b/src/features/run/testing/plugin.ts @@ -0,0 +1,146 @@ +import { tx } from "@/platform/i18n"; +import type { PluginTestEvent } from "@/bindings"; +import { parsePluginTests } from "@/workbench/contributions"; +import type { TestEvent, TestOutputParser } from "./types"; + +/** IDs stay private to a run; the existing tree uses display-name paths. */ +export class PluginTestEvents { + private paths = new Map<string, string[]>(); + private names = new Map<string, string>(); + map(events: readonly PluginTestEvent[]): TestEvent[] { + const paths = new Map(this.paths); + const names = new Map(this.names); + const out: TestEvent[] = []; + for (const event of events) { + if (event.type === "other") continue; + if (event.type === "suiteStarted" || event.type === "testStarted") { + if (!event.id || !event.name || (event.parent && !paths.has(event.parent))) + throw new Error("Invalid test parent"); + const path = [...(event.parent ? paths.get(event.parent)! : []), event.name]; + if (path.length > 128 || paths.size > 100000) throw new Error("Test tree too large"); + let key = JSON.stringify(path); + if (names.has(key) && names.get(key) !== event.id) { + path[path.length - 1] += ` [${event.id}]`; + key = JSON.stringify(path); + } + names.set(key, event.id); + paths.set(event.id, path); + out.push({ type: "start", path }); + if (event.location) + out.push({ + type: "location", + path, + location: { + path: event.location.path, + line: event.location.range.start.line + 1, + col: event.location.range.start.col + 1, + resolve: "path", + }, + }); + continue; + } + const path = paths.get(event.id); + if (!path) throw new Error("Unknown test id"); + if (event.type === "output") out.push({ type: "output", path, line: event.text }); + else if (event.type === "skipped") { + out.push({ type: "result", path, status: "skipped" }); + if (event.reason) out.push({ type: "output", path, line: event.reason }); + } else { + out.push({ + type: "result", + path, + status: event.type === "failed" ? "failed" : "passed", + durationMs: event.durationMs ?? undefined, + }); + if (event.type === "failed") { + out.push({ type: "output", path, line: event.message }); + if (event.expected !== null) out.push({ type: "output", path, line: tx`Expected: ${event.expected}` }); + if (event.actual !== null) out.push({ type: "output", path, line: tx`Actual: ${event.actual}` }); + if (event.location) + out.push({ + type: "location", + path, + location: { + path: event.location.path, + line: event.location.range.start.line + 1, + col: event.location.range.start.col + 1, + resolve: "path", + }, + }); + } + } + } + this.paths = paths; + this.names = names; + return out; + } +} + +/** Sequential, bounded batches; a parser failure switches this run to built-in parsing. */ +export class PluginTestOutput { + private readonly events = new PluginTestEvents(); + private pending = Promise.resolve(); + private pendingChars = 0; + private buffer = ""; + private scheduled = false; + private failed = false; + private fallbackEvents: TestEvent[] = []; + constructor( + private readonly config: string, + private readonly fallback: TestOutputParser, + private readonly apply: (events: TestEvent[]) => void, + ) {} + feed(line: string) { + const fallback = this.fallback.feed(line); + if (this.failed) { + this.apply(fallback); + return; + } + this.fallbackEvents.push(...fallback); + if (this.fallbackEvents.length > 10000) this.fallbackEvents.splice(0, this.fallbackEvents.length - 10000); + this.buffer += `${line}\n`; + if (!this.scheduled) { + this.scheduled = true; + queueMicrotask(() => { + this.scheduled = false; + this.flush(); + }); + } + } + private flush() { + while (this.buffer.length) { + let end = Math.min(this.buffer.length, 16000); + const last = this.buffer.charCodeAt(end - 1); + if (last >= 0xd800 && last <= 0xdbff) end--; + const chunk = this.buffer.slice(0, end); + this.buffer = this.buffer.slice(chunk.length); + this.pendingChars += chunk.length; + if (this.pendingChars > 1 << 20) { + this.failed = true; + this.buffer = ""; + this.apply(this.fallbackEvents); + this.fallbackEvents = []; + break; + } + this.pending = this.pending.then(async () => { + this.pendingChars -= chunk.length; + if (this.failed) return; + try { + const events = await parsePluginTests(this.config, chunk); + if (events === null) throw new Error("Parser unavailable"); + this.apply(this.events.map(events)); + } catch { + this.failed = true; + this.apply(this.fallbackEvents); + this.fallbackEvents = []; + } + }); + } + } + async end() { + this.flush(); + await this.pending; + if (this.failed) this.apply(this.fallback.end?.() ?? []); + this.fallbackEvents = []; + } +} diff --git a/src/features/search/find/FindToolWindow.tsx b/src/features/search/find/FindToolWindow.tsx index 90066080e..cfd11a405 100644 --- a/src/features/search/find/FindToolWindow.tsx +++ b/src/features/search/find/FindToolWindow.tsx @@ -1,3 +1,4 @@ +import { menuGroupItems } from "@/workbench/contributions"; import { useMemo, useRef, useState, type KeyboardEvent } from "react"; import clsx from "clsx"; import { create } from "zustand"; @@ -459,6 +460,9 @@ function FindTabView({ tab }: { tab: FindTab }) { const groups = [...sel].flatMap((k) => (byKey.get(k) ? nodeGroups(byKey.get(k)!) : [])); const excludedAll = groups.length > 0 && groups.every((g) => groupState(g, marks) === "excluded"); showContextMenu(e, [ + ...(_n?.kind === "match" + ? menuGroupItems("searchResults", { paths: [_n.path], line: _n.line, column: _n.col }) + : []), { id: "exclude", label: excludedAll ? "Include" : "Exclude", diff --git a/src/features/settings/__tests__/languages.test.ts b/src/features/settings/__tests__/languages.test.ts index 76e209a40..221727ea1 100644 --- a/src/features/settings/__tests__/languages.test.ts +++ b/src/features/settings/__tests__/languages.test.ts @@ -18,6 +18,9 @@ import { validateSettings } from "../model/schema"; describe("languages settings", () => { it("defaults and normalises malformed values", () => { expect(validateLanguages(undefined)).toEqual(DEFAULT_LANGUAGES); + expect(validateLanguages({ pluginFormatters: { sql: "example.formatter", text: 42 } }).pluginFormatters).toEqual({ + sql: "example.formatter", + }); const v = validateLanguages({ python: { interpreter: " /usr/bin/python3 ", activateVirtualenv: "yes" }, node: { packageManager: "pip" }, @@ -78,6 +81,7 @@ describe("languages settings", () => { it("defaults the PHP language level to Auto and keeps only known levels", () => { expect(validateSettings({}).languages.php).toEqual({ + runTarget: "", interpreter: "", languageLevel: "", includePaths: [], @@ -302,6 +306,7 @@ describe("language pages", () => { "Ruby", "Rust", "TypeScript", + "Plugin Formatters", ]); }); diff --git a/src/features/settings/languages/LanguagePages.tsx b/src/features/settings/languages/LanguagePages.tsx index 44d9cff15..ed0ced3d1 100644 --- a/src/features/settings/languages/LanguagePages.tsx +++ b/src/features/settings/languages/LanguagePages.tsx @@ -3,6 +3,8 @@ * Build Tools › Maven: the toolchain each language's server and run configurations * use (applied by `ide-sdk` when language servers start and when run configurations load). */ +import { useRunTargets } from "@/platform/pluginRunTargets"; +import { tx } from "@/platform/i18n"; import { useEffect, useId, useMemo, useState } from "react"; import { call, commands, type Sdk } from "@/platform/ipc"; import { IconButton, Input, type SelectOption, T } from "@/ui"; @@ -73,6 +75,23 @@ function SdkRow({ // --------------------------------------------------------------------------- // Node.js +function RunTargetRow({ language }: { language: "php" | "node" }) { + const [target, setTarget] = useSetting<string>(`languages.${language}.runTarget`); + const targets = useRunTargets(); + const options = [ + { value: "", label: "Default run target" }, + { value: "local", label: "Local machine" }, + ...targets.map((t) => ({ value: t.id, label: tx`Run in target ${t.name}` })), + ]; + if (target && target !== "local" && !targets.some((t) => t.id === target)) + options.push({ value: target, label: tx`Run in target ${target}` }); + return ( + <Row label="Run in target"> + <Select value={target ?? ""} options={options} onChange={setTarget} aria-label="Run in target" /> + </Row> + ); +} + export function NodePage() { const [pm, setPm] = useSetting<string>("languages.node.packageManager"); const { list } = useDetected("packageManager"); @@ -103,6 +122,7 @@ export function NodePage() { aria-label="Node interpreter" hint="Runs npm scripts and Node-based language servers (vtsls, eslint, pyright…)." /> + <RunTargetRow language="node" /> <Row label="Package manager" hint="Used by the npm run configurations detected from package.json scripts."> <Select value={pm} options={pmOptions} onChange={setPm} aria-label="Package manager" /> </Row> @@ -212,6 +232,7 @@ export function PhpPage() { /> </Section> <Section title="CLI Interpreter"> + <RunTargetRow language="php" /> <SdkRow k="languages.php.interpreter" label="PHP interpreter" diff --git a/src/features/settings/languages/PluginFormattersPage.tsx b/src/features/settings/languages/PluginFormattersPage.tsx new file mode 100644 index 000000000..d257779d3 --- /dev/null +++ b/src/features/settings/languages/PluginFormattersPage.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; +import { call, commands, type FormatterChoice } from "@/platform/ipc"; +import { tx } from "@/platform/i18n"; +import { Row, Section, Select, Toggle } from "../components/Form"; +import { useSetting } from "../components/context"; +export function PluginFormattersPage() { + const [known, setKnown] = useState<string[]>([]); + const [choices, setChoices] = useState<FormatterChoice[]>([]); + const [selected, setSelected] = useSetting<Record<string, string>>("languages.pluginFormatters"); + const [save, setSave] = useSetting<boolean>("editor.formatOnSave"); + useEffect(() => { + let active = true; + void call(commands.pluginFormatters()) + .then((v) => { + if (active) setChoices(v); + }) + .catch(() => {}); + return () => { + active = false; + }; + }, []); + useEffect(() => { + void commands + .languages() + .then((list) => setKnown(list.map((l) => l.id))) + .catch(() => {}); + }, []); + const languages = [ + ...new Set([...known, ...Object.keys(selected ?? {}), ...choices.flatMap((c) => c.languages)]), + ].filter( + (l) => l !== "*" && (selected?.[l] || choices.some((c) => c.languages.includes(l) || c.languages.includes("*"))), + ); + return ( + <Section title="Plugin Formatters"> + {languages.map((language) => ( + <Row key={language} label={language}> + <Select + aria-label={language} + value={selected?.[language] ?? ""} + onChange={(value) => setSelected({ ...selected, [language]: value })} + options={[ + { value: "", label: tx("Automatic (language server first)") }, + ...choices + .filter((c) => c.languages.includes(language) || c.languages.includes("*")) + .map((c) => ({ value: c.id, label: c.name })), + ...(selected?.[language] && !choices.some((c) => c.id === selected[language]) + ? [{ value: selected[language]!, label: selected[language]! }] + : []), + ]} + /> + </Row> + ))} + <Toggle label="Format on save" checked={save} onChange={setSave} /> + </Section> + ); +} diff --git a/src/features/settings/pages/languages.tsx b/src/features/settings/pages/languages.tsx index c456a7cc9..bec8ef533 100644 --- a/src/features/settings/pages/languages.tsx +++ b/src/features/settings/pages/languages.tsx @@ -1,3 +1,4 @@ +import { PluginFormattersPage } from "../languages/PluginFormattersPage"; /** * PyCharm's top-level "Python" group (Interpreter), "Project Structure", and * Languages & Frameworks › Go / Java / Markdown / Node.js / PHP / Rust / TypeScript. Pages live in @@ -15,6 +16,15 @@ import type { PageSpec } from "./spec"; export const LANGUAGES_GROUP = "Languages & Frameworks"; export const LANGUAGE_PAGES: PageSpec[] = [ + { + id: "languages.pluginFormatters", + title: "Plugin Formatters", + group: LANGUAGES_GROUP, + projectScope: true, + order: 450, + keywords: ["format", "plugins"], + component: PluginFormattersPage, + }, { id: "python.interpreter", projectScope: true, diff --git a/src/features/settings/pages/runTargets.tsx b/src/features/settings/pages/runTargets.tsx index fd1a2c2b6..b81efefa5 100644 --- a/src/features/settings/pages/runTargets.tsx +++ b/src/features/settings/pages/runTargets.tsx @@ -3,6 +3,7 @@ * containers run configurations can run on (`RunConfig.target`). Consumed by * `ide-run`, `ide-remote` and the run feature. */ +import { usePluginRunTargets } from "@/platform/pluginRunTargets"; import { useCallback, useEffect, useId, useState } from "react"; import clsx from "clsx"; import { call, commands, isTauri, type DockerConnection, type RemoteEntry } from "@/platform/ipc"; @@ -414,8 +415,17 @@ function ContainerFields({ target, update }: { target: RunTargetValid; update: ( } function RunTargetsEditor() { + const plugins = usePluginRunTargets((s) => s.targets); + const [defaultTarget, setDefaultTarget] = useSetting<string>("run.defaultTarget"); const [raw, setTargets] = useSetting<RunTargetValid[] | undefined>("runTargets"); const targets = raw ?? []; + const allTargets = [...targets, ...plugins.map((t) => ({ id: t.id, name: `${t.target.name} (${t.pluginName})` }))]; + const defaultOptions: SelectOption[] = [ + { value: "", label: "Local machine" }, + ...allTargets.map((t) => ({ value: t.id, label: t.name })), + ]; + if (defaultTarget && !allTargets.some((t) => t.id === defaultTarget)) + defaultOptions.push({ value: defaultTarget, label: tx`${defaultTarget} (missing)` }); const sshConfigs = useSshConfigs(); const [selected, setSelected] = useState<string | null>(targets[0]?.id ?? null); const current = targets.find((t) => t.id === selected) ?? null; @@ -478,11 +488,19 @@ function RunTargetsEditor() { /> </div> <div className={styles.toolList} role="listbox" aria-label={tx("Run targets")}> - {targets.length === 0 && ( + {targets.length === 0 && plugins.length === 0 && ( <div className={clsx(styles.toolRow, styles.empty)}> <T>No run targets. Click + to add one.</T> </div> )} + {plugins.map((t) => ( + <div key={t.id} className={styles.toolRow} role="option" aria-selected={false}> + <span className={styles.toolName}>{t.target.name}</span> + <span className={styles.toolGroup}> + {t.pluginName} · <T>Read-only</T> + </span> + </div> + ))} {targets.map((t) => ( <div key={t.id} @@ -504,6 +522,16 @@ function RunTargetsEditor() { ))} </div> </div> + <Section> + <Row label="Default run target"> + <Select + value={defaultTarget ?? ""} + onChange={setDefaultTarget} + aria-label="Default run target" + options={defaultOptions} + /> + </Row> + </Section> {current && ( <Section title="Target Settings"> <TextField label="Name" value={current.name} onChange={(name) => update({ name })} /> diff --git a/src/features/templates/__tests__/snippets.test.ts b/src/features/templates/__tests__/snippets.test.ts new file mode 100644 index 000000000..74ed379ac --- /dev/null +++ b/src/features/templates/__tests__/snippets.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { addContributedSnippets } from "@/workbench/contributions"; +import { liveTemplates, saveTemplate, removeTemplate } from "../live/store"; +import { snippetTemplates } from "../live/snippets"; +import { expandTemplate } from "../live/model"; +const snippet = { + id: "plugin-snippet:p:s:0", + pluginName: "Plugin", + language: "sql", + prefixes: ["sel", "select"], + body: "${2:table} ${1:column} $1 $0 \\$", + description: "Select rows", +}; +describe("plugin snippets", () => { + it("preserves defaults, linked stops, numeric tab order and escaped dollars", () => { + const ts = snippetTemplates(snippet); + expect(ts.map((t) => t.abbrev)).toEqual(["sel", "select"]); + expect(ts[0]!.variables?.map((v) => v.name)).toEqual(["P1", "P2"]); + const expanded = expandTemplate(ts[0]!); + expect(expanded.preview).toBe("table column column $"); + expect(expanded.snippet).toBe("${2:table} ${1:column} ${1:column} ${0} $"); + }); + it("withdraws read-only plugin entries without persisting customizations", () => { + const off = addContributedSnippets([snippet]); + const t = liveTemplates().find((t) => t.id.startsWith(snippet.id))!; + expect(t.pluginOrigin).toBe("Plugin"); + saveTemplate({ ...t, body: "changed" }); + removeTemplate(t.id); + expect(liveTemplates().find((x) => x.id === t.id)?.body).toBe(t.body); + off(); + expect(liveTemplates().some((t) => t.id.startsWith(snippet.id))).toBe(false); + }); +}); diff --git a/src/features/templates/__tests__/views.test.tsx b/src/features/templates/__tests__/views.test.tsx index ce63f2169..3a5fbc444 100644 --- a/src/features/templates/__tests__/views.test.tsx +++ b/src/features/templates/__tests__/views.test.tsx @@ -1,3 +1,4 @@ +import { addContributedSnippets } from "@/workbench/contributions"; import { describe, expect, it } from "vitest"; import { act, fireEvent, render, screen } from "@testing-library/react"; import { LiveTemplatesPage, groupTemplates } from "../settings/LiveTemplatesPage"; @@ -172,3 +173,24 @@ describe("dialogs", () => { expect(picked).toBe("t"); }); }); + +it("shows plugin snippets as read-only live templates and withdraws them with the plugin", () => { + const off = addContributedSnippets([ + { + id: "plugin-snippet:test:file:0", + pluginName: "SQL plugin", + language: "sql", + prefixes: ["selectrows"], + body: "SELECT ${1:column}$0", + description: "Select rows", + }, + ]); + const view = render(<LiveTemplatesPage />); + fireEvent.click(screen.getByText("selectrows")); + expect(screen.getByLabelText("Description")).toBeDisabled(); + expect(screen.getByRole("button", { name: "Remove Template" })).toBeDisabled(); + expect(screen.getByLabelText("Enable selectrows")).toBeDisabled(); + act(() => off()); + expect(screen.queryByText("selectrows")).toBeNull(); + view.unmount(); +}); diff --git a/src/features/templates/live/model.ts b/src/features/templates/live/model.ts index f947a0ac3..04d8b2539 100644 --- a/src/features/templates/live/model.ts +++ b/src/features/templates/live/model.ts @@ -21,6 +21,8 @@ export interface TemplateVariable { } export interface LiveTemplate { + /** Read-only contribution, withdrawn with its plugin. */ + pluginOrigin?: string; id: string; /** Settings grouping, e.g. "Rust", "Surround". */ group: string; diff --git a/src/features/templates/live/snippets.ts b/src/features/templates/live/snippets.ts new file mode 100644 index 000000000..324dbedef --- /dev/null +++ b/src/features/templates/live/snippets.ts @@ -0,0 +1,41 @@ +/** Numbered snippet placeholders translated into the existing linked live-template fields. */ +import type { ContributedSnippet } from "@/workbench/contributions"; +import type { LiveTemplate, TemplateVariable } from "./model"; +export function snippetTemplates(s: ContributedSnippet): LiveTemplate[] { + const variables = new Map<number, TemplateVariable>(); + let body = ""; + for (let i = 0; i < s.body.length;) { + if (s.body[i] === "\\" && /[$}\\]/.test(s.body[i + 1] ?? "")) { + body += s.body[i + 1] === "$" ? "$$" : s.body[i + 1]; + i += 2; + continue; + } + const m = /^\$(?:(\d+)|\{(\d+)(?::((?:\\.|[^}])*))?\})/.exec(s.body.slice(i)); + if (m) { + const n = Number(m[1] ?? m[2]); + if (n === 0) body += "$END$"; + else { + const name = `P${n}`; + const value = m[3]?.replace(/\\([$}\\])/g, "$1"); + if (!variables.has(n) || value !== undefined) + variables.set(n, { name, defaultValue: value === undefined ? undefined : JSON.stringify(value) }); + body += `$${name}$`; + } + i += m[0].length; + } else { + body += s.body[i] === "$" ? "$$" : s.body[i]; + i++; + } + } + return s.prefixes.map((abbrev, i) => ({ + id: `${s.id}:${i}`, + group: s.pluginName, + pluginOrigin: s.pluginName, + abbrev, + description: s.description, + body, + contexts: [s.language], + scope: "any", + variables: [...variables].sort(([a], [b]) => a - b).map(([, v]) => v), + })); +} diff --git a/src/features/templates/live/store.ts b/src/features/templates/live/store.ts index e048d919a..21ce2eb93 100644 --- a/src/features/templates/live/store.ts +++ b/src/features/templates/live/store.ts @@ -1,3 +1,5 @@ +import { useContributedSnippets } from "@/workbench/contributions"; +import { snippetTemplates } from "./snippets"; /** * Effective live templates: built-ins with the user's edits applied plus * user-defined templates, persisted globally in localStorage. @@ -21,7 +23,10 @@ export function mergeTemplates(builtins: readonly LiveTemplate[], c: LiveTemplat const deleted = new Set(c.deleted); const out = builtins.filter((t) => !deleted.has(t.id)).map((t) => c.templates[t.id] ?? t); for (const t of Object.values(c.templates)) if (!BUILTIN_IDS.has(t.id) && !deleted.has(t.id)) out.push(t); - return out; + return [ + ...out.filter((t) => !t.id.startsWith("plugin-snippet:")), + ...useContributedSnippets.getState().snippets.flatMap(snippetTemplates), + ]; } interface State { @@ -46,11 +51,13 @@ export function liveTemplates(): LiveTemplate[] { } export function saveTemplate(t: LiveTemplate) { + if (t.pluginOrigin) return; const c = useLiveTemplates.getState().custom; commit({ ...c, templates: { ...c.templates, [t.id]: t }, deleted: c.deleted.filter((d) => d !== t.id) }); } export function removeTemplate(id: string) { + if (id.startsWith("plugin-snippet:")) return; const c = useLiveTemplates.getState().custom; const templates = { ...c.templates }; delete templates[id]; @@ -102,3 +109,7 @@ export function reloadLiveTemplates() { useLiveTemplates.setState({ custom, templates: mergeTemplates(BUILTIN_TEMPLATES, custom) }); useLiveTemplateOptions.setState(loadJson(OPTIONS_KEY, { expandWithTab: true, showInCompletion: true })); } + +useContributedSnippets.subscribe(() => + useLiveTemplates.setState((s) => ({ templates: mergeTemplates(BUILTIN_TEMPLATES, s.custom) })), +); diff --git a/src/features/templates/settings/LiveTemplatesPage.tsx b/src/features/templates/settings/LiveTemplatesPage.tsx index 62a763a63..8b6cf95d8 100644 --- a/src/features/templates/settings/LiveTemplatesPage.tsx +++ b/src/features/templates/settings/LiveTemplatesPage.tsx @@ -73,7 +73,7 @@ export function LiveTemplatesPage() { icon="minus" size="small" title="Remove Template" - disabled={!selected} + disabled={!selected || !!selected.pluginOrigin} onClick={() => { if (!selected) return; removeTemplate(selected.id); @@ -105,6 +105,7 @@ export function LiveTemplatesPage() { onClick={() => setSelectedId(t.id)} > <Checkbox + disabled={!!t.pluginOrigin} checked={t.enabled !== false} onChange={(v) => saveTemplate({ ...t, enabled: v })} aria-label={tx`Enable ${t.abbrev}`} @@ -123,7 +124,9 @@ export function LiveTemplatesPage() { </div> <div className={styles.editor}> {selected ? ( - <TemplateEditor key={selected.id} template={selected} /> + <fieldset disabled={!!selected.pluginOrigin} className={styles.readonly}> + <TemplateEditor key={selected.id} template={selected} /> + </fieldset> ) : ( <div className={styles.empty}> <T>Select a template to edit it</T> diff --git a/src/features/templates/settings/Settings.module.css b/src/features/templates/settings/Settings.module.css index 9146bfaea..1cf90abff 100644 --- a/src/features/templates/settings/Settings.module.css +++ b/src/features/templates/settings/Settings.module.css @@ -220,3 +220,15 @@ background: var(--bg-selected); color: var(--fg-strong); } + +.readonly { + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; + gap: 8px; + border: 0; + padding: 0; + margin: 0; + min-width: 0; +} diff --git a/src/features/terminal/index.ts b/src/features/terminal/index.ts index 2aa97c529..06fd6c03c 100644 --- a/src/features/terminal/index.ts +++ b/src/features/terminal/index.ts @@ -101,6 +101,19 @@ export function activate(): () => void { hidden: true, run: ({ arg }) => openNewTab(typeof arg === "string" && arg ? arg : undefined), }, + { + id: "terminal.pluginSession", + title: "Open Plugin Terminal Session", + category: "Terminal", + hidden: true, + when: "projectOpen", + run: async ({ arg }) => { + const rt = await loadTerminalRuntime(); + await rt.pluginSession(arg as Parameters<typeof rt.pluginSession>[0]); + toolWindowService().show(WINDOW, true); + focusActiveSoon(); + }, + }, { id: "terminal.runCommand", title: "Run Command in Terminal", diff --git a/src/features/terminal/loader.ts b/src/features/terminal/loader.ts index 2bfdc8d7a..acd7d839d 100644 --- a/src/features/terminal/loader.ts +++ b/src/features/terminal/loader.ts @@ -4,6 +4,7 @@ * exist before it has loaded. */ import type * as Runtime from "./runtime"; +import { useProject } from "@/platform/project"; export type TerminalRuntime = typeof Runtime; @@ -16,5 +17,8 @@ export function terminalRuntime(): TerminalRuntime | null { } export function loadTerminalRuntime(): Promise<TerminalRuntime> { - return (pending ??= import("./runtime").then((m) => (loaded = m))); + return (pending ??= import("./runtime").then((m) => { + m.syncTerminalProject(useProject.getState().project?.root ?? null); + return (loaded = m); + })); } diff --git a/src/features/terminal/persist.test.ts b/src/features/terminal/persist.test.ts index aff470f25..062570d05 100644 --- a/src/features/terminal/persist.test.ts +++ b/src/features/terminal/persist.test.ts @@ -1,5 +1,6 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { decode } from "./persist"; +import { useProject } from "@/platform/project"; import { closeAll, getSession, @@ -15,24 +16,38 @@ import { vi.mock("./session", () => ({ TerminalSession: class { key: string; - state: { cwd: string }; - command: { program: string; args: string[]; shell?: boolean } | undefined; + state: { cwd: string; status: string }; + command: + | { program: string; args: string[]; shell?: boolean; onExit?: (code: number | null, closed: boolean) => void } + | undefined; fallbackRoot: string | undefined; constructor(opts: { key: string; cwd: string; - command?: { program: string; args: string[]; shell?: boolean }; + command?: { + program: string; + args: string[]; + shell?: boolean; + onExit?: (code: number | null, closed: boolean) => void; + }; fallbackRoot?: string; }) { this.key = opts.key; - this.state = { cwd: opts.cwd }; + this.state = { cwd: opts.cwd, status: "idle" }; this.command = opts.command; this.fallbackRoot = opts.fallbackRoot; } - dispose() {} + async start() { + this.state.status = "running"; + } + dispose() { + this.command?.onExit?.(null, true); + } }, })); +vi.mock("./runtime", async () => await vi.importActual("./store")); + const state = () => useTerminals.getState(); const names = () => state().tabs.map((t) => t.customName ?? t.name); const sessionOf = (key: string) => @@ -46,7 +61,30 @@ beforeEach(() => { closeAll(); }); +afterEach(() => useProject.setState({ project: null })); + describe("terminal tab memory", () => { + it("keeps a plugin terminal opened before the terminal pane first mounts", async () => { + useProject.setState({ project: { root: "/lazy-project" } } as never); + const { loadTerminalRuntime } = await import("./loader"); + const runtime = await loadTerminalRuntime(); + const onExit = vi.fn(); + await runtime.pluginSession({ + kind: "terminal", + operation: "first-plugin-terminal", + title: "Wrapper", + program: "/approved/wrapper", + cwd: "/lazy-project", + onExit, + }); + const tab = state().tabs[0]!; + const session = getSession(tab.session); + runtime.syncTerminalProject("/lazy-project"); + expect(getSession(tab.session)).toBe(session); + expect(state().tabs).toContainEqual(tab); + expect(onExit).not.toHaveBeenCalled(); + }); + it("rejects a stored value that is not this version", () => { expect(decode(null)).toBeNull(); expect(decode({ v: 2, tabs: [], layout: "g" })).toBeNull(); diff --git a/src/features/terminal/runtime.ts b/src/features/terminal/runtime.ts index fdb6e1d30..0816ce4ca 100644 --- a/src/features/terminal/runtime.ts +++ b/src/features/terminal/runtime.ts @@ -5,6 +5,7 @@ export { applyThemeToAll, closeAll, newTab, + pluginSession, syncTerminalProject, unbindTerminals, newTabAt, diff --git a/src/features/terminal/session.ts b/src/features/terminal/session.ts index 8e61ab329..4bd9ccd89 100644 --- a/src/features/terminal/session.ts +++ b/src/features/terminal/session.ts @@ -71,7 +71,14 @@ export interface SessionOptions { * Run this program instead of the configured shell (e.g. `docker exec -it <id> sh`). * `shell`: it is a shell (a predefined session), so shell integration still applies. */ - command?: { program: string; args: string[]; shell?: boolean }; + command?: { + program: string; + args: string[]; + shell?: boolean; + env?: Record<string, string>; + spawn?: (cols: number, rows: number, output: Channel<string>) => Promise<import("@/bindings").TerminalInfo>; + onExit?: (status: number | null, closed: boolean) => void; + }; /** * Restored tabs only: the project root, used when `cwd` no longer exists * (deleted since the last run, see `resolveStartCwd`). @@ -569,20 +576,22 @@ export class TerminalSession { const args = custom ? custom.args : shellArgs; const cwd = await resolveStartCwd(this.opts.cwd, this.opts.fallbackRoot); if (cwd !== this.opts.cwd) this.update({ cwd }); - const info = await call( - commands.termSpawn( - { - cwd, - shell: shell ?? null, - args, - env: { ...t.env }, - cols, - rows, - shellIntegration: (!custom || !!custom.shell) && t.shellIntegration, - }, - channel, - ), - ); + const info = custom?.spawn + ? await custom.spawn(cols, rows, channel) + : await call( + commands.termSpawn( + { + cwd, + shell: shell ?? null, + args, + env: custom?.onExit ? { ...custom.env } : { ...t.env, ...custom?.env }, + cols, + rows, + shellIntegration: (!custom || !!custom.shell) && t.shellIntegration, + }, + channel, + ), + ); if (this.disposed || generation !== this.generation) { void commands.termKill(info.id); return; @@ -593,7 +602,7 @@ export class TerminalSession { } catch (e) { this.pendingInput = ""; this.term.writeln(`\x1b[31mFailed to start terminal: ${e instanceof Error ? e.message : String(e)}\x1b[0m`); - this.term.writeln(RESTART_HINT); + if (!custom?.onExit) this.term.writeln(RESTART_HINT); this.update({ status: "error" }); } } @@ -622,18 +631,21 @@ export class TerminalSession { exited(code: number | null) { if (this.disposed) return; this.ptyId = null; + this.opts.command?.onExit?.(code, false); this.pendingInput = ""; this.update({ status: "exited", exitCode: code }); // Let output still in flight on the channel land first. setTimeout(() => { if (this.disposed || this.snapshot.status !== "exited") return; const suffix = code !== null && code !== 0 ? ` (exit code ${code})` : ""; - this.term.write(`\r\n\x1b[2m[Process completed${suffix}]\x1b[0m\r\n${RESTART_HINT}\r\n`); + const hint = this.opts.command?.onExit ? "" : `${RESTART_HINT}\r\n`; + this.term.write(`\r\n\x1b[2m[Process completed${suffix}]\x1b[0m\r\n${hint}`); this.opts.onExit?.(this.key); }, 60); } restart() { + if (this.opts.command?.onExit) return; if (this.snapshot.status === "running" || this.snapshot.status === "starting") return; this.term.write("\r\n"); this.pendingInput = ""; @@ -761,12 +773,14 @@ export class TerminalSession { dispose() { if (this.disposed) return; this.disposed = true; + if (this.snapshot.status === "running" || this.snapshot.status === "starting") + this.opts.command?.onExit?.(null, true); this.fitter.cancel(); this.ptyResize.cancel(); this.pendingInput = ""; this.resizeObserver?.disconnect(); this.host.removeEventListener("mousedown", this.onMiddleClick); - if (this.ptyId !== null) void commands.termKill(this.ptyId); + if (this.ptyId !== null && !this.opts.command?.spawn) void commands.termKill(this.ptyId); this.ptyId = null; this.disposables.forEach((d) => d.dispose()); this.term.dispose(); diff --git a/src/features/terminal/store.ts b/src/features/terminal/store.ts index 76e00f9a5..0f3a6382f 100644 --- a/src/features/terminal/store.ts +++ b/src/features/terminal/store.ts @@ -631,3 +631,44 @@ export function unbindTerminals() { restoring = false; } } + +const pluginSessions = new Map<string, string>(); + +/** Host-authorized plugin sessions use the same tabs and PTY controls as user terminals. */ +export async function pluginSession(a: { + kind: "terminal"; + operation: string; + title?: string; + program?: string; + args?: string[]; + cwd?: string; + env?: Record<string, string>; + spawn?: NonNullable<SessionOptions["command"]>["spawn"]; + onExit: (code: number | null, closed: boolean) => void; +}) { + const tab = useTerminals.getState().tabs.find((t) => t.id === pluginSessions.get(a.operation)); + const session = tab && getSession(tab.session); + if (session && session.state.status === "running") { + selectTab(tab.id); + return; + } + if (!a.cwd || !a.program) throw new Error("Invalid terminal request"); + const created = newTab(a.cwd, a.title, { + program: a.program, + args: a.args ?? [], + env: a.env, + spawn: a.spawn, + onExit: (code, closed) => { + pluginSessions.delete(a.operation); + a.onExit(code, closed); + }, + }); + pluginSessions.set(a.operation, created.id); + const started = getSession(created.session)!; + await started.start(); + if (started.state.status === "error") { + closeTab(created.id); + pluginSessions.delete(a.operation); + throw new Error("Terminal did not start"); + } +} diff --git a/src/features/vim/__tests__/vim.test.ts b/src/features/vim/__tests__/vim.test.ts index 097a3138e..069c2ee18 100644 --- a/src/features/vim/__tests__/vim.test.ts +++ b/src/features/vim/__tests__/vim.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { EditorState, Prec } from "@codemirror/state"; import { EditorView, keymap } from "@codemirror/view"; import { getCM, Vim } from "@replit/codemirror-vim"; -import { embeddedEditorExtensions, transientChange, useEditorRegistry } from "@/editor/api"; +import { embeddedEditorExtensions, transientChange, useEditorRegistry, onEditorEvent } from "@/editor/api"; import { executeAction, registerAction } from "@/platform/actions"; import { getContext } from "@/platform/context"; import { _handleEx, applyRcCommands, vimExtension } from "../engine"; @@ -49,6 +49,7 @@ describe("Vim mode toggle", () => { await executeAction("vim.toggle"); expect(useVimSettings.getState().enabled).toBe(false); expect(hasContribution()).toBe(false); + expect(getContext()["vim.mode"]).toBe(""); expect(diffSide.dom.hasAttribute("data-openidea-vim")).toBe(false); expect(getCM(diffSide)).toBeNull(); diffSide.destroy(); @@ -217,7 +218,17 @@ describe("Vim engine", () => { const closePopup = Prec.highest( keymap.of([{ key: "Escape", run: () => (popupOpen ? !(popupOpen = false) : false) }]), ); - const view = makeView([vimExtension(), closePopup]); + const modes: string[] = []; + const offModes = onEditorEvent((event) => { + if (event.type === "inputModeChanged") { + expect(event.path).toBe("/p/a.ts"); + modes.push(event.mode); + } + }); + const view = makeView([ + vimExtension({ doc: 1, path: "/p/a.ts", language: "typescript", uiMode: null, readOnly: false }), + closePopup, + ]); view.focus(); const cm = getCM(view)!; Vim.handleKey(cm as never, "i", "user"); @@ -233,6 +244,9 @@ describe("Vim engine", () => { expect(cm.state.vim?.insertMode).toBe(false); Vim.handleKey(cm as never, "v", "user"); expect(useVimMode.getState().mode).toBe("visual"); + expect(getContext()["vim.mode"]).toBe("visual"); + expect(modes).toEqual(expect.arrayContaining(["insert", "normal", "visual"])); + offModes(); view.destroy(); }); }); diff --git a/src/features/vim/engine.ts b/src/features/vim/engine.ts index c3612a200..4a8209bea 100644 --- a/src/features/vim/engine.ts +++ b/src/features/vim/engine.ts @@ -6,7 +6,8 @@ import { EditorState, Prec, StateEffect, StateField, Transaction, type Extension } from "@codemirror/state"; import { EditorView, ViewPlugin, runScopeHandlers } from "@codemirror/view"; import { Vim, getCM, vim } from "@replit/codemirror-vim"; -import { transientChange } from "@/editor/api"; +import { transientChange, reportInputMode, type EditorDocContext } from "@/editor/api"; +import { setContext } from "@/platform/context"; import { executeAction, getAction } from "@/platform/actions"; import { showStatusInfo } from "@/workbench/contributions"; import { VIM_EDITOR_ATTR } from "./conflicts"; @@ -190,27 +191,32 @@ const escapeClosesPopupsFirst = EditorView.domEventHandlers({ }, }); -const modeTracker = ViewPlugin.define((view) => { - const cm = getCM(view); - let state: VimModeState = { mode: cm?.state.vim?.insertMode ? "insert" : "normal", sub: null }; - const publish = () => useVimMode.setState(state); - // Not `view.hasFocus`: that also needs the window focused, and this editor stays the last focused one. - const focused = () => view.root.activeElement === view.contentDOM; - const onModeChange = (e: { mode: string; subMode?: string }) => { - state = { mode: e.mode, sub: e.subMode === "linewise" || e.subMode === "blockwise" ? e.subMode : null }; - queueMicrotask(() => view.dispatch({ effects: setVimInsert.of(e.mode === "insert" || e.mode === "replace") })); +const modeTracker = (ctx?: EditorDocContext) => + ViewPlugin.define((view) => { + const cm = getCM(view); + let state: VimModeState = { mode: cm?.state.vim?.insertMode ? "insert" : "normal", sub: null }; + const publish = () => { + useVimMode.setState(state); + setContext("vim.mode", state.mode); + if (ctx) reportInputMode(ctx.path, state.mode, state.sub); + }; + // Not `view.hasFocus`: that also needs the window focused, and this editor stays the last focused one. + const focused = () => view.root.activeElement === view.contentDOM; + const onModeChange = (e: { mode: string; subMode?: string }) => { + state = { mode: e.mode, sub: e.subMode === "linewise" || e.subMode === "blockwise" ? e.subMode : null }; + queueMicrotask(() => view.dispatch({ effects: setVimInsert.of(e.mode === "insert" || e.mode === "replace") })); + if (focused()) publish(); + }; + cm?.on("vim-mode-change", onModeChange); + view.contentDOM.addEventListener("focus", publish); if (focused()) publish(); - }; - cm?.on("vim-mode-change", onModeChange); - view.contentDOM.addEventListener("focus", publish); - if (focused()) publish(); - return { - destroy() { - cm?.off("vim-mode-change", onModeChange); - view.contentDOM.removeEventListener("focus", publish); - }, - }; -}); + return { + destroy() { + cm?.off("vim-mode-change", onModeChange); + view.contentDOM.removeEventListener("focus", publish); + }, + }; + }); const caret = "var(--caret, var(--fg-strong))"; @@ -237,7 +243,7 @@ const vimTheme = EditorView.theme({ }); /** Vim for one editor. Highest precedence: Vim sees keys before the editor's keymaps. */ -export function vimExtension(): Extension { +export function vimExtension(ctx?: EditorDocContext): Extension { defineExCommands(); return [ Prec.highest([ @@ -248,7 +254,7 @@ export function vimExtension(): Extension { imeTransactions, escapeClosesPopupsFirst, vim(), - modeTracker, + modeTracker(ctx), ]), imeCleanup, vimTheme, diff --git a/src/features/vim/index.ts b/src/features/vim/index.ts index 7ccdba177..c9c684f00 100644 --- a/src/features/vim/index.ts +++ b/src/features/vim/index.ts @@ -52,6 +52,7 @@ export function activate(): () => void { const gen = ++generation; setContext("vimMode", enabled); if (!enabled) { + setContext("vim.mode", ""); contribution?.(); contribution = null; return; @@ -63,7 +64,7 @@ export function activate(): () => void { id: "vim", order: 0, embedded: true, - extensions: () => e.vimExtension(), + extensions: (ctx) => e.vimExtension(ctx), }); }; @@ -117,6 +118,7 @@ export function activate(): () => void { contribution?.(); contribution = null; setContext("vimMode", false); + setContext("vim.mode", ""); disposers.reverse().forEach((d) => d()); }; } diff --git a/src/platform/actions.test.ts b/src/platform/actions.test.ts new file mode 100644 index 000000000..614393ce1 --- /dev/null +++ b/src/platform/actions.test.ts @@ -0,0 +1,21 @@ +import { expect, it, vi } from "vitest"; +import { executeAction, executeActionOrThrow, registerAction } from "./actions"; + +it("propagates host launch errors while retaining ordinary action error handling", async () => { + const dispose = registerAction({ + id: "test.hostLaunch", + title: "Test", + run: async () => { + throw new Error("spawn failed"); + }, + }); + const log = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + await expect(executeActionOrThrow("test.hostLaunch")).rejects.toThrow("spawn failed"); + await expect(executeAction("test.hostLaunch")).resolves.toBe(true); + expect(log).toHaveBeenCalledOnce(); + } finally { + dispose(); + log.mockRestore(); + } +}); diff --git a/src/platform/actions.ts b/src/platform/actions.ts index dd5777a8f..74362d1fa 100644 --- a/src/platform/actions.ts +++ b/src/platform/actions.ts @@ -167,7 +167,16 @@ export function onActionExecuted(listener: ActionListener): () => void { } /** Runs an action if registered and enabled. Returns whether it ran. */ -export async function executeAction(id: string, arg?: unknown): Promise<boolean> { +export function executeAction(id: string, arg?: unknown): Promise<boolean> { + return runAction(id, arg, false); +} + +/** Host requests need the launch error before acknowledging an operation. */ +export async function executeActionOrThrow(id: string, arg?: unknown): Promise<void> { + if (!(await runAction(id, arg, true))) throw new Error(`Action unavailable: ${id}`); +} + +async function runAction(id: string, arg: unknown, propagateError: boolean): Promise<boolean> { const action = getAction(id); if (!action) { console.warn(`unknown action: ${id}`); @@ -192,6 +201,7 @@ export async function executeAction(id: string, arg?: unknown): Promise<boolean> } await pending; } catch (e) { + if (propagateError) throw e; console.error(`action ${id} failed`, e); } return true; diff --git a/src/platform/languageSettings.ts b/src/platform/languageSettings.ts index f97105753..329683d8c 100644 --- a/src/platform/languageSettings.ts +++ b/src/platform/languageSettings.ts @@ -130,10 +130,12 @@ export interface ArtifactConfigValid { } export interface LanguagesValid { + pluginFormatters: Record<string, string>; python: { interpreter: string; activateVirtualenv: boolean }; ruby: { interpreter: string }; - node: { interpreter: string; packageManager: "" | "npm" | "pnpm" | "yarn" | "bun" }; + node: { runTarget: string; interpreter: string; packageManager: "" | "npm" | "pnpm" | "yarn" | "bun" }; php: { + runTarget: string; interpreter: string; languageLevel: (typeof PHP_LANGUAGE_LEVELS)[number]; includePaths: string[]; @@ -225,10 +227,11 @@ export type FolderListKey = | "unmarked"; export const DEFAULT_LANGUAGES: LanguagesValid = { + pluginFormatters: {}, python: { interpreter: "", activateVirtualenv: true }, ruby: { interpreter: "" }, - node: { interpreter: "", packageManager: "" }, - php: { interpreter: "", languageLevel: "", includePaths: [], stubs: [], phpstanConfig: "" }, + node: { runTarget: "", interpreter: "", packageManager: "" }, + php: { runTarget: "", interpreter: "", languageLevel: "", includePaths: [], stubs: [], phpstanConfig: "" }, rust: { toolchain: "", stdlib: "", checkOnSave: true, checkCommand: "check", rustfmtArgs: "" }, go: { goroot: "", gopath: "", buildTags: DEFAULT_GO_BUILD_TAGS, modules: { env: {}, autoVendor: true } }, typescript: { version: "bundled" }, @@ -344,16 +347,23 @@ export function validateLanguages(raw: unknown): LanguagesValid { const gradle = obj(java.gradle); const markdown = obj(l.markdown); return { + pluginFormatters: Object.fromEntries( + Object.entries(obj(l.pluginFormatters)).filter( + (entry): entry is [string, string] => typeof entry[1] === "string", + ), + ), python: { interpreter: text(py.interpreter, d.python.interpreter), activateVirtualenv: bool(py.activateVirtualenv, d.python.activateVirtualenv), }, ruby: { interpreter: text(ruby.interpreter, d.ruby.interpreter) }, node: { + runTarget: text(node.runTarget, ""), interpreter: text(node.interpreter, d.node.interpreter), packageManager: oneOf(node.packageManager, ["", "npm", "pnpm", "yarn", "bun"] as const, d.node.packageManager), }, php: { + runTarget: text(php.runTarget, ""), interpreter: text(php.interpreter, d.php.interpreter), languageLevel: oneOf(php.languageLevel, PHP_LANGUAGE_LEVELS, d.php.languageLevel), includePaths: strings(php.includePaths), diff --git a/src/platform/locales/text/en.json b/src/platform/locales/text/en.json index d630a2740..260ade947 100644 --- a/src/platform/locales/text/en.json +++ b/src/platform/locales/text/en.json @@ -561,5 +561,45 @@ "Show status bar": "Show status bar", "No background tasks": "No background tasks", "Status Bar Widgets": "Status Bar Widgets", - "Background Tasks": "Background Tasks" + "Background Tasks": "Background Tasks", + "Choose plugin executable": "Choose plugin executable", + "Approve plugin executable": "Approve plugin executable", + "Executable scope": "Executable scope", + "Choose executable…": "Choose executable…", + "Reset executable": "Reset executable", + "Revalidate the default executable? If it is unavailable, this permission will be revoked.": "Revalidate the default executable? If it is unavailable, this permission will be revoked.", + "Allow {name} to run {selected}? This is equivalent to running arbitrary code on this machine.": "Allow {name} to run {selected}? This is equivalent to running arbitrary code on this machine.", + "Connect to {host} over unencrypted HTTP (from the machine the project is on)": "Connect to {host} over unencrypted HTTP (from the machine the project is on)", + "Connect to local socket {path} — equivalent to running arbitrary code on the project machine": "Connect to local socket {path} — equivalent to running arbitrary code on the project machine", + "Output: {channel}": "Output: {channel}", + "You can close this tab.": "You can close this tab.", + "Run any IDE action or other plugins' commands as if you chose them, including running programs, committing and pushing": "Run any IDE action or other plugins' commands as if you chose them, including running programs, committing and pushing", + "Run Plugin Configuration": "Run Plugin Configuration", + "Open Plugin Terminal Session": "Open Plugin Terminal Session", + "Debug Plugin Configuration": "Debug Plugin Configuration", + "Expected: {0}": "Expected: {0}", + "Actual: {0}": "Actual: {0}", + "Use {0} for running PHP / Node / Composer in this project?": "Use {0} for running PHP / Node / Composer in this project?", + "Use": "Use", + "Not now": "Not now", + "Run in target": "Run in target", + "Run in target {0}": "Run in target {0}", + "Default run target": "Default run target", + "Read-only": "Read-only", + "Snippets": "Snippets", + "Plugin Formatters": "Plugin Formatters", + "Format on save": "Format on save", + "The selected plugin formatter is unavailable": "The selected plugin formatter is unavailable", + "Plugin formatter returned an invalid range": "Plugin formatter returned an invalid range", + "Plugin formatter returned overlapping edits": "Plugin formatter returned overlapping edits", + "Invalid plugin command": "Invalid plugin command", + "Plugin host unavailable": "Plugin host unavailable", + "The plugin did not declare this command": "The plugin did not declare this command", + "Invalid color range": "Invalid color range", + "Manage its own database sources and store their passwords in trusted projects": "Manage its own database sources and store their passwords in trusted projects", + "Connect Plugin Data Source": "Connect Plugin Data Source", + "Invalid data source id": "Invalid data source id", + "Database connection failed": "Database connection failed", + "Database service unavailable": "Database service unavailable", + "Trust this project to connect plugin data sources": "Trust this project to connect plugin data sources" } diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index bbe7692cd..5aca76293 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -6904,5 +6904,44 @@ "When off, Alt key combinations still work but pressing Alt alone does not focus the main menu": "关闭后,Alt 组合键仍然可用,但单独按下 Alt 不会聚焦主菜单", "the bundle of {gemfile} has no `debug` gem. Add `gem \"debug\"` to the Gemfile and run `bundle install`": "{gemfile} 的 bundle 中没有 `debug` gem。请在 Gemfile 中添加 `gem \"debug\"`,然后运行 `bundle install`", "No background tasks": "没有后台任务", - "Status Bar Widgets": "状态栏微件" + "Status Bar Widgets": "状态栏微件", + "Choose plugin executable": "选择插件可执行文件", + "Approve plugin executable": "批准插件可执行文件", + "Executable scope": "可执行文件作用域", + "Choose executable…": "选择可执行文件…", + "Reset executable": "重置可执行文件", + "Revalidate the default executable? If it is unavailable, this permission will be revoked.": "重新验证默认可执行文件?如果不可用,将撤销此权限。", + "Allow {name} to run {selected}? This is equivalent to running arbitrary code on this machine.": "允许 {name} 运行 {selected}?这等同于在此计算机上运行任意代码。", + "Connect to {host} over unencrypted HTTP (from the machine the project is on)": "通过未加密的 HTTP 连接到 {host}(从项目所在的计算机)", + "Connect to local socket {path} — equivalent to running arbitrary code on the project machine": "连接到本地套接字 {path} — 等同于在项目所在的计算机上运行任意代码", + "Output: {channel}": "输出:{channel}", + "You can close this tab.": "你可以关闭此标签页。", + "Run any IDE action or other plugins' commands as if you chose them, including running programs, committing and pushing": "如同您亲自选择一样运行任意 IDE 操作或其他插件的命令,包括运行程序、提交和推送", + "Run Plugin Configuration": "运行插件配置", + "Open Plugin Terminal Session": "打开插件终端会话", + "Debug Plugin Configuration": "调试插件配置", + "Expected: {0}": "期望值:{0}", + "Actual: {0}": "实际值:{0}", + "Use {0} for running PHP / Node / Composer in this project?": "在此项目中使用 {0} 运行 PHP / Node / Composer?", + "Use": "使用", + "Not now": "暂不使用", + "Run in target": "在目标中运行", + "Run in target {0}": "在目标 {0} 中运行", + "Default run target": "默认运行目标", + "Snippets": "代码片段", + "Plugin Formatters": "插件格式化工具", + "Format on save": "保存时格式化", + "The selected plugin formatter is unavailable": "所选插件格式化工具不可用", + "Plugin formatter returned an invalid range": "插件格式化工具返回了无效范围", + "Plugin formatter returned overlapping edits": "插件格式化工具返回了重叠的编辑", + "Invalid plugin command": "无效的插件命令", + "Plugin host unavailable": "插件宿主不可用", + "The plugin did not declare this command": "插件未声明此命令", + "Invalid color range": "无效的颜色范围", + "Manage its own database sources and store their passwords in trusted projects": "在受信任项目中管理其自己的数据库数据源并保存密码", + "Connect Plugin Data Source": "连接插件数据源", + "Invalid data source id": "数据源标识无效", + "Database connection failed": "数据库连接失败", + "Database service unavailable": "数据库服务不可用", + "Trust this project to connect plugin data sources": "信任此项目后才能连接插件数据源" } diff --git a/src/platform/locales/textCatalog.test.ts b/src/platform/locales/textCatalog.test.ts index c6fa6e52d..030a9c05b 100644 --- a/src/platform/locales/textCatalog.test.ts +++ b/src/platform/locales/textCatalog.test.ts @@ -565,7 +565,7 @@ function scanTypeScript(): Scan { /** Read strings without letting a Rust character literal open a string match. */ function backendLiterals(source: string): string[] { const out: string[] = []; - for (const m of source.matchAll(/'(?:\\.|[^'\\\r\n])'|"((?:[^"\\]|\\.)*)"/g)) { + for (const m of source.matchAll(/'(?:\\.|[^'\\\r\n])'|"((?:[^"\\]|\\[\s\S])*)"/g)) { if (m[1] !== undefined) out.push(m[1]); } return out; @@ -577,6 +577,13 @@ it("reads backend messages after quote character literals", () => { ).toEqual(["Select a .NET project", "Expected 'x'"]); }); +it("reads backend messages continued over a line", () => { + expect(backendLiterals('let m = "can\'t rename; \\\n half renamed"; let n = "Next";')).toEqual([ + "can't rename; \\\n half renamed", + "Next", + ]); +}); + /** String literals of the Rust crates and language definitions (backend messages). */ function scanBackend(literals: Set<string>) { const files = [ diff --git a/src/platform/pluginRunTargets.ts b/src/platform/pluginRunTargets.ts new file mode 100644 index 000000000..d8c3d67d2 --- /dev/null +++ b/src/platform/pluginRunTargets.ts @@ -0,0 +1,24 @@ +/** Host-owned registrations shared by plugins, Run and Settings. */ +import { create } from "zustand"; +import { useMemo } from "react"; +import type { RegisteredRunTarget } from "@/bindings"; +import { DEFAULT_RUN_TARGET, type RunTargetValid } from "./remoteSettings"; +import { useWorkflowSettings, workflowSettings } from "./workflowSettings"; + +export const usePluginRunTargets = create<{ targets: RegisteredRunTarget[] }>(() => ({ targets: [] })); +export type RuntimeRunTarget = Omit<RunTargetValid, "kind"> & { kind: RunTargetValid["kind"] | "plugin" }; +const converted = (targets: RegisteredRunTarget[]): RuntimeRunTarget[] => + targets.map((t) => ({ + ...DEFAULT_RUN_TARGET, + kind: "plugin", + id: t.id, + name: `${t.target.name} (${t.pluginName})`, + })); +export function runTargets(): RuntimeRunTarget[] { + return [...workflowSettings().runTargets, ...converted(usePluginRunTargets.getState().targets)]; +} +export function useRunTargets(): RuntimeRunTarget[] { + const saved = useWorkflowSettings((s) => s.runTargets); + const plugins = usePluginRunTargets((s) => s.targets); + return useMemo(() => [...saved, ...converted(plugins)], [saved, plugins]); +} diff --git a/src/platform/remoteSettings.ts b/src/platform/remoteSettings.ts index b1576f629..75ad2fefe 100644 --- a/src/platform/remoteSettings.ts +++ b/src/platform/remoteSettings.ts @@ -243,7 +243,7 @@ let seq = 0; export const newRemoteId = (prefix: string) => `${prefix}-${Date.now().toString(36)}-${++seq}`; /** Whether `target` runs configurations in a container (Docker, Compose) rather than over SSH. */ -export const inContainer = (target: Pick<RunTargetValid, "kind">) => target.kind !== "ssh"; +export const inContainer = (target: { kind: string }) => target.kind === "docker" || target.kind === "compose"; /** Backend connection descriptor for deployment and folder browsing. */ export function deploymentConnection(server: DeploymentServerValid): { kind: "local" } | { kind: "sftp"; ssh: string } { diff --git a/src/platform/runTargetSelection.ts b/src/platform/runTargetSelection.ts new file mode 100644 index 000000000..58ed19e3a --- /dev/null +++ b/src/platform/runTargetSelection.ts @@ -0,0 +1,24 @@ +import type { RunConfig } from "@/bindings"; +import { validateLanguages } from "./languageSettings"; +import { useProject } from "./project"; +import { workflowSettings } from "./workflowSettings"; + +/** Mirrors ide-sdk's interpreter target selection for guards before IPC launch. */ +export function withRunTarget(config: RunConfig): RunConfig { + if (config.target !== undefined && config.target !== null) return config; + const languages = validateLanguages(useProject.getState().settings?.languages); + const name = config.command + .split(/[\\/]/) + .at(-1) + ?.replace(/\.(exe|cmd|bat)$/i, ""); + const selected = + ["php", "composer", "phpunit", "pest"].includes(name ?? "") || ["php", "composer", "phpunit"].includes(config.kind) + ? languages.php.runTarget + : ["node", "npm", "npx", "pnpm", "yarn", "bun"].includes(name ?? "") || + ["node", "npm", "vitest", "jest", "mocha"].includes(config.kind) + ? languages.node.runTarget + : ""; + if (selected === "local") return { ...config, target: "" }; + const target = selected || workflowSettings().run.defaultTarget; + return target ? { ...config, target } : config; +} diff --git a/src/platform/workflowSettings.ts b/src/platform/workflowSettings.ts index 5e1caa97b..2c5a90138 100644 --- a/src/platform/workflowSettings.ts +++ b/src/platform/workflowSettings.ts @@ -95,6 +95,7 @@ export interface DebuggerSettingsValid { } export interface RunSettingsValid { + defaultTarget: string; confirmRerun: boolean; confirmTerminate: boolean; showOnStart: boolean; @@ -256,7 +257,7 @@ export const DEFAULT_WORKFLOW: WorkflowSettings = { skipLibraryCode: true, python: { attachSubprocess: true, gevent: false }, }, - run: { confirmRerun: true, confirmTerminate: true, showOnStart: true, terminationTimeoutMs: 3000 }, + run: { defaultTarget: "", confirmRerun: true, confirmTerminate: true, showOnStart: true, terminationTimeoutMs: 3000 }, terminal: { startDirectory: "", env: {}, @@ -403,6 +404,7 @@ export function validateRun(raw: unknown): RunSettingsValid { const d = DEFAULT_WORKFLOW.run; const v = obj(raw); return { + defaultTarget: text(v.defaultTarget, ""), confirmRerun: bool(v.confirmRerun, d.confirmRerun), confirmTerminate: bool(v.confirmTerminate, d.confirmTerminate), showOnStart: bool(v.showOnStart, d.showOnStart), diff --git a/src/workbench/ContributedToolbar.test.tsx b/src/workbench/ContributedToolbar.test.tsx new file mode 100644 index 000000000..9a7fe9785 --- /dev/null +++ b/src/workbench/ContributedToolbar.test.tsx @@ -0,0 +1,33 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { ContributedToolbar } from "./ContributedToolbar"; +import { registerMenuSlotItem } from "./contributions"; +import { registerAction } from "@/platform/actions"; +import { setContext } from "@/platform/context"; +const offs: (() => void)[] = []; +afterEach(() => + offs + .splice(0) + .reverse() + .forEach((f) => f()), +); +it("renders toolbar slots with command icons and honors when clauses", () => { + const run = vi.fn(); + offs.push(registerAction({ id: "test.pluginMenu", title: "Plugin action", icon: "run", run })); + offs.push( + registerMenuSlotItem("editor.title", { + action: "test.pluginMenu", + label: "Plugin action", + icon: "run", + when: "editorLang == sql", + }), + ); + setContext("editorLang", "sql"); + const r = render(<ContributedToolbar slot="editor.title" />); + fireEvent.click(screen.getByRole("button", { name: /Plugin action/ })); + expect(run).toHaveBeenCalledOnce(); + r.unmount(); + setContext("editorLang", "text"); + render(<ContributedToolbar slot="editor.title" />); + expect(screen.queryByRole("button")).toBeNull(); +}); diff --git a/src/workbench/ContributedToolbar.tsx b/src/workbench/ContributedToolbar.tsx new file mode 100644 index 000000000..77cc0f64c --- /dev/null +++ b/src/workbench/ContributedToolbar.tsx @@ -0,0 +1,26 @@ +import { useActions } from "@/platform/actions"; +/** Plugin action slots in existing editor and main toolbars. */ +import { useContextKeys, evaluateWhen } from "@/platform/context"; +import { IconButton } from "@/ui"; +import { useMenuSlotItems } from "./contributions"; +export function ContributedToolbar({ slot }: { slot: string }) { + const actions = useActions((s) => s.actions); + const items = useMenuSlotItems((s) => s.slots[slot]); + useContextKeys(); + return ( + <> + {(items ?? []) + .filter((i) => evaluateWhen(i.when ?? undefined)) + .map((i) => ( + <IconButton + key={i.action} + icon={actions.get(i.action)?.icon ?? i.icon ?? "plugin"} + action={i.action} + actionArg={i.arg} + title={i.label} + size="small" + /> + ))} + </> + ); +} diff --git a/src/workbench/TitleBar.tsx b/src/workbench/TitleBar.tsx index e88b337ce..459640323 100644 --- a/src/workbench/TitleBar.tsx +++ b/src/workbench/TitleBar.tsx @@ -1,3 +1,4 @@ +import { ContributedToolbar } from "./ContributedToolbar"; import { useMemo, useRef, useState } from "react"; import clsx from "clsx"; import { useActions } from "@/platform/actions"; @@ -357,6 +358,7 @@ export function TitleBar({ {!minimal && ( <> {toolbar && !menuExpanded && shown.right.map(entry)} + {toolbar && !menuExpanded && <ContributedToolbar slot="mainToolbar" />} {!menuExpanded && ( <div className={styles.tools}> <GearButton customize={menu} /> diff --git a/src/workbench/contributions.ts b/src/workbench/contributions.ts index 9b42a9a24..50da15fb5 100644 --- a/src/workbench/contributions.ts +++ b/src/workbench/contributions.ts @@ -1043,6 +1043,8 @@ export type ContextMenuId = | "editor" | "projectView" | "editorTab" + | "problems" + | "searchResults" | "editorGutter" | "openIn" | "stopProcess" @@ -1085,6 +1087,8 @@ export interface MenuGroupContext { text?: string; /** Editor menu: 0-based caret line; gutter menu: the clicked line. */ line?: number; + /** Zero-based UTF-16 column for problem/search result commands. */ + column?: number; /** The tab's input, when the menu is for an editor tab without a path. */ input?: EditorInput; } @@ -1731,3 +1735,31 @@ export function stepFocusedSurfaceFontZoom(delta: number): boolean { export function resetSurfaceFontZoom(): void { for (const step of fontZoomSurfaces.values()) step(0); } + +/** Optional plugin test parser; the Run feature keeps the existing test tree. */ +type PluginTestParser = (config: string, chunk: string) => Promise<import("@/bindings").PluginTestEvent[] | null>; +let pluginTestParser: PluginTestParser | null = null; +export function providePluginTestParser(parser: PluginTestParser): () => void { + pluginTestParser = parser; + return () => { + if (pluginTestParser === parser) pluginTestParser = null; + }; +} +export function parsePluginTests(config: string, chunk: string) { + return pluginTestParser?.(config, chunk) ?? Promise.resolve(null); +} + +/** Read-only named snippets supplied by plugins. */ +export interface ContributedSnippet { + id: string; + pluginName: string; + language: string; + prefixes: string[]; + body: string; + description: string; +} +export const useContributedSnippets = create<{ snippets: ContributedSnippet[] }>(() => ({ snippets: [] })); +export function addContributedSnippets(snippets: ContributedSnippet[]): () => void { + useContributedSnippets.setState((s) => ({ snippets: [...s.snippets, ...snippets] })); + return () => useContributedSnippets.setState((s) => ({ snippets: s.snippets.filter((x) => !snippets.includes(x)) })); +} diff --git a/wit/commit.wit b/wit/commit.wit index a70b889c1..91dbe0e28 100644 --- a/wit/commit.wit +++ b/wit/commit.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// The commit being prepared in the window the current command or event came /// from (the Commit tool window, or the Commit dialog while it is open): the diff --git a/wit/database.wit b/wit/database.wit new file mode 100644 index 000000000..4e77bd9c8 --- /dev/null +++ b/wit/database.wit @@ -0,0 +1,34 @@ +package openidea:plugin@1.4.0; + +/// Plugin-owned project data sources. Requires database.sources and project trust. +/// Runs on the Database subsystem's host, including on remote project servers. +interface database { + use types.{error}; + use workspace.{project}; + /// Non-secret connection settings. Id is private to this plugin (1–128 bytes). + /// Drivers: sqlite, postgres, mysql, sqlServer, clickhouse, oracle. + /// SQLite uses database as its file path, or a sqlite: URL. Options are driver properties. + /// URLs and options must not contain passwords, tokens or other credentials. + record data-source { + id: string, + name: string, + driver: string, + host: option<string>, + port: option<u16>, + database: option<string>, + user: option<string>, + url: option<string>, + options: list<tuple<string, string>>, + } + /// Creates or replaces only this plugin's source; unrelated file entries are preserved. + upsert-source: func(project: borrow<project>, source: data-source) -> result<_, error>; + /// Removes an owned source and its credentials; not-found for every other id. + remove-source: func(project: borrow<project>, id: string) -> result<_, error>; + /// Lists only this plugin's private ids, never connection settings or credentials. + sources: func(project: borrow<project>) -> result<list<string>, error>; + /// Writes an owned source's password directly to the Database secure store. Never readable. + set-credential: func(project: borrow<project>, id: string, password: string) -> result<_, error>; + /// Requests the project's usual connection/introspection in the invoking window. + /// Invalid outside a command/event of that project's window. + connect: func(project: borrow<project>, id: string) -> result<_, error>; +} diff --git a/wit/decorations.wit b/wit/decorations.wit new file mode 100644 index 000000000..1c5a033e6 --- /dev/null +++ b/wit/decorations.wit @@ -0,0 +1,22 @@ +package openidea:plugin@1.4.0; + +/// Project-scoped decorations (API 1.4). Requires project.read; paths must be inside +/// an open project. Sets belong to the plugin and disappear when it stops. +/// Styles: highlight, search, added, modified, removed, warning, error, info, +/// underline, strikethrough, muted. Unknown styles are ignored. Theme tokens only. +interface decorations { + use types.{error, range}; + /// An editor annotation; after is plain text shown after the range. + record decoration { range: range, style: string, hover: option<string>, after: option<string> } + /// A project-tree annotation; badge is at most two Unicode characters. + record file-decoration { badge: option<string>, style: string, tooltip: option<string> } + /// Replace this set for a file. At most 10,000 items/file and 50 editor sets/plugin. + /// Metadata is bounded to 4 MiB per plugin. Unknown styles are ignored. + set: func(path: string, set-id: string, items: list<decoration>) -> result<_, error>; + /// Remove this editor set from every file/project. + clear: func(set-id: string) -> result<_, error>; + /// Replace this set's tree annotation for a file; at most 50 tree sets/plugin. + set-file: func(path: string, set-id: string, item: file-decoration) -> result<_, error>; + /// Remove this tree set from every file/project. + clear-file: func(set-id: string) -> result<_, error>; +} diff --git a/wit/details.wit b/wit/details.wit index 9e889851a..f7342991d 100644 --- a/wit/details.wit +++ b/wit/details.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// What the pane beside the Services tree shows for a plugin's node. interface detail-types { diff --git a/wit/diagnostics.wit b/wit/diagnostics.wit index d5d62de72..499335fde 100644 --- a/wit/diagnostics.wit +++ b/wit/diagnostics.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-diagnostics`): problems in the current, /// possibly unsaved document, for the languages declared with the diff --git a/wit/endpoints.wit b/wit/endpoints.wit index 95302fcb0..f4a446fe0 100644 --- a/wit/endpoints.wit +++ b/wit/endpoints.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// HTTP endpoints found in a project (`[[contributes.endpoints]]`). interface endpoints-provider { diff --git a/wit/events.wit b/wit/events.wit index 8851d18a7..f52030971 100644 --- a/wit/events.wit +++ b/wit/events.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Workbench events, delivered in batches to `event-handler.handle`. interface events { @@ -29,6 +29,9 @@ interface events { /// Delivers `kind` events from now on, for the rest of the session (also /// across idle reclaim: an event activates the plugin again). Declare /// `activation = ["on-event:<kind>"]` to get them before first use. + /// API 1.4 also delivers `file-closed` (JSON path string), `settings-changed` + /// (JSON array of the plugin's changed setting keys), and `input-mode-changed` + /// (JSON {path, mode, sub}) through `other`. The last needs `events.input`. /// Kinds: `file-opened`, `file-saved`, `files-changed` (batched disk changes /// as `other`, with created/modified/removed path lists in JSON), `editor-focused` (the focused editor /// changed), `vcs-changed` (a repository's state changed: commit, diff --git a/wit/exports.wit b/wit/exports.wit index 67d7c6060..434b29ceb 100644 --- a/wit/exports.wit +++ b/wit/exports.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; interface lifecycle { use types.{context, error}; diff --git a/wit/file-actions.wit b/wit/file-actions.wit index 477809180..12b4aec17 100644 --- a/wit/file-actions.wit +++ b/wit/file-actions.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-file-actions`): quick fixes and /// refactorings that create, rename or delete files, next to the text edits diff --git a/wit/fs.wit b/wit/fs.wit index 7cd07fd71..c1a96cf83 100644 --- a/wit/fs.wit +++ b/wit/fs.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Files outside the open projects: the configuration of the program a /// plugin integrates with, a tool installed elsewhere, a licence file. diff --git a/wit/host.wit b/wit/host.wit index 91c3e12fc..c5ce20fbe 100644 --- a/wit/host.wit +++ b/wit/host.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Writes to the IDE log, tagged with the plugin id. interface log { @@ -53,6 +53,11 @@ interface ui { /// Shows or updates the plugin's status bar item `id` in every window. set-status-item: func(id: string, item: status-item); remove-status-item: func(id: string); + /// Overrides the global item in the invoking window only (API 1.4). + /// Invalid outside a command or event of a window; removed on window close or plugin stop. + set-window-status-item: func(id: string, item: status-item) -> result<_, error>; + /// Removes this window's override, falling back to the global item. + remove-window-status-item: func(id: string) -> result<_, error>; /// Sets the plugin's context key `key`, which `when` clauses test as /// `plugin.<id>.<key>`, in the window the current command or event came /// from (every window when there is none). A window's value takes @@ -104,3 +109,23 @@ interface storage { /// Keys starting with `prefix`, sorted. keys: func(prefix: string) -> result<list<string>, error>; } + +/// Executes stable IDE action ids or `plugin.<plugin-id>.<command-id>` (API 1.4). +interface command-runner { + use types.{error}; + /// Only in commands/events of a window. Returns a plugin command's value, + /// or none for IDE actions; unknown ids are not-found. Nested calls are bounded. + /// Requires high-risk `ide.commands`, except for this plugin's own commands. + execute: func(id: string, arg: option<string>) -> result<option<string>, error>; +} + +/// Per-plugin named, read-only Output tabs (API 1.4), with a 1 MiB UTF-8 ring buffer. +interface output { + use types.{error}; + /// Appends text to a channel (at most 32 channels, names 1-128 bytes). + append: func(channel: string, text: string) -> result<_, error>; + /// Clears a channel's buffer. + clear: func(channel: string) -> result<_, error>; + /// Reveals the channel in the invoking window; only commands/events of a window. + reveal: func(channel: string) -> result<_, error>; +} diff --git a/wit/hover.wit b/wit/hover.wit index a95c22758..5ddde792e 100644 --- a/wit/hover.wit +++ b/wit/hover.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-hover`): Markdown added to the hover of /// documents of the languages declared with the `hover` provider kind, diff --git a/wit/indexing.wit b/wit/indexing.wit index 4ee08d64b..227aab726 100644 --- a/wit/indexing.wit +++ b/wit/indexing.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Cross-file associations a plugin publishes for the files it declares under /// `[[contributes.documentIndexers]]`. The host calls `index` in cancellable diff --git a/wit/injections.wit b/wit/injections.wit new file mode 100644 index 000000000..5f9d7c2eb --- /dev/null +++ b/wit/injections.wit @@ -0,0 +1,12 @@ +package openidea:plugin@1.4.0; + +/// Embedded languages. This host routes plugin providers, not embedded LSP servers. +interface injections { + /// A versioned document snapshot. Coordinates are zero-based UTF-16. + record document { path: string, language: string, version: u64, text: string } + /// A host-document range containing the named language. + record injection { range: range, language: string } + use types.{range}; + /// Embedded ranges, refreshed after edits under the editor-provider budget. + injections: func(doc: document) -> list<injection>; +} diff --git a/wit/inlay.wit b/wit/inlay.wit index ab00b0c9b..1098869a5 100644 --- a/wit/inlay.wit +++ b/wit/inlay.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-inlay-hints`): inline hints in documents of /// the languages declared with the `inlay-hints` provider kind, including diff --git a/wit/io.wit b/wit/io.wit index cffd883be..b27e48d62 100644 --- a/wit/io.wit +++ b/wit/io.wit @@ -1,8 +1,13 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Problems in files, shown in the editor and the Problems view. interface diagnostics { use types.{error, range, location}; + /// Problems reported by a source for a file in this window's project. + record file-problems { path: string, source: string, diagnostics: list<diagnostic> } + /// Current Problems view snapshot, optionally filtered to a project file. + /// Requires project.read and an invoking window. Empty for remote projects. + problems: func(path: option<string>) -> result<list<file-problems>, error>; enum severity { error, @@ -35,7 +40,7 @@ interface diagnostics { publish: func(path: string, diagnostics: list<diagnostic>) -> result<_, error>; } -/// HTTP(S) to the hosts granted with `net:<host>`. +/// HTTPS with `net:<host>`; unencrypted HTTP with `net.http:<host>`. interface net { use types.{error}; @@ -46,7 +51,8 @@ interface net { record request { method: string, - /// `https://…`; plain `http://` only to `localhost` / `127.0.0.1`. + /// `https://…` requires `net:<host>`. Plain `http://…` requires + /// `net.http:<host>` (API 1.4), except loopback, which uses `net:<host>`. url: string, /// `{{secret:<name>}}` in a value is replaced by the plugin's secret /// `name` on the way out (requires `secrets`); the plugin never sees it. @@ -72,6 +78,11 @@ interface net { /// OS keychain). Plugins only use them through `{{secret:<name>}}` in /// `net` requests. Requires `secrets`. interface secrets { + use types.{error}; + /// Stores a declared secret without ever exposing it back to plugin code (API 1.4). + set: func(name: string, value: string) -> result<_, error>; + /// Deletes a declared secret (API 1.4). + delete: func(name: string) -> result<_, error>; is-set: func(name: string) -> bool; } @@ -144,3 +155,38 @@ interface process { /// also ends its processes. start: func(cmd: command, cwd: option<borrow<project>>) -> result<child, error>; } + +/// Local sockets on the project machine, including the server for remote projects. +/// Requires `ipc:<path>`; host-expanded `~/`, `$TMPDIR/`, `$XDG_RUNTIME_DIR/` +/// and Windows `\\.\pipe\name` are accepted. API 1.4. +interface ipc { + use types.{error}; + /// A byte stream. At most eight per plugin; dropping closes it, as does stopping the plugin. + resource connection { + /// Writes up to 1 MiB, subject to the current call's budget and cancellation. + write: func(bytes: list<u8>) -> result<_, error>; + /// Reads up to 1 MiB; empty on EOF or timeout. Zero timeout only checks. + read: func(max-bytes: u32, timeout-ms: u32) -> result<list<u8>, error>; + /// Closes the stream; subsequent reads and writes fail. + close: func() -> result<_, error>; + } + /// Connects to an absolute Unix socket or Windows named pipe; never TCP. + connect: func(name: string) -> result<connection, error>; +} + +/// OAuth browser redirects received on the desktop, even for remote projects (API 1.4). +/// No capability needed; only commands/events of a window may use this interface. +interface auth { + use types.{error}; + /// A one-shot IPv4 loopback listener. At most two open per plugin. + resource loopback { + /// `http://127.0.0.1:<random-port>/<path>` for the authorization URL. + redirect-uri: func() -> string; + /// Returns the first matching request's raw query string, or none on timeout. + /// Waiting is excluded from the call budget. The listener serves a short + /// completion page and closes; resource drop and plugin stop also close it. + wait: func(timeout-ms: u32) -> result<option<string>, error>; + } + /// Binds 127.0.0.1 only. `path` has no query or fragment. + begin-loopback: func(path: string) -> result<loopback, error>; +} diff --git a/wit/language-features.wit b/wit/language-features.wit new file mode 100644 index 000000000..c47c4b621 --- /dev/null +++ b/wit/language-features.wit @@ -0,0 +1,114 @@ +package openidea:plugin@1.4.0; + +/// Additional language features, called only for declared API 1.4 provider kinds. +/// Editor calls have a 50 ms soft / 500 ms hard budget; formatting has 5 seconds. +interface language-features { + use types.{error, position, range, text-edit, location}; + /// A versioned document snapshot. Coordinates are zero-based UTF-16. + record document { + path: string, + language: string, + version: u64, + text: string, + } + /// A parameter label, either literal text or UTF-16 offsets within the signature label. + variant parameter-label { text(string), offsets(tuple<u32, u32>) } + /// Indentation options selected by the editor. + record format-options { + tab-size: u32, + insert-spaces: bool, + } + /// One parameter, with optional Markdown documentation. + record parameter { + label: parameter-label, + documentation: option<string>, + } + /// One callable signature; documentation is Markdown. + record signature { + label: string, + documentation: option<string>, + parameters: list<parameter>, + } + /// Callable signatures and zero-based active signature/parameter indices. + record signature-help-result { + signatures: list<signature>, + active-signature: u32, + active-parameter: u32, + } + /// An occurrence of the symbol. Kind: text, read or write; unknown is text. + record highlight { + range: range, + kind: string, + } + /// An absolute path, project-relative path or HTTP(S) URL to follow on click. + record document-link { + range: range, + target: string, + } + /// An inline action. Command must be this plugin's declared command id. + record code-lens { + range: range, + title: string, + command: string, + argument: option<string>, + } + /// A multi-line fold. Kinds: region, comment or imports; unknown is region. + record fold-range { + start-line: u32, + end-line: u32, + kind: string, + } + /// A color literal. All channels are finite floats in the inclusive range 0–1. + record color-info { + range: range, + red: f32, + green: f32, + blue: f32, + alpha: f32, + } + /// An outline node. Parent indexes this list, precedes the child and contains it. + /// Kinds are LSP-style names (e.g. class, method, enum-member); unknown is neutral. + record document-symbol { + name: string, + detail: string, + kind: string, + range: range, + selection-range: range, + parent: option<u32>, + } + /// A line action for this plugin's own declared command. Icons follow line-marker + /// aliases, registered icon names or declared manifest marker SVG paths. + record gutter-action { + line: u32, + icon: option<string>, + tooltip: string, + command: string, + argument: option<string>, + } + /// Format the whole document or the selected range. + format: func(doc: document, range: option<range>, options: format-options) -> result<list<text-edit>, error>; + /// Signature help when the language server has no signatures. + signature-help: func(doc: document, at: position) -> option<signature-help-result>; + /// Occurrences at the caret, appended to language-server highlights. + document-highlights: func(doc: document, at: position) -> list<highlight>; + /// Navigable ranges, appended to language-server document links. + document-links: func(doc: document) -> list<document-link>; + /// Clicks run only this plugin's declared commands. + code-lenses: func(doc: document) -> list<code-lens>; + /// Nested ranges, innermost to outermost. + selection-ranges: func(doc: document, at: position) -> list<range>; + /// Folds to merge with tree-sitter, for any declared language. + folding: func(doc: document) -> list<fold-range>; + /// Color literals to show in the existing color-preview gutter and picker. + colors: func(doc: document) -> list<color-info>; + /// Replacement strings for the selected color range. + color-presentations: func(doc: document, color: color-info) -> list<string>; + /// Used only when no LSP document symbols are available. + document-symbols: func(doc: document) -> list<document-symbol>; + /// Implementation locations, merged with language-server results. + implementation: func(doc: document, at: position) -> list<location>; + /// Type-declaration locations, merged with language-server results. + type-definition: func(doc: document, at: position) -> list<location>; + /// Clicks run only this plugin's declared commands. + gutter-actions: func(doc: document) -> list<gutter-action>; +} diff --git a/wit/mcp.wit b/wit/mcp.wit new file mode 100644 index 000000000..e250f5496 --- /dev/null +++ b/wit/mcp.wit @@ -0,0 +1,16 @@ +package openidea:plugin@1.4.0; + +/// Discovery of the IDE's built-in MCP server; never exposes its URL or token. +interface mcp { + /// Stdio bridge argv for the invoking project; none without a window/project or while off. + stdio-command: func() -> option<list<string>>; +} + +/// Optional tools declared by contributes.mcpTools, under the plugin's grants and command budget. +interface mcp-provider { + use types.{error}; + use invocation.{invocation}; + /// Calls the declared local tool name with JSON arguments and the project's window context. + /// Returns text; tools may encode structured data as JSON text. + call: func(tool: string, arguments: string, ctx: borrow<invocation>) -> result<string, error>; +} diff --git a/wit/plugin.wit b/wit/plugin.wit index 8d1451ac3..04c707d20 100644 --- a/wit/plugin.wit +++ b/wit/plugin.wit @@ -8,7 +8,7 @@ /// `fs` and `workspace`, and everything else goes through the imports below, /// gated by the capabilities granted in `plugin.toml` (a missing one returns /// `error::denied`). -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; world plugin { import log; @@ -17,6 +17,7 @@ world plugin { import fs; import workspace; import editor; + import decorations; import invocation; import vcs; import commit; @@ -26,7 +27,15 @@ world plugin { import net; import secrets; import process; + import ipc; + import output; + import auth; + import command-runner; import views; + import run; + import run-targets; + import database; + import mcp; import settings; export lifecycle; @@ -168,3 +177,26 @@ world plugin-diagnostics { world editor-diagnostics { export diagnostics-provider; } + +/// A plugin that exports project run configurations. +world plugin-run-configurations { include plugin; export run-configurations; } +/// Export-only configurations world, composed with plugin by SDK macros. +world configurations-provider { export run-configurations; } +/// A plugin that parses its test output. +world plugin-test-results { include plugin; export test-results; } +/// Export-only test parser world, composed with plugin by SDK macros. +world test-results-provider { export test-results; } + +/// A plugin with additional API 1.4 language features. +world plugin-language-features { include plugin; export language-features; } +/// Export-only language features, composed with the base plugin world. +world editor-language-features { export language-features; } +/// A plugin that identifies embedded language ranges. +world plugin-injections { include plugin; export injections; } +/// Export-only injections, composed with the base plugin world. +world language-injections { export injections; } + +/// A plugin contributing tools to the built-in MCP server. +world plugin-mcp { include plugin; export mcp-provider; } +/// Export-only MCP tools, composed with the base plugin world. +world mcp-tools { export mcp-provider; } diff --git a/wit/providers.wit b/wit/providers.wit index 317eb81d1..8c0713ec6 100644 --- a/wit/providers.wit +++ b/wit/providers.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Editor features a plugin provides for the languages it declares under /// `[[contributes.providers]]` (the host calls only declared kinds). They run diff --git a/wit/run-targets.wit b/wit/run-targets.wit new file mode 100644 index 000000000..66a41e700 --- /dev/null +++ b/wit/run-targets.wit @@ -0,0 +1,36 @@ +package openidea:plugin@1.4.0; + +/// Project-scoped targets owned by the registering plugin (API 1.4). +interface run-targets { + use types.{error}; + use workspace.{project}; + + /// An argv wrapper using a granted, pinned process program. No shell is used. + /// Normally: program + args + workdir-args + command + command arguments. + /// An override matches wrap::program_name(command) and replaces args AND command: + /// program + override + workdir-args + command arguments. + /// DDEV: program="ddev", args=["exec"], commands=[("composer", ["composer"])]: + /// `ddev exec php x.php` and `ddev composer install`. + /// Each workdir-args element may contain {cwd}, mapped under container-root. + /// All workdir-args are omitted without container-root or for cwd outside the project. + /// Without workdir-args the wrapper manages its container directory itself. + /// The wrapper always starts in the configuration's LOCAL working directory. + /// Environment reaches the wrapper; forwarding into a container depends on its conventions. + /// Registration never selects the target. Debugging plugin targets is invalid. + record run-target { + id: string, + name: string, + program: string, + args: list<string>, + workdir-args: list<string>, + container-root: option<string>, + commands: list<tuple<string, list<string>>>, + } + + /// Registers/replaces this plugin's target in an open, trusted project. + /// Targets survive idle reclamation, and disappear on unregister or plugin stop. + /// Remote project registration and execution take place on the server. + register: func(project: borrow<project>, target: run-target) -> result<_, error>; + /// Removes only this plugin's target from the project; missing ids are harmless. + unregister: func(project: borrow<project>, id: string); +} diff --git a/wit/run.wit b/wit/run.wit new file mode 100644 index 000000000..c1bc79cf9 --- /dev/null +++ b/wit/run.wit @@ -0,0 +1,50 @@ +package openidea:plugin@1.4.0; + +/// Launches this plugin's configurations in the invoking window's Run tool window. +interface run { + use types.{error}; + /// Starts an owned configuration, appending args. Debug requires a debugger. + /// Only from a window command/event. The returned id receives run-exited. + start: func(config: string, args: list<string>, debug: bool) -> result<string, error>; +} + +/// Optional dynamic configurations, refreshed alongside run detectors. +interface run-configurations { + /// A configuration running an approved process in the project sandbox. + record run-configuration { + id: string, + name: string, + program: string, + args: list<string>, + cwd: option<string>, + test: bool, + framework: option<string>, + } + /// Returns the complete set for this project root. + configurations: func(project-root: string) -> list<run-configuration>; +} + +/// Optional parser for test configurations whose framework is plugin. +interface test-results { + use types.{location, extension}; + /// A suite or test; parents must precede children and ids remain stable for a run. + record test-start { id: string, parent: option<string>, name: string, location: option<location> } + /// A successful test and optional elapsed milliseconds. + record test-passed { id: string, duration-ms: option<u32> } + /// A failed test, comparison and optional declaration location. + record test-failed { + id: string, duration-ms: option<u32>, message: string, + expected: option<string>, actual: option<string>, location: option<location>, + } + /// A skipped test and its explanation. + record test-skipped { id: string, reason: option<string> } + /// Output attached to a test or suite. + record test-output { id: string, text: string } + /// Incremental updates to the IDE's test tree. Unknown extensions are ignored. + variant test-event { + suite-started(test-start), test-started(test-start), passed(test-passed), + failed(test-failed), skipped(test-skipped), output(test-output), other(extension), + } + /// Parses one bounded output batch; failures fall back to built-in parsers. + parse: func(config: string, chunk: string) -> list<test-event>; +} diff --git a/wit/semantic.wit b/wit/semantic.wit index ec89fe405..586c324e6 100644 --- a/wit/semantic.wit +++ b/wit/semantic.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Optional export (world `plugin-semantic-tokens`): semantic colouring of /// documents of the languages declared with the `semantic-tokens` provider diff --git a/wit/types.wit b/wit/types.wit index 8114a2db0..4192e783c 100644 --- a/wit/types.wit +++ b/wit/types.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Shared types. Positions are 0-based lines and UTF-16 code unit columns, /// as in the editor and LSP. diff --git a/wit/views.wit b/wit/views.wit index 78da35354..a34be2d46 100644 --- a/wit/views.wit +++ b/wit/views.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; /// Declarative views: tool windows declared /// under `[[contributes.views]]` and custom editors under @@ -124,6 +124,31 @@ interface view-types { interface views { use types.{error}; use detail-types.{process-log}; + use process.{command}; + + /// An editor toolbar action; at most one action is the Save handler. + record content-action { id: string, label: string, icon: option<string>, primary: bool, save: bool } + /// A plugin-owned content editor, scoped by key to the invoking window. + record content-tab { + key: string, title: string, file-name: option<string>, language: option<string>, + content: string, read-only: bool, actions: list<content-action>, + } + /// A comparison opened by a content action reply. + record content-diff { left: string, right: string, left-label: option<string>, right-label: option<string>, title: option<string> } + /// Optional updates to an outstanding action or Save request. + record content-tab-reply { + content: option<string>, saved: option<bool>, read-only: option<bool>, + status: option<string>, error: option<string>, diff: option<content-diff>, + } + /// Opens or focuses a running session with this key; env is append-only. + /// Only from a window command/event; the id receives one terminal-exited. + open-terminal-session: func(key: string, title: string, cmd: command) -> result<string, error>; + /// Writes to this plugin's running keyed terminal in the invoking window. + write-terminal: func(key: string, text: string) -> result<_, error>; + /// Opens the existing content editor protocol without a web view. + open-content-tab: func(tab: content-tab) -> result<_, error>; + /// Answers content-tab-action in its originating window. + reply-content-tab: func(request-id: string, reply: content-tab-reply) -> result<_, error>; /// The view (or every custom editor of that id) shows outdated content: /// the IDE renders it again. diff --git a/wit/workspace.wit b/wit/workspace.wit index 556dd0da8..7c1e9dd19 100644 --- a/wit/workspace.wit +++ b/wit/workspace.wit @@ -1,4 +1,4 @@ -package openidea:plugin@1.3.0; +package openidea:plugin@1.4.0; interface workspace { use types.{error, location}; @@ -39,10 +39,28 @@ interface workspace { location: location, } + /// Text search options. Include/exclude are project-relative globs, as in Find in Files. + record text-query { + pattern: string, + regex: bool, + case-sensitive: bool, + whole-word: bool, + %include: list<string>, + exclude: list<string>, + } + /// A match with zero-based lines and UTF-16 columns, and its displayed line. + record text-match { location: location, line: string } + /// Bounded search results; limit-hit says there may be more matches. + record text-search { matches: list<text-match>, limit-hit: bool } + /// A project open in the IDE. resource project { /// Absolute path of the project root. Requires `project.read`. root: func() -> result<string, error>; + /// Find in Files with unsaved documents and project excludes. Requires project.read. + /// Cancellable, at most 10,000 matches. Zero limit returns no matches. + /// Line previews are capped at 4096 UTF-8 bytes, results at 4 MiB (limit-hit). + search-text: func(query: text-query, limit: u32) -> result<text-search, error>; /// Reads a UTF-8 text file inside the project (absolute, or relative /// to the root); paths leaving the root are refused. Unsaved editor /// contents win over the disk. A missing path is `not-found`. @@ -94,10 +112,22 @@ interface editor { text: string, } + /// An editor in the invoking window. + record open-editor { path: string, language: string, dirty: bool, active: bool } + /// Only from commands/events of a window. Requires editor.read. + open-editors: func() -> result<list<open-editor>, error>; + /// Open a project file (project.read) or a file in plugin-dir, selecting a range. + /// Only from commands/events of a window. Paths follow views.open-location. + open: func(path: string, selection: option<range>) -> result<_, error>; + /// A file open in an editor. resource text-editor { /// Absolute path of the file. path: func() -> string; + /// Replace selections in this window's editor. No capability; commands/events only. + set-selections: func(selections: list<range>) -> result<_, error>; + /// Scroll a range into view in this window's editor. No capability; commands/events only. + reveal: func(range: range) -> result<_, error>; /// Language id (`rust`, `solidity`, …). language: func() -> string; /// Current version and text. Requires `editor.read`. @@ -151,6 +181,22 @@ interface vcs { changed-files: u32, } + /// A commit in repository history; date is Unix seconds. + record commit-info { + id: string, short-id: string, author: string, author-email: string, + date: s64, subject: string, body: string, parents: list<string>, + } + /// One zero-based document line's attribution; date is Unix seconds. + record blame-line { line: u32, commit: string, author: string, date: s64, summary: string } + /// Newest commits first, at most 1000. Trusted projects only; requires vcs.read. + log: func(project: borrow<project>, path: option<string>, limit: u32) -> result<list<commit-info>, error>; + /// Includes unsaved text, with changed lines attributed to uncommitted changes. + /// Trusted projects only; requires vcs.read. Paths must be inside the project. + blame: func(project: borrow<project>, path: string) -> result<list<blame-line>, error>; + /// Working tree versus HEAD or against. None outside a repository. Disk text only. + /// Fails above 4 MiB. Trusted projects only; requires vcs.read. + diff: func(project: borrow<project>, path: option<string>, against: option<string>) -> result<option<string>, error>; + /// `none` when the project is not in a repository. Requires `vcs.read`. status: func(project: borrow<project>) -> result<option<repository-status>, error>; /// Configured remotes (`name`, fetch `url`), in order. Requires `vcs.read`.

w#wMdn)To8`lQ9Dkk2Q!H0-8=EdaZQSSCbVz3M1 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 literal 0 HcmV?d00001 diff --git a/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm.sources new file mode 100644 index 000000000..4813269ff --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/api14/dist/plugin.wasm.sources @@ -0,0 +1 @@ +b9b06665132bd6db753aa098703f2d83c9185d756114cdd1abb986f9d6d56c06 diff --git a/crates/ide-plugin/tests/fixtures/api14/plugin.toml b/crates/ide-plugin/tests/fixtures/api14/plugin.toml new file mode 100644 index 000000000..6bf897867 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/api14/plugin.toml @@ -0,0 +1,137 @@ +id = "api14" +name = "API 1.4 host tests" +version = "0.1.0" +engines.openidea = "^1.4" +wasm = "dist/plugin.wasm" +activation = ["on-command:*", "on-event:file-closed", "on-event:settings-changed", "on-event:input-mode-changed"] + +[capabilities] +required = ["storage"] +optional = ["ide.commands", "project.read", "vcs.read", "events", "events.input", "editor.read", "secrets", "net:localhost", "net.http:example.com", "ipc:$TMPDIR/api14-*"] + +[[contributes.secrets]] +name = "token" +label = "Token" + +[contributes.settings] +schema = "settings.json" + +[[contributes.commands]] +id = "ipc" +title = "ipc" + +[[contributes.commands]] +id = "fetch" +title = "fetch" + +[[contributes.commands]] +id = "window-status" +title = "window-status" + +[[contributes.commands]] +id = "remove-window-status" +title = "remove-window-status" + +[[contributes.commands]] +id = "global-status" +title = "global-status" + +[[contributes.commands]] +id = "execute" +title = "execute" + +[[contributes.commands]] +id = "oauth" +title = "oauth" + +[[contributes.commands]] +id = "secret" +title = "secret" + +[[contributes.commands]] +id = "delete-secret" +title = "delete-secret" + +[[contributes.commands]] +id = "undeclared-secret" +title = "undeclared-secret" + +[[contributes.commands]] +id = "output" +title = "output" + +[[contributes.commands]] +id = "events" +title = "events" + +[[contributes.commands]] +id = "process-run" +title = "process-run" + +[[contributes.commands]] +id = "process-run-command" +title = "process-run-command" + +[[contributes.commands]] +id = "process-start" +title = "process-start" + +[[contributes.commands]] +id = "terminal" +title = "terminal" + +[[contributes.commands]] +id = "log" +title = "log" + +[[contributes.commands]] +id = "open-editors" +title = "open-editors" + +[[contributes.commands]] +id = "open-editor" +title = "open-editor" + +[[contributes.commands]] +id = "set-selections" +title = "set-selections" + +[[contributes.commands]] +id = "reveal" +title = "reveal" + +[[contributes.commands]] +id = "search-text" +title = "search-text" + +[[contributes.commands]] +id = "problems" +title = "problems" + +[[contributes.commands]] +id = "decorations" +title = "decorations" + +[[contributes.commands]] +id = "clear-decorations" +title = "clear-decorations" + +[[contributes.commands]] +id = "file-decoration" +title = "file-decoration" + +[[contributes.commands]] +id = "clear-file-decorations" +title = "clear-file-decorations" + +[[contributes.commands]] +id = "vcs-log" +title = "vcs-log" + +[[contributes.commands]] +id = "vcs-blame" +title = "vcs-blame" + +[[contributes.commands]] +id = "vcs-diff" +title = "vcs-diff" diff --git a/crates/ide-plugin/tests/fixtures/api14/settings.json b/crates/ide-plugin/tests/fixtures/api14/settings.json new file mode 100644 index 000000000..9a6af9374 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/api14/settings.json @@ -0,0 +1 @@ +{"properties":{"enabled":{"type":"boolean","title":"Enabled","default":false}}} diff --git a/crates/ide-plugin/tests/fixtures/api14/src/lib.rs b/crates/ide-plugin/tests/fixtures/api14/src/lib.rs new file mode 100644 index 000000000..659f147e3 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/api14/src/lib.rs @@ -0,0 +1,158 @@ +use openidea_plugin::{Error, Event, Invocation, Plugin, StatusItem, host, register_plugin}; +#[derive(Default)] +struct Api14; +impl Plugin for Api14 { + fn run_command(&mut self, id: &str, ctx: &Invocation, arg: Option<&str>) -> Result, Error> { + let arg = arg.unwrap_or_default(); + match id { + "open-editors" => Ok(Some(format!("{:?}", host::open_editors()?))), + "open-editor" => { + host::open_editor(arg, Some(openidea_plugin::Range::default()))?; + Ok(None) + } + "set-selections" => { + ctx.editor().unwrap().set_selections(&[openidea_plugin::Range::default()])?; + Ok(None) + } + "reveal" => { + ctx.editor().unwrap().reveal(openidea_plugin::Range::default())?; + Ok(None) + } + "search-text" => { + let result = ctx + .project() + .unwrap() + .search_text(&openidea_plugin::TextQuery { pattern: arg.into(), ..Default::default() }, 10)?; + Ok(Some(format!("{result:?}"))) + } + "problems" => Ok(Some(format!("{:?}", host::problems(if arg.is_empty() { None } else { Some(arg) })?))), + "decorations" => { + host::set_decorations( + arg, + "test", + &[openidea_plugin::Decoration { + range: openidea_plugin::Range::default(), + style: "highlight".into(), + hover: Some("hover".into()), + after: Some("after".into()), + }], + )?; + Ok(None) + } + "clear-decorations" => { + host::clear_decorations("test")?; + Ok(None) + } + "file-decoration" => { + host::set_file_decoration( + arg, + "test", + &openidea_plugin::FileDecoration { badge: Some("OK".into()), style: "added".into(), tooltip: None }, + )?; + Ok(None) + } + "clear-file-decorations" => { + host::clear_file_decorations("test")?; + Ok(None) + } + "vcs-log" => Ok(Some(format!( + "{:?}", + ctx.project().unwrap().vcs_log(if arg.is_empty() { None } else { Some(arg) }, 10)? + ))), + "vcs-blame" => Ok(Some(format!("{:?}", ctx.project().unwrap().vcs_blame(arg)?))), + "vcs-diff" => ctx.project().unwrap().vcs_diff(if arg.is_empty() { None } else { Some(arg) }, None), + "ipc" => { + let stream = host::ipc_connect(arg)?; + stream.write(b"echo")?; + let bytes = stream.read(4, 2000)?; + stream.close()?; + Ok(Some(String::from_utf8_lossy(&bytes).into())) + } + "fetch" => { + let response = host::fetch(&openidea_plugin::HttpRequest::get(arg))?; + Ok(Some(String::from_utf8_lossy(&response.body).into())) + } + "global-status" | "window-status" => { + let item = StatusItem { text: arg.into(), tooltip: None, command: Some("events".into()) }; + if id == "global-status" { + host::set_status_item("state", &item); + } else { + host::set_window_status_item("state", &item)?; + } + Ok(None) + } + "remove-window-status" => { + host::remove_window_status_item("state")?; + Ok(None) + } + "execute" => host::execute_command(arg, Some("argument")), + "oauth" => { + let redirect = host::begin_loopback("oauth/callback")?; + host::output_append("oauth", &redirect.redirect_uri())?; + redirect.wait(2000) + } + "secret" => { + host::secret_set("token", arg)?; + Ok(None) + } + "delete-secret" => { + host::secret_delete("token")?; + Ok(None) + } + "undeclared-secret" => { + host::secret_set("undeclared", arg)?; + Ok(None) + } + "output" => { + host::output_append("test", arg)?; + host::output_reveal("test")?; + Ok(None) + } + "process-run" => Ok(Some(host::run_process("api14-tool", &[], None)?.stdout)), + "process-run-command" => Ok(Some(host::run_command(&host::Command::new("api14-tool"), None, None)?.stdout)), + "process-start" => { + let child = host::start(&host::Command::new("api14-tool"), None)?; + Ok(Some(child.wait(2000)?.stdout)) + } + "terminal" => { + host::open_terminal("API test", "api14-tool", &[])?; + Ok(None) + } + "log" => { + host::open_log_tab( + "api-test", + "API test", + &openidea_plugin::details::ProcessLog { + program: "api14-tool".into(), + args: Vec::new(), + toggles: Vec::new(), + }, + )?; + Ok(None) + } + "events" => host::storage::get("events"), + _ => Err(Error::NotFound(id.into())), + } + } + fn handle_events(&mut self, events: &[Event], ctx: &Invocation) { + let mut received: Vec = + host::storage::get("events").unwrap().map(|s| serde_json::from_str(&s).unwrap()).unwrap_or_default(); + for event in events { + if let Event::Other { kind, data } = event { + let editor = ctx.editor().map(|e| { + ( + e.path(), + e.selections() + .unwrap() + .into_iter() + .map(|r| [r.start.line, r.start.character, r.end.line, r.end.character]) + .collect::>(), + ) + }); + received.push(serde_json::json!({ "kind": kind, "data": data, "editor": editor })); + } + } + host::storage::set("events", &serde_json::to_string(&received).unwrap()).unwrap(); + } +} +register_plugin!(Api14); diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm index 4980186337908545963e5b70e5b62a222e1acb8b..8d716ad4a711c9da9c54cfcf23790304e210b4ba 100644 GIT binary patch delta 4999 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 diff --git a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources index 60b38a389..1dddda010 100644 --- a/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources +++ b/crates/ide-plugin/tests/fixtures/commit/dist/plugin.wasm.sources @@ -1 +1 @@ -6d7d707c7492d487fbd9debae791ad7e3755ac005f1f518efa54e6536ec59f08 +e986322ee0a7e8b09096deee3335e542823ad836cecdf14a4f44e2066fadf7b5 diff --git a/crates/ide-plugin/tests/fixtures/data-tools/Cargo.lock b/crates/ide-plugin/tests/fixtures/data-tools/Cargo.lock new file mode 100644 index 000000000..f1adf681a --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/data-tools/Cargo.lock @@ -0,0 +1,337 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "openidea-plugin" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "wit-bindgen", +] + +[[package]] +name = "openidea-plugin-data-tools" +version = "0.1.0" +dependencies = [ + "openidea-plugin", + "serde_json", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ff6668190d9fb70372b306b4f9fe8930f0e2f57ce72e4922bf2f117900949" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a987ab61d365382d25c7d47e26edd48941b3bf6e3fbf76eff918bc71dab06" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e122e390603abfacd548027016cd0113a6e2bf2f8ce6bdd3658b013456ea8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e26e13b9d972026778d2ffb7985736de116026fdc8d9464f966e12df2eb240" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a008a0d6b4840677326beb4edbeb210a2f1b7d0183fb7cc33f466b90fd7fb6" +dependencies = [ + "anyhow", + "macro-string", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078def49fdb49cab405c3006e05793b3fdaf7069126ba6b9c97d01ec8c98fe4c" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f0dc051466ffe46787721fca3d088536f158ae400be044f1b1c386ba9397ad" +dependencies = [ + "anyhow", + "hashbrown", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/ide-plugin/tests/fixtures/data-tools/Cargo.toml b/crates/ide-plugin/tests/fixtures/data-tools/Cargo.toml new file mode 100644 index 000000000..94b74bc10 --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/data-tools/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "openidea-plugin-data-tools" +version = "0.1.0" +edition = "2024" +publish = false + +[workspace] + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +openidea-plugin = { path = "../../../../../sdk/rust/openidea-plugin" } +serde_json = "1" + +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 +panic = "abort" diff --git a/crates/ide-plugin/tests/fixtures/data-tools/arguments.json b/crates/ide-plugin/tests/fixtures/data-tools/arguments.json new file mode 100644 index 000000000..ae6f5476b --- /dev/null +++ b/crates/ide-plugin/tests/fixtures/data-tools/arguments.json @@ -0,0 +1 @@ +{"type":"object","properties":{"text":{"type":"string"}},"additionalProperties":false} diff --git a/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm b/crates/ide-plugin/tests/fixtures/data-tools/dist/plugin.wasm new file mode 100644 index 0000000000000000000000000000000000000000..d921b44006a942abdb0c3ccc97a840cdd8d29393 GIT binary patch literal 173468 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